VUE_GabenParadise/node_modules/@firebase/firestore/dist/index.node.cjs.js

23017 lines
1004 KiB
JavaScript
Raw Normal View History

2020-08-10 12:35:19 +00:00
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
var tslib = require('tslib');
var firebase = _interopDefault(require('@firebase/app'));
var logger = require('@firebase/logger');
var util = require('@firebase/util');
var component = require('@firebase/component');
var crypto = require('crypto');
var util$1 = require('util');
var grpcJs = require('@grpc/grpc-js');
var package_json = require('@grpc/grpc-js/package.json');
var protoLoader = require('@grpc/proto-loader');
var path = require('path');
require('protobufjs');
/**
* @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. */
var SDK_VERSION = firebase.SDK_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.
*/
/**
* Provides singleton helpers where setup code can inject a platform at runtime.
* setPlatform needs to be set before Firestore is used and must be set exactly
* once.
*/
var PlatformSupport = /** @class */ (function () {
function PlatformSupport() {
}
PlatformSupport.setPlatform = function (platform) {
if (PlatformSupport.platform) {
fail('Platform already defined');
}
PlatformSupport.platform = platform;
};
PlatformSupport.getPlatform = function () {
if (!PlatformSupport.platform) {
fail('Platform not set');
}
return PlatformSupport.platform;
};
return PlatformSupport;
}());
/**
* @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.
*/
var logClient = new logger.Logger('@firebase/firestore');
// Helper methods are needed because variables can't be exported as read/write
function getLogLevel() {
return logClient.logLevel;
}
function setLogLevel(newLevel) {
logClient.logLevel = newLevel;
}
function logDebug(msg) {
var obj = [];
for (var _i = 1; _i < arguments.length; _i++) {
obj[_i - 1] = arguments[_i];
}
if (logClient.logLevel <= logger.LogLevel.DEBUG) {
var args = obj.map(argToString);
logClient.debug.apply(logClient, tslib.__spreadArrays(["Firestore (" + SDK_VERSION + "): " + msg], args));
}
}
function logError(msg) {
var obj = [];
for (var _i = 1; _i < arguments.length; _i++) {
obj[_i - 1] = arguments[_i];
}
if (logClient.logLevel <= logger.LogLevel.ERROR) {
var args = obj.map(argToString);
logClient.error.apply(logClient, tslib.__spreadArrays(["Firestore (" + SDK_VERSION + "): " + msg], args));
}
}
/**
* Converts an additional log parameter to a string representation.
*/
function argToString(obj) {
if (typeof obj === 'string') {
return obj;
}
else {
var platform = PlatformSupport.getPlatform();
try {
return platform.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.
*/
/**
* Unconditionally fails, throwing an Error with the given message.
* Messages are stripped in production builds.
*
* Returns `never` and can be used in expressions:
* @example
* let futureVar = fail('not implemented yet');
*/
function fail(failure) {
if (failure === void 0) { failure = 'Unexpected state'; }
// Log the failure in addition to throw an exception, just in case the
// exception is swallowed.
var message = "FIRESTORE (" + SDK_VERSION + ") INTERNAL ASSERTION FAILED: " + failure;
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);
}
/**
* Fails if the given assertion condition is false, throwing an Error with the
* given message if it did.
*
* Messages are stripped in production builds.
*/
function hardAssert(assertion, message) {
if (!assertion) {
fail(message);
}
}
/**
* 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.
*/
function debugAssert(assertion, message) {
if (!assertion) {
fail(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) {
debugAssert(obj instanceof constructor, "Expected type '" + constructor.name + "', but was '" + obj.constructor.name + "'");
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.
*/
var AutoId = /** @class */ (function () {
function AutoId() {
}
AutoId.newId = function () {
// Alphanumeric characters
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
// The largest byte value that is a multiple of `char.length`.
var maxMultiple = Math.floor(256 / chars.length) * chars.length;
debugAssert(0 < maxMultiple && maxMultiple < 256, "Expect maxMultiple to be (0, 256), but got " + maxMultiple);
var autoId = '';
var targetLength = 20;
while (autoId.length < targetLength) {
var bytes = PlatformSupport.getPlatform().randomBytes(40);
for (var 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);
}
}
}
debugAssert(autoId.length === targetLength, 'Invalid auto ID: ' + autoId);
return autoId;
};
return AutoId;
}());
function primitiveComparator(left, right) {
if (left < right) {
return -1;
}
if (left > right) {
return 1;
}
return 0;
}
/** Helper to compare arrays using isEqual(). */
function arrayEquals(left, right, comparator) {
if (left.length !== right.length) {
return false;
}
return left.every(function (value, index) { return comparator(value, right[index]); });
}
/**
* 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.
*/
var DatabaseInfo = /** @class */ (function () {
/**
* Constructs a DatabaseInfo using the provided host, databaseId and
* persistenceKey.
*
* @param databaseId The database to use.
* @param persistenceKey A unique identifier for this Firestore's local
* storage (used in conjunction with the databaseId).
* @param host The Firestore backend host to connect to.
* @param ssl Whether to use SSL when connecting.
* @param forceLongPolling Whether to use the forceLongPolling option
* when using WebChannel as the network transport.
*/
function DatabaseInfo(databaseId, persistenceKey, host, ssl, forceLongPolling) {
this.databaseId = databaseId;
this.persistenceKey = persistenceKey;
this.host = host;
this.ssl = ssl;
this.forceLongPolling = forceLongPolling;
}
return DatabaseInfo;
}());
/** The default database name for a project. */
var DEFAULT_DATABASE_NAME = '(default)';
/** Represents the database ID a Firestore client is associated with. */
var DatabaseId = /** @class */ (function () {
function DatabaseId(projectId, database) {
this.projectId = projectId;
this.database = database ? database : DEFAULT_DATABASE_NAME;
}
Object.defineProperty(DatabaseId.prototype, "isDefaultDatabase", {
get: function () {
return this.database === DEFAULT_DATABASE_NAME;
},
enumerable: true,
configurable: true
});
DatabaseId.prototype.isEqual = function (other) {
return (other instanceof DatabaseId &&
other.projectId === this.projectId &&
other.database === this.database);
};
DatabaseId.prototype.compareTo = function (other) {
return (primitiveComparator(this.projectId, other.projectId) ||
primitiveComparator(this.database, other.database));
};
return DatabaseId;
}());
/**
* @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.
*/
/**
* Simple wrapper around a nullable UID. Mostly exists to make code more
* readable.
*/
var User = /** @class */ (function () {
function User(uid) {
this.uid = uid;
}
User.prototype.isAuthenticated = function () {
return this.uid != null;
};
/**
* Returns a key representing this user, suitable for inclusion in a
* dictionary.
*/
User.prototype.toKey = function () {
if (this.isAuthenticated()) {
return 'uid:' + this.uid;
}
else {
return 'anonymous-user';
}
};
User.prototype.isEqual = function (otherUser) {
return otherUser.uid === this.uid;
};
return User;
}());
/** A user with a null UID. */
User.UNAUTHENTICATED = new User(null);
// TODO(mikelehen): Look into getting a proper uid-equivalent for
// non-FirebaseAuth providers.
User.GOOGLE_CREDENTIALS = new User('google-credentials-uid');
User.FIRST_PARTY = new User('first-party-uid');
/**
* @license
* Copyright 2018 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.
*/
/**
* `ListenSequence` is a monotonic sequence. It is initialized with a minimum value to
* exceed. All subsequent calls to next will return increasing values. If provided with a
* `SequenceNumberSyncer`, it will additionally bump its next value when told of a new value, as
* well as write out sequence numbers that it produces via `next()`.
*/
var ListenSequence = /** @class */ (function () {
function ListenSequence(previousValue, sequenceNumberSyncer) {
var _this = this;
this.previousValue = previousValue;
if (sequenceNumberSyncer) {
sequenceNumberSyncer.sequenceNumberHandler = function (sequenceNumber) { return _this.setPreviousValue(sequenceNumber); };
this.writeNewSequenceNumber = function (sequenceNumber) { return sequenceNumberSyncer.writeSequenceNumber(sequenceNumber); };
}
}
ListenSequence.prototype.setPreviousValue = function (externalPreviousValue) {
this.previousValue = Math.max(externalPreviousValue, this.previousValue);
return this.previousValue;
};
ListenSequence.prototype.next = function () {
var nextValue = ++this.previousValue;
if (this.writeNewSequenceNumber) {
this.writeNewSequenceNumber(nextValue);
}
return nextValue;
};
return ListenSequence;
}());
ListenSequence.INVALID = -1;
/**
* @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.
var SortedMap = /** @class */ (function () {
function SortedMap(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.
SortedMap.prototype.insert = function (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.
SortedMap.prototype.remove = function (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.
SortedMap.prototype.get = function (key) {
var node = this.root;
while (!node.isEmpty()) {
var 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.
SortedMap.prototype.indexOf = function (key) {
// Number of nodes that were pruned when descending right
var prunedNodes = 0;
var node = this.root;
while (!node.isEmpty()) {
var 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;
};
SortedMap.prototype.isEmpty = function () {
return this.root.isEmpty();
};
Object.defineProperty(SortedMap.prototype, "size", {
// Returns the total number of nodes in the map.
get: function () {
return this.root.size;
},
enumerable: true,
configurable: true
});
// Returns the minimum key in the map.
SortedMap.prototype.minKey = function () {
return this.root.minKey();
};
// Returns the maximum key in the map.
SortedMap.prototype.maxKey = function () {
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.
SortedMap.prototype.inorderTraversal = function (action) {
return this.root.inorderTraversal(action);
};
SortedMap.prototype.forEach = function (fn) {
this.inorderTraversal(function (k, v) {
fn(k, v);
return false;
});
};
SortedMap.prototype.toString = function () {
var descriptions = [];
this.inorderTraversal(function (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.
SortedMap.prototype.reverseTraversal = function (action) {
return this.root.reverseTraversal(action);
};
// Returns an iterator over the SortedMap.
SortedMap.prototype.getIterator = function () {
return new SortedMapIterator(this.root, null, this.comparator, false);
};
SortedMap.prototype.getIteratorFrom = function (key) {
return new SortedMapIterator(this.root, key, this.comparator, false);
};
SortedMap.prototype.getReverseIterator = function () {
return new SortedMapIterator(this.root, null, this.comparator, true);
};
SortedMap.prototype.getReverseIteratorFrom = function (key) {
return new SortedMapIterator(this.root, key, this.comparator, true);
};
return SortedMap;
}()); // end SortedMap
// An iterator over an LLRBNode.
var SortedMapIterator = /** @class */ (function () {
function SortedMapIterator(node, startKey, comparator, isReverse) {
this.isReverse = isReverse;
this.nodeStack = [];
var cmp = 1;
while (!node.isEmpty()) {
cmp = startKey ? comparator(node.key, startKey) : 1;
// flip the comparison if we're going in reverse
if (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;
}
}
}
}
SortedMapIterator.prototype.getNext = function () {
debugAssert(this.nodeStack.length > 0, 'getNext() called on iterator when hasNext() is false.');
var node = this.nodeStack.pop();
var 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;
};
SortedMapIterator.prototype.hasNext = function () {
return this.nodeStack.length > 0;
};
SortedMapIterator.prototype.peek = function () {
if (this.nodeStack.length === 0) {
return null;
}
var node = this.nodeStack[this.nodeStack.length - 1];
return { key: node.key, value: node.value };
};
return SortedMapIterator;
}()); // end SortedMapIterator
// Represents a node in a Left-leaning Red-Black tree.
var LLRBNode = /** @class */ (function () {
function LLRBNode(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.
LLRBNode.prototype.copy = function (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);
};
LLRBNode.prototype.isEmpty = function () {
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.
LLRBNode.prototype.inorderTraversal = function (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.
LLRBNode.prototype.reverseTraversal = function (action) {
return (this.right.reverseTraversal(action) ||
action(this.key, this.value) ||
this.left.reverseTraversal(action));
};
// Returns the minimum node in the tree.
LLRBNode.prototype.min = function () {
if (this.left.isEmpty()) {
return this;
}
else {
return this.left.min();
}
};
// Returns the maximum key in the tree.
LLRBNode.prototype.minKey = function () {
return this.min().key;
};
// Returns the maximum key in the tree.
LLRBNode.prototype.maxKey = function () {
if (this.right.isEmpty()) {
return this.key;
}
else {
return this.right.maxKey();
}
};
// Returns new tree, with the key/value added.
LLRBNode.prototype.insert = function (key, value, comparator) {
var n = this;
var 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();
};
LLRBNode.prototype.removeMin = function () {
if (this.left.isEmpty()) {
return LLRBNode.EMPTY;
}
var 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.
LLRBNode.prototype.remove = function (key, comparator) {
var smallest;
var 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();
};
LLRBNode.prototype.isRed = function () {
return this.color;
};
// Returns new tree after performing any needed rotations.
LLRBNode.prototype.fixUp = function () {
var 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;
};
LLRBNode.prototype.moveRedLeft = function () {
var 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;
};
LLRBNode.prototype.moveRedRight = function () {
var n = this.colorFlip();
if (n.left.left.isRed()) {
n = n.rotateRight();
n = n.colorFlip();
}
return n;
};
LLRBNode.prototype.rotateLeft = function () {
var nl = this.copy(null, null, LLRBNode.RED, null, this.right.left);
return this.right.copy(null, null, this.color, nl, null);
};
LLRBNode.prototype.rotateRight = function () {
var nr = this.copy(null, null, LLRBNode.RED, this.left.right, null);
return this.left.copy(null, null, this.color, null, nr);
};
LLRBNode.prototype.colorFlip = function () {
var left = this.left.copy(null, null, !this.left.color, null, null);
var right = this.right.copy(null, null, !this.right.color, null, null);
return this.copy(null, null, !this.color, left, right);
};
// For testing.
LLRBNode.prototype.checkMaxDepth = function () {
var 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.
LLRBNode.prototype.check = function () {
if (this.isRed() && this.left.isRed()) {
throw fail('Red node has red child(' + this.key + ',' + this.value + ')');
}
if (this.right.isRed()) {
throw fail('Right child of (' + this.key + ',' + this.value + ') is red');
}
var blackDepth = this.left.check();
if (blackDepth !== this.right.check()) {
throw fail('Black depths differ');
}
else {
return blackDepth + (this.isRed() ? 0 : 1);
}
};
return LLRBNode;
}()); // 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).
var LLRBEmptyNode = /** @class */ (function () {
function LLRBEmptyNode() {
this.size = 0;
}
Object.defineProperty(LLRBEmptyNode.prototype, "key", {
get: function () {
throw fail('LLRBEmptyNode has no key.');
},
enumerable: true,
configurable: true
});
Object.defineProperty(LLRBEmptyNode.prototype, "value", {
get: function () {
throw fail('LLRBEmptyNode has no value.');
},
enumerable: true,
configurable: true
});
Object.defineProperty(LLRBEmptyNode.prototype, "color", {
get: function () {
throw fail('LLRBEmptyNode has no color.');
},
enumerable: true,
configurable: true
});
Object.defineProperty(LLRBEmptyNode.prototype, "left", {
get: function () {
throw fail('LLRBEmptyNode has no left child.');
},
enumerable: true,
configurable: true
});
Object.defineProperty(LLRBEmptyNode.prototype, "right", {
get: function () {
throw fail('LLRBEmptyNode has no right child.');
},
enumerable: true,
configurable: true
});
// Returns a copy of the current node.
LLRBEmptyNode.prototype.copy = function (key, value, color, left, right) {
return this;
};
// Returns a copy of the tree, with the specified key/value added.
LLRBEmptyNode.prototype.insert = function (key, value, comparator) {
return new LLRBNode(key, value);
};
// Returns a copy of the tree, with the specified key removed.
LLRBEmptyNode.prototype.remove = function (key, comparator) {
return this;
};
LLRBEmptyNode.prototype.isEmpty = function () {
return true;
};
LLRBEmptyNode.prototype.inorderTraversal = function (action) {
return false;
};
LLRBEmptyNode.prototype.reverseTraversal = function (action) {
return false;
};
LLRBEmptyNode.prototype.minKey = function () {
return null;
};
LLRBEmptyNode.prototype.maxKey = function () {
return null;
};
LLRBEmptyNode.prototype.isRed = function () {
return false;
};
// For testing.
LLRBEmptyNode.prototype.checkMaxDepth = function () {
return true;
};
LLRBEmptyNode.prototype.check = function () {
return 0;
};
return LLRBEmptyNode;
}()); // 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!
*/
var SortedSet = /** @class */ (function () {
function SortedSet(comparator) {
this.comparator = comparator;
this.data = new SortedMap(this.comparator);
}
SortedSet.prototype.has = function (elem) {
return this.data.get(elem) !== null;
};
SortedSet.prototype.first = function () {
return this.data.minKey();
};
SortedSet.prototype.last = function () {
return this.data.maxKey();
};
Object.defineProperty(SortedSet.prototype, "size", {
get: function () {
return this.data.size;
},
enumerable: true,
configurable: true
});
SortedSet.prototype.indexOf = function (elem) {
return this.data.indexOf(elem);
};
/** Iterates elements in order defined by "comparator" */
SortedSet.prototype.forEach = function (cb) {
this.data.inorderTraversal(function (k, v) {
cb(k);
return false;
});
};
/** Iterates over `elem`s such that: range[0] <= elem < range[1]. */
SortedSet.prototype.forEachInRange = function (range, cb) {
var iter = this.data.getIteratorFrom(range[0]);
while (iter.hasNext()) {
var 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.
*/
SortedSet.prototype.forEachWhile = function (cb, start) {
var iter;
if (start !== undefined) {
iter = this.data.getIteratorFrom(start);
}
else {
iter = this.data.getIterator();
}
while (iter.hasNext()) {
var elem = iter.getNext();
var result = cb(elem.key);
if (!result) {
return;
}
}
};
/** Finds the least element greater than or equal to `elem`. */
SortedSet.prototype.firstAfterOrEqual = function (elem) {
var iter = this.data.getIteratorFrom(elem);
return iter.hasNext() ? iter.getNext().key : null;
};
SortedSet.prototype.getIterator = function () {
return new SortedSetIterator(this.data.getIterator());
};
SortedSet.prototype.getIteratorFrom = function (key) {
return new SortedSetIterator(this.data.getIteratorFrom(key));
};
/** Inserts or updates an element */
SortedSet.prototype.add = function (elem) {
return this.copy(this.data.remove(elem).insert(elem, true));
};
/** Deletes an element */
SortedSet.prototype.delete = function (elem) {
if (!this.has(elem)) {
return this;
}
return this.copy(this.data.remove(elem));
};
SortedSet.prototype.isEmpty = function () {
return this.data.isEmpty();
};
SortedSet.prototype.unionWith = function (other) {
var 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(function (elem) {
result = result.add(elem);
});
return result;
};
SortedSet.prototype.isEqual = function (other) {
if (!(other instanceof SortedSet)) {
return false;
}
if (this.size !== other.size) {
return false;
}
var thisIt = this.data.getIterator();
var otherIt = other.data.getIterator();
while (thisIt.hasNext()) {
var thisElem = thisIt.getNext().key;
var otherElem = otherIt.getNext().key;
if (this.comparator(thisElem, otherElem) !== 0) {
return false;
}
}
return true;
};
SortedSet.prototype.toArray = function () {
var res = [];
this.forEach(function (targetId) {
res.push(targetId);
});
return res;
};
SortedSet.prototype.toString = function () {
var result = [];
this.forEach(function (elem) { return result.push(elem); });
return 'SortedSet(' + result.toString() + ')';
};
SortedSet.prototype.copy = function (data) {
var result = new SortedSet(this.comparator);
result.data = data;
return result;
};
return SortedSet;
}());
var SortedSetIterator = /** @class */ (function () {
function SortedSetIterator(iter) {
this.iter = iter;
}
SortedSetIterator.prototype.getNext = function () {
return this.iter.getNext().key;
};
SortedSetIterator.prototype.hasNext = function () {
return this.iter.hasNext();
};
return SortedSetIterator;
}());
/**
* @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.
*/
var 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 can not 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 class used for Firestore-generated errors. Ideally we should be
* using FirebaseError, but integrating with it is overly arduous at the moment,
* so we define our own compatible error class (with a `name` of 'FirebaseError'
* and compatible `code` and `message` fields.)
*/
var FirestoreError = /** @class */ (function (_super) {
tslib.__extends(FirestoreError, _super);
function FirestoreError(code, message) {
var _this = _super.call(this, message) || this;
_this.code = code;
_this.message = message;
_this.name = 'FirebaseError';
// 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 = function () { return _this.name + ": [code=" + _this.code + "]: " + _this.message; };
return _this;
}
return FirestoreError;
}(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.
*/
var DOCUMENT_KEY_NAME = '__name__';
/**
* Path represents an ordered sequence of string segments.
*/
var BasePath = /** @class */ (function () {
function BasePath(segments, offset, length) {
if (offset === undefined) {
offset = 0;
}
else if (offset > segments.length) {
fail('offset ' + offset + ' out of range ' + segments.length);
}
if (length === undefined) {
length = segments.length - offset;
}
else if (length > segments.length - offset) {
fail('length ' + length + ' out of range ' + (segments.length - offset));
}
this.segments = segments;
this.offset = offset;
this.len = length;
}
Object.defineProperty(BasePath.prototype, "length", {
get: function () {
return this.len;
},
enumerable: true,
configurable: true
});
BasePath.prototype.isEqual = function (other) {
return BasePath.comparator(this, other) === 0;
};
BasePath.prototype.child = function (nameOrPath) {
var segments = this.segments.slice(this.offset, this.limit());
if (nameOrPath instanceof BasePath) {
nameOrPath.forEach(function (segment) {
segments.push(segment);
});
}
else {
segments.push(nameOrPath);
}
return this.construct(segments);
};
/** The index of one past the last segment of the path. */
BasePath.prototype.limit = function () {
return this.offset + this.length;
};
BasePath.prototype.popFirst = function (size) {
size = size === undefined ? 1 : size;
debugAssert(this.length >= size, "Can't call popFirst() with less segments");
return this.construct(this.segments, this.offset + size, this.length - size);
};
BasePath.prototype.popLast = function () {
debugAssert(!this.isEmpty(), "Can't call popLast() on empty path");
return this.construct(this.segments, this.offset, this.length - 1);
};
BasePath.prototype.firstSegment = function () {
debugAssert(!this.isEmpty(), "Can't call firstSegment() on empty path");
return this.segments[this.offset];
};
BasePath.prototype.lastSegment = function () {
return this.get(this.length - 1);
};
BasePath.prototype.get = function (index) {
debugAssert(index < this.length, 'Index out of range');
return this.segments[this.offset + index];
};
BasePath.prototype.isEmpty = function () {
return this.length === 0;
};
BasePath.prototype.isPrefixOf = function (other) {
if (other.length < this.length) {
return false;
}
for (var i = 0; i < this.length; i++) {
if (this.get(i) !== other.get(i)) {
return false;
}
}
return true;
};
BasePath.prototype.isImmediateParentOf = function (potentialChild) {
if (this.length + 1 !== potentialChild.length) {
return false;
}
for (var i = 0; i < this.length; i++) {
if (this.get(i) !== potentialChild.get(i)) {
return false;
}
}
return true;
};
BasePath.prototype.forEach = function (fn) {
for (var i = this.offset, end = this.limit(); i < end; i++) {
fn(this.segments[i]);
}
};
BasePath.prototype.toArray = function () {
return this.segments.slice(this.offset, this.limit());
};
BasePath.comparator = function (p1, p2) {
var len = Math.min(p1.length, p2.length);
for (var i = 0; i < len; i++) {
var left = p1.get(i);
var right = p2.get(i);
if (left < right) {
return -1;
}
if (left > right) {
return 1;
}
}
if (p1.length < p2.length) {
return -1;
}
if (p1.length > p2.length) {
return 1;
}
return 0;
};
return BasePath;
}());
/**
* A slash-separated path for navigating resources (documents and collections)
* within Firestore.
*/
var ResourcePath = /** @class */ (function (_super) {
tslib.__extends(ResourcePath, _super);
function ResourcePath() {
return _super !== null && _super.apply(this, arguments) || this;
}
ResourcePath.prototype.construct = function (segments, offset, length) {
return new ResourcePath(segments, offset, length);
};
ResourcePath.prototype.canonicalString = function () {
// 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('/');
};
ResourcePath.prototype.toString = function () {
return this.canonicalString();
};
/**
* Creates a resource path from the given slash-delimited string.
*/
ResourcePath.fromString = function (path) {
// 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).
if (path.indexOf('//') >= 0) {
throw new FirestoreError(Code.INVALID_ARGUMENT, "Invalid path (" + path + "). Paths must not contain // in them.");
}
// We may still have an empty segment at the beginning or end if they had a
// leading or trailing slash (which we allow).
var segments = path.split('/').filter(function (segment) { return segment.length > 0; });
return new ResourcePath(segments);
};
return ResourcePath;
}(BasePath));
ResourcePath.EMPTY_PATH = new ResourcePath([]);
var identifierRegExp = /^[_a-zA-Z][_a-zA-Z0-9]*$/;
/** A dot-separated path for navigating sub-objects within a document. */
var FieldPath = /** @class */ (function (_super) {
tslib.__extends(FieldPath, _super);
function FieldPath() {
return _super !== null && _super.apply(this, arguments) || this;
}
FieldPath.prototype.construct = function (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.
*/
FieldPath.isValidIdentifier = function (segment) {
return identifierRegExp.test(segment);
};
FieldPath.prototype.canonicalString = function () {
return this.toArray()
.map(function (str) {
str = str.replace('\\', '\\\\').replace('`', '\\`');
if (!FieldPath.isValidIdentifier(str)) {
str = '`' + str + '`';
}
return str;
})
.join('.');
};
FieldPath.prototype.toString = function () {
return this.canonicalString();
};
/**
* Returns true if this field references the key of a document.
*/
FieldPath.prototype.isKeyField = function () {
return this.length === 1 && this.get(0) === DOCUMENT_KEY_NAME;
};
/**
* The field designating the key of a document.
*/
FieldPath.keyField = function () {
return new FieldPath([DOCUMENT_KEY_NAME]);
};
/**
* Parses a field string from the given server-formatted string.
*
* - Splitting the empty string is not allowed (for now at least).
* - Empty segments within the string (e.g. if there are two consecutive
* separators) are not allowed.
*
* TODO(b/37244157): we should make this more strict. Right now, it allows
* non-identifier path components, even if they aren't escaped.
*/
FieldPath.fromServerFormat = function (path) {
var segments = [];
var current = '';
var i = 0;
var addCurrentSegment = function () {
if (current.length === 0) {
throw new FirestoreError(Code.INVALID_ARGUMENT, "Invalid field path (" + path + "). Paths must not be empty, begin " +
"with '.', end with '.', or contain '..'");
}
segments.push(current);
current = '';
};
var inBackticks = false;
while (i < path.length) {
var c = path[i];
if (c === '\\') {
if (i + 1 === path.length) {
throw new FirestoreError(Code.INVALID_ARGUMENT, 'Path has trailing escape character: ' + path);
}
var next = path[i + 1];
if (!(next === '\\' || next === '.' || next === '`')) {
throw new FirestoreError(Code.INVALID_ARGUMENT, 'Path has invalid escape sequence: ' + path);
}
current += next;
i += 2;
}
else if (c === '`') {
inBackticks = !inBackticks;
i++;
}
else if (c === '.' && !inBackticks) {
addCurrentSegment();
i++;
}
else {
current += c;
i++;
}
}
addCurrentSegment();
if (inBackticks) {
throw new FirestoreError(Code.INVALID_ARGUMENT, 'Unterminated ` in path: ' + path);
}
return new FieldPath(segments);
};
return FieldPath;
}(BasePath));
FieldPath.EMPTY_PATH = new FieldPath([]);
/**
* @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.
*/
var DocumentKey = /** @class */ (function () {
function DocumentKey(path) {
this.path = path;
debugAssert(DocumentKey.isDocumentKey(path), 'Invalid DocumentKey with an odd number of segments: ' +
path.toArray().join('/'));
}
DocumentKey.fromName = function (name) {
return new DocumentKey(ResourcePath.fromString(name).popFirst(5));
};
/** Returns true if the document is in the specified collectionId. */
DocumentKey.prototype.hasCollectionId = function (collectionId) {
return (this.path.length >= 2 &&
this.path.get(this.path.length - 2) === collectionId);
};
DocumentKey.prototype.isEqual = function (other) {
return (other !== null && ResourcePath.comparator(this.path, other.path) === 0);
};
DocumentKey.prototype.toString = function () {
return this.path.toString();
};
DocumentKey.comparator = function (k1, k2) {
return ResourcePath.comparator(k1.path, k2.path);
};
DocumentKey.isDocumentKey = function (path) {
return path.length % 2 === 0;
};
/**
* Creates and returns a new document key with the given segments.
*
* @param segments The segments of the path to the document
* @return A new instance of DocumentKey
*/
DocumentKey.fromSegments = function (segments) {
return new DocumentKey(new ResourcePath(segments.slice()));
};
return DocumentKey;
}());
DocumentKey.EMPTY = new DocumentKey(new ResourcePath([]));
/**
* @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.
*/
var EMPTY_MAYBE_DOCUMENT_MAP = new SortedMap(DocumentKey.comparator);
function maybeDocumentMap() {
return EMPTY_MAYBE_DOCUMENT_MAP;
}
function nullableMaybeDocumentMap() {
return maybeDocumentMap();
}
var EMPTY_DOCUMENT_MAP = new SortedMap(DocumentKey.comparator);
function documentMap() {
return EMPTY_DOCUMENT_MAP;
}
var EMPTY_DOCUMENT_VERSION_MAP = new SortedMap(DocumentKey.comparator);
function documentVersionMap() {
return EMPTY_DOCUMENT_VERSION_MAP;
}
var EMPTY_DOCUMENT_KEY_SET = new SortedSet(DocumentKey.comparator);
function documentKeySet() {
var keys = [];
for (var _i = 0; _i < arguments.length; _i++) {
keys[_i] = arguments[_i];
}
var set = EMPTY_DOCUMENT_KEY_SET;
for (var _e = 0, keys_1 = keys; _e < keys_1.length; _e++) {
var key = keys_1[_e];
set = set.add(key);
}
return set;
}
var EMPTY_TARGET_ID_SET = new SortedSet(primitiveComparator);
function targetIdSet() {
return EMPTY_TARGET_ID_SET;
}
/**
* @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 whether a variable is either undefined or null.
*/
function isNullOrUndefined(value) {
return value === null || value === undefined;
}
/** Returns whether the value represents -0. */
function isNegativeZero(value) {
// Detect if the value is -0.0. Based on polyfill from
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
return value === -0 && 1 / value === 1 / -0;
}
/**
* Returns whether a value is an integer and in the safe integer range
* @param value The value to test for being an integer and in the safe range
*/
function isSafeInteger(value) {
return (typeof value === 'number' &&
Number.isInteger(value) &&
!isNegativeZero(value) &&
value <= Number.MAX_SAFE_INTEGER &&
value >= Number.MIN_SAFE_INTEGER);
}
/**
* @license
* Copyright 2019 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 format of the LocalStorage key that stores the client state is:
// firestore_clients_<persistence_prefix>_<instance_key>
var CLIENT_STATE_KEY_PREFIX = 'firestore_clients';
/** Assembles the key for a client state in WebStorage */
function createWebStorageClientStateKey(persistenceKey, clientId) {
debugAssert(clientId.indexOf('_') === -1, "Client key cannot contain '_', but was '" + clientId + "'");
return CLIENT_STATE_KEY_PREFIX + "_" + persistenceKey + "_" + clientId;
}
// The format of the WebStorage key that stores the mutation state is:
// firestore_mutations_<persistence_prefix>_<batch_id>
// (for unauthenticated users)
// or: firestore_mutations_<persistence_prefix>_<batch_id>_<user_uid>
//
// 'user_uid' is last to avoid needing to escape '_' characters that it might
// contain.
var MUTATION_BATCH_KEY_PREFIX = 'firestore_mutations';
/** Assembles the key for a mutation batch in WebStorage */
function createWebStorageMutationBatchKey(persistenceKey, user, batchId) {
var mutationKey = MUTATION_BATCH_KEY_PREFIX + "_" + persistenceKey + "_" + batchId;
if (user.isAuthenticated()) {
mutationKey += "_" + user.uid;
}
return mutationKey;
}
// The format of the WebStorage key that stores a query target's metadata is:
// firestore_targets_<persistence_prefix>_<target_id>
var QUERY_TARGET_KEY_PREFIX = 'firestore_targets';
/** Assembles the key for a query state in WebStorage */
function createWebStorageQueryTargetMetadataKey(persistenceKey, targetId) {
return QUERY_TARGET_KEY_PREFIX + "_" + persistenceKey + "_" + targetId;
}
// The WebStorage prefix that stores the primary tab's online state. The
// format of the key is:
// firestore_online_state_<persistence_prefix>
var ONLINE_STATE_KEY_PREFIX = 'firestore_online_state';
/** Assembles the key for the online state of the primary tab. */
function createWebStorageOnlineStateKey(persistenceKey) {
return ONLINE_STATE_KEY_PREFIX + "_" + persistenceKey;
}
// The WebStorage key prefix for the key that stores the last sequence number allocated. The key
// looks like 'firestore_sequence_number_<persistence_prefix>'.
var SEQUENCE_NUMBER_KEY_PREFIX = 'firestore_sequence_number';
/** Assembles the key for the current sequence number. */
function createWebStorageSequenceNumberKey(persistenceKey) {
return SEQUENCE_NUMBER_KEY_PREFIX + "_" + persistenceKey;
}
/**
* @license
* Copyright 2018 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.
*/
var LOG_TAG = 'SharedClientState';
/**
* Holds the state of a mutation batch, including its user ID, batch ID and
* whether the batch is 'pending', 'acknowledged' or 'rejected'.
*/
// Visible for testing
var MutationMetadata = /** @class */ (function () {
function MutationMetadata(user, batchId, state, error) {
this.user = user;
this.batchId = batchId;
this.state = state;
this.error = error;
debugAssert((error !== undefined) === (state === 'rejected'), "MutationMetadata must contain an error iff state is 'rejected'");
}
/**
* Parses a MutationMetadata from its JSON representation in WebStorage.
* Logs a warning and returns null if the format of the data is not valid.
*/
MutationMetadata.fromWebStorageEntry = function (user, batchId, value) {
var mutationBatch = JSON.parse(value);
var validData = typeof mutationBatch === 'object' &&
['pending', 'acknowledged', 'rejected'].indexOf(mutationBatch.state) !==
-1 &&
(mutationBatch.error === undefined ||
typeof mutationBatch.error === 'object');
var firestoreError = undefined;
if (validData && mutationBatch.error) {
validData =
typeof mutationBatch.error.message === 'string' &&
typeof mutationBatch.error.code === 'string';
if (validData) {
firestoreError = new FirestoreError(mutationBatch.error.code, mutationBatch.error.message);
}
}
if (validData) {
return new MutationMetadata(user, batchId, mutationBatch.state, firestoreError);
}
else {
logError(LOG_TAG, "Failed to parse mutation state for ID '" + batchId + "': " + value);
return null;
}
};
MutationMetadata.prototype.toWebStorageJSON = function () {
var batchMetadata = {
state: this.state,
updateTimeMs: Date.now() // Modify the existing value to trigger update.
};
if (this.error) {
batchMetadata.error = {
code: this.error.code,
message: this.error.message
};
}
return JSON.stringify(batchMetadata);
};
return MutationMetadata;
}());
/**
* Holds the state of a query target, including its target ID and whether the
* target is 'not-current', 'current' or 'rejected'.
*/
// Visible for testing
var QueryTargetMetadata = /** @class */ (function () {
function QueryTargetMetadata(targetId, state, error) {
this.targetId = targetId;
this.state = state;
this.error = error;
debugAssert((error !== undefined) === (state === 'rejected'), "QueryTargetMetadata must contain an error iff state is 'rejected'");
}
/**
* Parses a QueryTargetMetadata from its JSON representation in WebStorage.
* Logs a warning and returns null if the format of the data is not valid.
*/
QueryTargetMetadata.fromWebStorageEntry = function (targetId, value) {
var targetState = JSON.parse(value);
var validData = typeof targetState === 'object' &&
['not-current', 'current', 'rejected'].indexOf(targetState.state) !==
-1 &&
(targetState.error === undefined ||
typeof targetState.error === 'object');
var firestoreError = undefined;
if (validData && targetState.error) {
validData =
typeof targetState.error.message === 'string' &&
typeof targetState.error.code === 'string';
if (validData) {
firestoreError = new FirestoreError(targetState.error.code, targetState.error.message);
}
}
if (validData) {
return new QueryTargetMetadata(targetId, targetState.state, firestoreError);
}
else {
logError(LOG_TAG, "Failed to parse target state for ID '" + targetId + "': " + value);
return null;
}
};
QueryTargetMetadata.prototype.toWebStorageJSON = function () {
var targetState = {
state: this.state,
updateTimeMs: Date.now() // Modify the existing value to trigger update.
};
if (this.error) {
targetState.error = {
code: this.error.code,
message: this.error.message
};
}
return JSON.stringify(targetState);
};
return QueryTargetMetadata;
}());
/**
* This class represents the immutable ClientState for a client read from
* WebStorage, containing the list of active query targets.
*/
var RemoteClientState = /** @class */ (function () {
function RemoteClientState(clientId, activeTargetIds) {
this.clientId = clientId;
this.activeTargetIds = activeTargetIds;
}
/**
* Parses a RemoteClientState from the JSON representation in WebStorage.
* Logs a warning and returns null if the format of the data is not valid.
*/
RemoteClientState.fromWebStorageEntry = function (clientId, value) {
var clientState = JSON.parse(value);
var validData = typeof clientState === 'object' &&
clientState.activeTargetIds instanceof Array;
var activeTargetIdsSet = targetIdSet();
for (var i = 0; validData && i < clientState.activeTargetIds.length; ++i) {
validData = isSafeInteger(clientState.activeTargetIds[i]);
activeTargetIdsSet = activeTargetIdsSet.add(clientState.activeTargetIds[i]);
}
if (validData) {
return new RemoteClientState(clientId, activeTargetIdsSet);
}
else {
logError(LOG_TAG, "Failed to parse client data for instance '" + clientId + "': " + value);
return null;
}
};
return RemoteClientState;
}());
/**
* This class represents the online state for all clients participating in
* multi-tab. The online state is only written to by the primary client, and
* used in secondary clients to update their query views.
*/
var SharedOnlineState = /** @class */ (function () {
function SharedOnlineState(clientId, onlineState) {
this.clientId = clientId;
this.onlineState = onlineState;
}
/**
* Parses a SharedOnlineState from its JSON representation in WebStorage.
* Logs a warning and returns null if the format of the data is not valid.
*/
SharedOnlineState.fromWebStorageEntry = function (value) {
var onlineState = JSON.parse(value);
var validData = typeof onlineState === 'object' &&
['Unknown', 'Online', 'Offline'].indexOf(onlineState.onlineState) !==
-1 &&
typeof onlineState.clientId === 'string';
if (validData) {
return new SharedOnlineState(onlineState.clientId, onlineState.onlineState);
}
else {
logError(LOG_TAG, "Failed to parse online state: " + value);
return null;
}
};
return SharedOnlineState;
}());
/**
* Metadata state of the local client. Unlike `RemoteClientState`, this class is
* mutable and keeps track of all pending mutations, which allows us to
* update the range of pending mutation batch IDs as new mutations are added or
* removed.
*
* The data in `LocalClientState` is not read from WebStorage and instead
* updated via its instance methods. The updated state can be serialized via
* `toWebStorageJSON()`.
*/
// Visible for testing.
var LocalClientState = /** @class */ (function () {
function LocalClientState() {
this.activeTargetIds = targetIdSet();
}
LocalClientState.prototype.addQueryTarget = function (targetId) {
this.activeTargetIds = this.activeTargetIds.add(targetId);
};
LocalClientState.prototype.removeQueryTarget = function (targetId) {
this.activeTargetIds = this.activeTargetIds.delete(targetId);
};
/**
* Converts this entry into a JSON-encoded format we can use for WebStorage.
* Does not encode `clientId` as it is part of the key in WebStorage.
*/
LocalClientState.prototype.toWebStorageJSON = function () {
var data = {
activeTargetIds: this.activeTargetIds.toArray(),
updateTimeMs: Date.now() // Modify the existing value to trigger update.
};
return JSON.stringify(data);
};
return LocalClientState;
}());
/**
* `WebStorageSharedClientState` uses WebStorage (window.localStorage) as the
* backing store for the SharedClientState. It keeps track of all active
* clients and supports modifications of the local client's data.
*/
var WebStorageSharedClientState = /** @class */ (function () {
function WebStorageSharedClientState(queue, platform, persistenceKey, localClientId, initialUser) {
this.queue = queue;
this.platform = platform;
this.persistenceKey = persistenceKey;
this.localClientId = localClientId;
this.syncEngine = null;
this.onlineStateHandler = null;
this.sequenceNumberHandler = null;
this.storageListener = this.handleWebStorageEvent.bind(this);
this.activeClients = new SortedMap(primitiveComparator);
this.started = false;
/**
* Captures WebStorage events that occur before `start()` is called. These
* events are replayed once `WebStorageSharedClientState` is started.
*/
this.earlyEvents = [];
if (!WebStorageSharedClientState.isAvailable(this.platform)) {
throw new FirestoreError(Code.UNIMPLEMENTED, 'LocalStorage is not available on this platform.');
}
// Escape the special characters mentioned here:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
var escapedPersistenceKey = persistenceKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
this.storage = this.platform.window.localStorage;
this.currentUser = initialUser;
this.localClientStorageKey = createWebStorageClientStateKey(this.persistenceKey, this.localClientId);
this.sequenceNumberKey = createWebStorageSequenceNumberKey(this.persistenceKey);
this.activeClients = this.activeClients.insert(this.localClientId, new LocalClientState());
this.clientStateKeyRe = new RegExp("^" + CLIENT_STATE_KEY_PREFIX + "_" + escapedPersistenceKey + "_([^_]*)$");
this.mutationBatchKeyRe = new RegExp("^" + MUTATION_BATCH_KEY_PREFIX + "_" + escapedPersistenceKey + "_(\\d+)(?:_(.*))?$");
this.queryTargetKeyRe = new RegExp("^" + QUERY_TARGET_KEY_PREFIX + "_" + escapedPersistenceKey + "_(\\d+)$");
this.onlineStateKey = createWebStorageOnlineStateKey(this.persistenceKey);
// Rather than adding the storage observer during start(), we add the
// storage observer during initialization. This ensures that we collect
// events before other components populate their initial state (during their
// respective start() calls). Otherwise, we might for example miss a
// mutation that is added after LocalStore's start() processed the existing
// mutations but before we observe WebStorage events.
this.platform.window.addEventListener('storage', this.storageListener);
}
/** Returns 'true' if WebStorage is available in the current environment. */
WebStorageSharedClientState.isAvailable = function (platform) {
return !!(platform.window && platform.window.localStorage != null);
};
WebStorageSharedClientState.prototype.start = function () {
return tslib.__awaiter(this, void 0, void 0, function () {
var existingClients, _i, existingClients_1, clientId, storageItem, clientState, onlineStateJSON, onlineState, _e, _f, event_1;
var _this = this;
return tslib.__generator(this, function (_g) {
switch (_g.label) {
case 0:
debugAssert(!this.started, 'WebStorageSharedClientState already started');
debugAssert(this.syncEngine !== null, 'syncEngine property must be set before calling start()');
debugAssert(this.onlineStateHandler !== null, 'onlineStateHandler property must be set before calling start()');
return [4 /*yield*/, this.syncEngine.getActiveClients()];
case 1:
existingClients = _g.sent();
for (_i = 0, existingClients_1 = existingClients; _i < existingClients_1.length; _i++) {
clientId = existingClients_1[_i];
if (clientId === this.localClientId) {
continue;
}
storageItem = this.getItem(createWebStorageClientStateKey(this.persistenceKey, clientId));
if (storageItem) {
clientState = RemoteClientState.fromWebStorageEntry(clientId, storageItem);
if (clientState) {
this.activeClients = this.activeClients.insert(clientState.clientId, clientState);
}
}
}
this.persistClientState();
onlineStateJSON = this.storage.getItem(this.onlineStateKey);
if (onlineStateJSON) {
onlineState = this.fromWebStorageOnlineState(onlineStateJSON);
if (onlineState) {
this.handleOnlineStateEvent(onlineState);
}
}
for (_e = 0, _f = this.earlyEvents; _e < _f.length; _e++) {
event_1 = _f[_e];
this.handleWebStorageEvent(event_1);
}
this.earlyEvents = [];
// Register a window unload hook to remove the client metadata entry from
// WebStorage even if `shutdown()` was not called.
this.platform.window.addEventListener('unload', function () { return _this.shutdown(); });
this.started = true;
return [2 /*return*/];
}
});
});
};
WebStorageSharedClientState.prototype.writeSequenceNumber = function (sequenceNumber) {
this.setItem(this.sequenceNumberKey, JSON.stringify(sequenceNumber));
};
WebStorageSharedClientState.prototype.getAllActiveQueryTargets = function () {
return this.extractActiveQueryTargets(this.activeClients);
};
WebStorageSharedClientState.prototype.isActiveQueryTarget = function (targetId) {
var found = false;
this.activeClients.forEach(function (key, value) {
if (value.activeTargetIds.has(targetId)) {
found = true;
}
});
return found;
};
WebStorageSharedClientState.prototype.addPendingMutation = function (batchId) {
this.persistMutationState(batchId, 'pending');
};
WebStorageSharedClientState.prototype.updateMutationState = function (batchId, state, error) {
this.persistMutationState(batchId, state, error);
// Once a final mutation result is observed by other clients, they no longer
// access the mutation's metadata entry. Since WebStorage replays events
// in order, it is safe to delete the entry right after updating it.
this.removeMutationState(batchId);
};
WebStorageSharedClientState.prototype.addLocalQueryTarget = function (targetId) {
var queryState = 'not-current';
// Lookup an existing query state if the target ID was already registered
// by another tab
if (this.isActiveQueryTarget(targetId)) {
var storageItem = this.storage.getItem(createWebStorageQueryTargetMetadataKey(this.persistenceKey, targetId));
if (storageItem) {
var metadata = QueryTargetMetadata.fromWebStorageEntry(targetId, storageItem);
if (metadata) {
queryState = metadata.state;
}
}
}
this.localClientState.addQueryTarget(targetId);
this.persistClientState();
return queryState;
};
WebStorageSharedClientState.prototype.removeLocalQueryTarget = function (targetId) {
this.localClientState.removeQueryTarget(targetId);
this.persistClientState();
};
WebStorageSharedClientState.prototype.isLocalQueryTarget = function (targetId) {
return this.localClientState.activeTargetIds.has(targetId);
};
WebStorageSharedClientState.prototype.clearQueryState = function (targetId) {
this.removeItem(createWebStorageQueryTargetMetadataKey(this.persistenceKey, targetId));
};
WebStorageSharedClientState.prototype.updateQueryState = function (targetId, state, error) {
this.persistQueryTargetState(targetId, state, error);
};
WebStorageSharedClientState.prototype.handleUserChange = function (user, removedBatchIds, addedBatchIds) {
var _this = this;
removedBatchIds.forEach(function (batchId) {
_this.removeMutationState(batchId);
});
this.currentUser = user;
addedBatchIds.forEach(function (batchId) {
_this.addPendingMutation(batchId);
});
};
WebStorageSharedClientState.prototype.setOnlineState = function (onlineState) {
this.persistOnlineState(onlineState);
};
WebStorageSharedClientState.prototype.shutdown = function () {
if (this.started) {
this.platform.window.removeEventListener('storage', this.storageListener);
this.removeItem(this.localClientStorageKey);
this.started = false;
}
};
WebStorageSharedClientState.prototype.getItem = function (key) {
var value = this.storage.getItem(key);
logDebug(LOG_TAG, 'READ', key, value);
return value;
};
WebStorageSharedClientState.prototype.setItem = function (key, value) {
logDebug(LOG_TAG, 'SET', key, value);
this.storage.setItem(key, value);
};
WebStorageSharedClientState.prototype.removeItem = function (key) {
logDebug(LOG_TAG, 'REMOVE', key);
this.storage.removeItem(key);
};
WebStorageSharedClientState.prototype.handleWebStorageEvent = function (event) {
var _this = this;
if (event.storageArea === this.storage) {
logDebug(LOG_TAG, 'EVENT', event.key, event.newValue);
if (event.key === this.localClientStorageKey) {
logError('Received WebStorage notification for local change. Another client might have ' +
'garbage-collected our state');
return;
}
this.queue.enqueueRetryable(function () { return tslib.__awaiter(_this, void 0, void 0, function () {
var clientState, clientId, mutationMetadata, queryTargetMetadata, onlineState, sequenceNumber;
return tslib.__generator(this, function (_e) {
if (!this.started) {
this.earlyEvents.push(event);
return [2 /*return*/];
}
if (event.key === null) {
return [2 /*return*/];
}
if (this.clientStateKeyRe.test(event.key)) {
if (event.newValue != null) {
clientState = this.fromWebStorageClientState(event.key, event.newValue);
if (clientState) {
return [2 /*return*/, this.handleClientStateEvent(clientState.clientId, clientState)];
}
}
else {
clientId = this.fromWebStorageClientStateKey(event.key);
return [2 /*return*/, this.handleClientStateEvent(clientId, null)];
}
}
else if (this.mutationBatchKeyRe.test(event.key)) {
if (event.newValue !== null) {
mutationMetadata = this.fromWebStorageMutationMetadata(event.key, event.newValue);
if (mutationMetadata) {
return [2 /*return*/, this.handleMutationBatchEvent(mutationMetadata)];
}
}
}
else if (this.queryTargetKeyRe.test(event.key)) {
if (event.newValue !== null) {
queryTargetMetadata = this.fromWebStorageQueryTargetMetadata(event.key, event.newValue);
if (queryTargetMetadata) {
return [2 /*return*/, this.handleQueryTargetEvent(queryTargetMetadata)];
}
}
}
else if (event.key === this.onlineStateKey) {
if (event.newValue !== null) {
onlineState = this.fromWebStorageOnlineState(event.newValue);
if (onlineState) {
return [2 /*return*/, this.handleOnlineStateEvent(onlineState)];
}
}
}
else if (event.key === this.sequenceNumberKey) {
debugAssert(!!this.sequenceNumberHandler, 'Missing sequenceNumberHandler');
sequenceNumber = fromWebStorageSequenceNumber(event.newValue);
if (sequenceNumber !== ListenSequence.INVALID) {
this.sequenceNumberHandler(sequenceNumber);
}
}
return [2 /*return*/];
});
}); });
}
};
Object.defineProperty(WebStorageSharedClientState.prototype, "localClientState", {
get: function () {
return this.activeClients.get(this.localClientId);
},
enumerable: true,
configurable: true
});
WebStorageSharedClientState.prototype.persistClientState = function () {
this.setItem(this.localClientStorageKey, this.localClientState.toWebStorageJSON());
};
WebStorageSharedClientState.prototype.persistMutationState = function (batchId, state, error) {
var mutationState = new MutationMetadata(this.currentUser, batchId, state, error);
var mutationKey = createWebStorageMutationBatchKey(this.persistenceKey, this.currentUser, batchId);
this.setItem(mutationKey, mutationState.toWebStorageJSON());
};
WebStorageSharedClientState.prototype.removeMutationState = function (batchId) {
var mutationKey = createWebStorageMutationBatchKey(this.persistenceKey, this.currentUser, batchId);
this.removeItem(mutationKey);
};
WebStorageSharedClientState.prototype.persistOnlineState = function (onlineState) {
var entry = {
clientId: this.localClientId,
onlineState: onlineState
};
this.storage.setItem(this.onlineStateKey, JSON.stringify(entry));
};
WebStorageSharedClientState.prototype.persistQueryTargetState = function (targetId, state, error) {
var targetKey = createWebStorageQueryTargetMetadataKey(this.persistenceKey, targetId);
var targetMetadata = new QueryTargetMetadata(targetId, state, error);
this.setItem(targetKey, targetMetadata.toWebStorageJSON());
};
/**
* Parses a client state key in WebStorage. Returns null if the key does not
* match the expected key format.
*/
WebStorageSharedClientState.prototype.fromWebStorageClientStateKey = function (key) {
var match = this.clientStateKeyRe.exec(key);
return match ? match[1] : null;
};
/**
* Parses a client state in WebStorage. Returns 'null' if the value could not
* be parsed.
*/
WebStorageSharedClientState.prototype.fromWebStorageClientState = function (key, value) {
var clientId = this.fromWebStorageClientStateKey(key);
debugAssert(clientId !== null, "Cannot parse client state key '" + key + "'");
return RemoteClientState.fromWebStorageEntry(clientId, value);
};
/**
* Parses a mutation batch state in WebStorage. Returns 'null' if the value
* could not be parsed.
*/
WebStorageSharedClientState.prototype.fromWebStorageMutationMetadata = function (key, value) {
var match = this.mutationBatchKeyRe.exec(key);
debugAssert(match !== null, "Cannot parse mutation batch key '" + key + "'");
var batchId = Number(match[1]);
var userId = match[2] !== undefined ? match[2] : null;
return MutationMetadata.fromWebStorageEntry(new User(userId), batchId, value);
};
/**
* Parses a query target state from WebStorage. Returns 'null' if the value
* could not be parsed.
*/
WebStorageSharedClientState.prototype.fromWebStorageQueryTargetMetadata = function (key, value) {
var match = this.queryTargetKeyRe.exec(key);
debugAssert(match !== null, "Cannot parse query target key '" + key + "'");
var targetId = Number(match[1]);
return QueryTargetMetadata.fromWebStorageEntry(targetId, value);
};
/**
* Parses an online state from WebStorage. Returns 'null' if the value
* could not be parsed.
*/
WebStorageSharedClientState.prototype.fromWebStorageOnlineState = function (value) {
return SharedOnlineState.fromWebStorageEntry(value);
};
WebStorageSharedClientState.prototype.handleMutationBatchEvent = function (mutationBatch) {
return tslib.__awaiter(this, void 0, void 0, function () {
return tslib.__generator(this, function (_e) {
if (mutationBatch.user.uid !== this.currentUser.uid) {
logDebug(LOG_TAG, "Ignoring mutation for non-active user " + mutationBatch.user.uid);
return [2 /*return*/];
}
return [2 /*return*/, this.syncEngine.applyBatchState(mutationBatch.batchId, mutationBatch.state, mutationBatch.error)];
});
});
};
WebStorageSharedClientState.prototype.handleQueryTargetEvent = function (targetMetadata) {
return this.syncEngine.applyTargetState(targetMetadata.targetId, targetMetadata.state, targetMetadata.error);
};
WebStorageSharedClientState.prototype.handleClientStateEvent = function (clientId, clientState) {
var _this = this;
var updatedClients = clientState
? this.activeClients.insert(clientId, clientState)
: this.activeClients.remove(clientId);
var existingTargets = this.extractActiveQueryTargets(this.activeClients);
var newTargets = this.extractActiveQueryTargets(updatedClients);
var addedTargets = [];
var removedTargets = [];
newTargets.forEach(function (targetId) {
if (!existingTargets.has(targetId)) {
addedTargets.push(targetId);
}
});
existingTargets.forEach(function (targetId) {
if (!newTargets.has(targetId)) {
removedTargets.push(targetId);
}
});
return this.syncEngine.applyActiveTargetsChange(addedTargets, removedTargets).then(function () {
_this.activeClients = updatedClients;
});
};
WebStorageSharedClientState.prototype.handleOnlineStateEvent = function (onlineState) {
// We check whether the client that wrote this online state is still active
// by comparing its client ID to the list of clients kept active in
// IndexedDb. If a client does not update their IndexedDb client state
// within 5 seconds, it is considered inactive and we don't emit an online
// state event.
if (this.activeClients.get(onlineState.clientId)) {
this.onlineStateHandler(onlineState.onlineState);
}
};
WebStorageSharedClientState.prototype.extractActiveQueryTargets = function (clients) {
var activeTargets = targetIdSet();
clients.forEach(function (kev, value) {
activeTargets = activeTargets.unionWith(value.activeTargetIds);
});
return activeTargets;
};
return WebStorageSharedClientState;
}());
function fromWebStorageSequenceNumber(seqString) {
var sequenceNumber = ListenSequence.INVALID;
if (seqString != null) {
try {
var parsed = JSON.parse(seqString);
hardAssert(typeof parsed === 'number', 'Found non-numeric sequence number');
sequenceNumber = parsed;
}
catch (e) {
logError(LOG_TAG, 'Failed to read sequence number from WebStorage', e);
}
}
return sequenceNumber;
}
/**
* `MemorySharedClientState` is a simple implementation of SharedClientState for
* clients using memory persistence. The state in this class remains fully
* isolated and no synchronization is performed.
*/
var MemorySharedClientState = /** @class */ (function () {
function MemorySharedClientState() {
this.localState = new LocalClientState();
this.queryState = {};
this.syncEngine = null;
this.onlineStateHandler = null;
this.sequenceNumberHandler = null;
}
MemorySharedClientState.prototype.addPendingMutation = function (batchId) {
// No op.
};
MemorySharedClientState.prototype.updateMutationState = function (batchId, state, error) {
// No op.
};
MemorySharedClientState.prototype.addLocalQueryTarget = function (targetId) {
this.localState.addQueryTarget(targetId);
return this.queryState[targetId] || 'not-current';
};
MemorySharedClientState.prototype.updateQueryState = function (targetId, state, error) {
this.queryState[targetId] = state;
};
MemorySharedClientState.prototype.removeLocalQueryTarget = function (targetId) {
this.localState.removeQueryTarget(targetId);
};
MemorySharedClientState.prototype.isLocalQueryTarget = function (targetId) {
return this.localState.activeTargetIds.has(targetId);
};
MemorySharedClientState.prototype.clearQueryState = function (targetId) {
delete this.queryState[targetId];
};
MemorySharedClientState.prototype.getAllActiveQueryTargets = function () {
return this.localState.activeTargetIds;
};
MemorySharedClientState.prototype.isActiveQueryTarget = function (targetId) {
return this.localState.activeTargetIds.has(targetId);
};
MemorySharedClientState.prototype.start = function () {
this.localState = new LocalClientState();
return Promise.resolve();
};
MemorySharedClientState.prototype.handleUserChange = function (user, removedBatchIds, addedBatchIds) {
// No op.
};
MemorySharedClientState.prototype.setOnlineState = function (onlineState) {
// No op.
};
MemorySharedClientState.prototype.shutdown = function () { };
MemorySharedClientState.prototype.writeSequenceNumber = function (sequenceNumber) { };
return MemorySharedClientState;
}());
/**
* @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 earlist date supported by Firestore timestamps (0001-01-01T00:00:00Z).
var MIN_SECONDS = -62135596800;
var Timestamp = /** @class */ (function () {
function Timestamp(seconds, nanoseconds) {
this.seconds = seconds;
this.nanoseconds = nanoseconds;
if (nanoseconds < 0) {
throw new FirestoreError(Code.INVALID_ARGUMENT, 'Timestamp nanoseconds out of range: ' + nanoseconds);
}
if (nanoseconds >= 1e9) {
throw new FirestoreError(Code.INVALID_ARGUMENT, 'Timestamp nanoseconds out of range: ' + nanoseconds);
}
if (seconds < MIN_SECONDS) {
throw new FirestoreError(Code.INVALID_ARGUMENT, 'Timestamp seconds out of range: ' + seconds);
}
// This will break in the year 10,000.
if (seconds >= 253402300800) {
throw new FirestoreError(Code.INVALID_ARGUMENT, 'Timestamp seconds out of range: ' + seconds);
}
}
Timestamp.now = function () {
return Timestamp.fromMillis(Date.now());
};
Timestamp.fromDate = function (date) {
return Timestamp.fromMillis(date.getTime());
};
Timestamp.fromMillis = function (milliseconds) {
var seconds = Math.floor(milliseconds / 1000);
var nanos = (milliseconds - seconds * 1000) * 1e6;
return new Timestamp(seconds, nanos);
};
Timestamp.prototype.toDate = function () {
return new Date(this.toMillis());
};
Timestamp.prototype.toMillis = function () {
return this.seconds * 1000 + this.nanoseconds / 1e6;
};
Timestamp.prototype._compareTo = function (other) {
if (this.seconds === other.seconds) {
return primitiveComparator(this.nanoseconds, other.nanoseconds);
}
return primitiveComparator(this.seconds, other.seconds);
};
Timestamp.prototype.isEqual = function (other) {
return (other.seconds === this.seconds && other.nanoseconds === this.nanoseconds);
};
Timestamp.prototype.toString = function () {
return ('Timestamp(seconds=' +
this.seconds +
', nanoseconds=' +
this.nanoseconds +
')');
};
Timestamp.prototype.valueOf = function () {
// This method returns a string of the form <seconds>.<nanoseconds> where <seconds> is
// translated to have a non-negative value and both <seconds> and <nanoseconds> are left-padded
// with zeroes to be a consistent length. Strings with this format then have a lexiographical
// ordering that matches the expected ordering. The <seconds> translation is done to avoid
// having a leading negative sign (i.e. a leading '-' character) in its string representation,
// which would affect its lexiographical ordering.
var adjustedSeconds = this.seconds - MIN_SECONDS;
// Note: Up to 12 decimal digits are required to represent all valid 'seconds' values.
var formattedSeconds = String(adjustedSeconds).padStart(12, '0');
var formattedNanoseconds = String(this.nanoseconds).padStart(9, '0');
return formattedSeconds + '.' + formattedNanoseconds;
};
return Timestamp;
}());
/**
* @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 version of a document in Firestore. This corresponds to the version
* timestamp, such as update_time or read_time.
*/
var SnapshotVersion = /** @class */ (function () {
function SnapshotVersion(timestamp) {
this.timestamp = timestamp;
}
SnapshotVersion.fromTimestamp = function (value) {
return new SnapshotVersion(value);
};
SnapshotVersion.min = function () {
return new SnapshotVersion(new Timestamp(0, 0));
};
SnapshotVersion.prototype.compareTo = function (other) {
return this.timestamp._compareTo(other.timestamp);
};
SnapshotVersion.prototype.isEqual = function (other) {
return this.timestamp.isEqual(other.timestamp);
};
/** Returns a number representation of the version for use in spec tests. */
SnapshotVersion.prototype.toMicroseconds = function () {
// Convert to microseconds.
return this.timestamp.seconds * 1e6 + this.timestamp.nanoseconds / 1000;
};
SnapshotVersion.prototype.toString = function () {
return 'SnapshotVersion(' + this.timestamp.toString() + ')';
};
SnapshotVersion.prototype.toTimestamp = function () {
return this.timestamp;
};
return SnapshotVersion;
}());
/**
* @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 objectSize(obj) {
var count = 0;
for (var key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
count++;
}
}
return count;
}
function forEach(obj, fn) {
for (var key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
fn(key, obj[key]);
}
}
}
function isEmpty(obj) {
debugAssert(obj != null && typeof obj === 'object', 'isEmpty() expects object parameter.');
for (var key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
return false;
}
}
return true;
}
/**
* @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.
*/
/**
* Immutable class that represents a "proto" byte string.
*
* Proto byte strings can either be Base64-encoded strings or Uint8Arrays when
* sent on the wire. This class abstracts away this differentiation by holding
* the proto byte string in a common class that must be converted into a string
* before being sent as a proto.
*/
var ByteString = /** @class */ (function () {
function ByteString(binaryString) {
this.binaryString = binaryString;
}
ByteString.fromBase64String = function (base64) {
var binaryString = PlatformSupport.getPlatform().atob(base64);
return new ByteString(binaryString);
};
ByteString.fromUint8Array = function (array) {
var binaryString = binaryStringFromUint8Array(array);
return new ByteString(binaryString);
};
ByteString.prototype.toBase64 = function () {
return PlatformSupport.getPlatform().btoa(this.binaryString);
};
ByteString.prototype.toUint8Array = function () {
return uint8ArrayFromBinaryString(this.binaryString);
};
ByteString.prototype.approximateByteSize = function () {
return this.binaryString.length * 2;
};
ByteString.prototype.compareTo = function (other) {
return primitiveComparator(this.binaryString, other.binaryString);
};
ByteString.prototype.isEqual = function (other) {
return this.binaryString === other.binaryString;
};
return ByteString;
}());
ByteString.EMPTY_BYTE_STRING = new ByteString('');
/**
* Helper function to convert an Uint8array to a binary string.
*/
function binaryStringFromUint8Array(array) {
var binaryString = '';
for (var i = 0; i < array.length; ++i) {
binaryString += String.fromCharCode(array[i]);
}
return binaryString;
}
/**
* Helper function to convert a binary string to an Uint8Array.
*/
function uint8ArrayFromBinaryString(binaryString) {
var buffer = new Uint8Array(binaryString.length);
for (var i = 0; i < binaryString.length; i++) {
buffer[i] = binaryString.charCodeAt(i);
}
return buffer;
}
/**
* @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.
*/
/**
* Represents a locally-applied ServerTimestamp.
*
* Server Timestamps are backed by MapValues that contain an internal field
* `__type__` with a value of `server_timestamp`. The previous value and local
* write time are stored in its `__previous_value__` and `__local_write_time__`
* fields respectively.
*
* Notes:
* - ServerTimestampValue instances are created as the result of applying a
* TransformMutation (see TransformMutation.applyTo()). They can only exist in
* the local view of a document. Therefore they do not need to be parsed or
* serialized.
* - When evaluated locally (e.g. for snapshot.data()), they by default
* evaluate to `null`. This behavior can be configured by passing custom
* FieldValueOptions to value().
* - With respect to other ServerTimestampValues, they sort by their
* localWriteTime.
*/
var SERVER_TIMESTAMP_SENTINEL = 'server_timestamp';
var TYPE_KEY = '__type__';
var PREVIOUS_VALUE_KEY = '__previous_value__';
var LOCAL_WRITE_TIME_KEY = '__local_write_time__';
function isServerTimestamp(value) {
var _a, _b;
var type = (_b = (((_a = value === null || value === void 0 ? void 0 : value.mapValue) === null || _a === void 0 ? void 0 : _a.fields) || {})[TYPE_KEY]) === null || _b === void 0 ? void 0 : _b.stringValue;
return type === SERVER_TIMESTAMP_SENTINEL;
}
/**
* Creates a new ServerTimestamp proto value (using the internal format).
*/
function serverTimestamp(localWriteTime, previousValue) {
var _e;
var mapValue = {
fields: (_e = {},
_e[TYPE_KEY] = {
stringValue: SERVER_TIMESTAMP_SENTINEL
},
_e[LOCAL_WRITE_TIME_KEY] = {
timestampValue: {
seconds: localWriteTime.seconds,
nanos: localWriteTime.nanoseconds
}
},
_e)
};
if (previousValue) {
mapValue.fields[PREVIOUS_VALUE_KEY] = previousValue;
}
return { mapValue: mapValue };
}
/**
* Returns the value of the field before this ServerTimestamp was set.
*
* Preserving the previous values allows the user to display the last resoled
* value until the backend responds with the timestamp.
*/
function getPreviousValue(value) {
var previousValue = value.mapValue.fields[PREVIOUS_VALUE_KEY];
if (isServerTimestamp(previousValue)) {
return getPreviousValue(previousValue);
}
return previousValue;
}
/**
* Returns the local time at which this timestamp was first set.
*/
function getLocalWriteTime(value) {
var localWriteTime = normalizeTimestamp(value.mapValue.fields[LOCAL_WRITE_TIME_KEY].timestampValue);
return new Timestamp(localWriteTime.seconds, localWriteTime.nanos);
}
/**
* @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.
*/
// A RegExp matching ISO 8601 UTC timestamps with optional fraction.
var ISO_TIMESTAMP_REG_EXP = new RegExp(/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.(\d+))?Z$/);
/** Extracts the backend's type order for the provided value. */
function typeOrder(value) {
if ('nullValue' in value) {
return 0 /* NullValue */;
}
else if ('booleanValue' in value) {
return 1 /* BooleanValue */;
}
else if ('integerValue' in value || 'doubleValue' in value) {
return 2 /* NumberValue */;
}
else if ('timestampValue' in value) {
return 3 /* TimestampValue */;
}
else if ('stringValue' in value) {
return 5 /* StringValue */;
}
else if ('bytesValue' in value) {
return 6 /* BlobValue */;
}
else if ('referenceValue' in value) {
return 7 /* RefValue */;
}
else if ('geoPointValue' in value) {
return 8 /* GeoPointValue */;
}
else if ('arrayValue' in value) {
return 9 /* ArrayValue */;
}
else if ('mapValue' in value) {
if (isServerTimestamp(value)) {
return 4 /* ServerTimestampValue */;
}
return 10 /* ObjectValue */;
}
else {
return fail('Invalid value type: ' + JSON.stringify(value));
}
}
/** Tests `left` and `right` for equality based on the backend semantics. */
function valueEquals(left, right) {
var leftType = typeOrder(left);
var rightType = typeOrder(right);
if (leftType !== rightType) {
return false;
}
switch (leftType) {
case 0 /* NullValue */:
return true;
case 1 /* BooleanValue */:
return left.booleanValue === right.booleanValue;
case 4 /* ServerTimestampValue */:
return getLocalWriteTime(left).isEqual(getLocalWriteTime(right));
case 3 /* TimestampValue */:
return timestampEquals(left, right);
case 5 /* StringValue */:
return left.stringValue === right.stringValue;
case 6 /* BlobValue */:
return blobEquals(left, right);
case 7 /* RefValue */:
return left.referenceValue === right.referenceValue;
case 8 /* GeoPointValue */:
return geoPointEquals(left, right);
case 2 /* NumberValue */:
return numberEquals(left, right);
case 9 /* ArrayValue */:
return arrayEquals(left.arrayValue.values || [], right.arrayValue.values || [], valueEquals);
case 10 /* ObjectValue */:
return objectEquals(left, right);
default:
return fail('Unexpected value type: ' + JSON.stringify(left));
}
}
function timestampEquals(left, right) {
if (typeof left.timestampValue === 'string' &&
typeof right.timestampValue === 'string' &&
left.timestampValue.length === right.timestampValue.length) {
// Use string equality for ISO 8601 timestamps
return left.timestampValue === right.timestampValue;
}
var leftTimestamp = normalizeTimestamp(left.timestampValue);
var rightTimestamp = normalizeTimestamp(right.timestampValue);
return (leftTimestamp.seconds === rightTimestamp.seconds &&
leftTimestamp.nanos === rightTimestamp.nanos);
}
function geoPointEquals(left, right) {
return (normalizeNumber(left.geoPointValue.latitude) ===
normalizeNumber(right.geoPointValue.latitude) &&
normalizeNumber(left.geoPointValue.longitude) ===
normalizeNumber(right.geoPointValue.longitude));
}
function blobEquals(left, right) {
return normalizeByteString(left.bytesValue).isEqual(normalizeByteString(right.bytesValue));
}
function numberEquals(left, right) {
if ('integerValue' in left && 'integerValue' in right) {
return (normalizeNumber(left.integerValue) === normalizeNumber(right.integerValue));
}
else if ('doubleValue' in left && 'doubleValue' in right) {
var n1 = normalizeNumber(left.doubleValue);
var n2 = normalizeNumber(right.doubleValue);
if (n1 === n2) {
return isNegativeZero(n1) === isNegativeZero(n2);
}
else {
return isNaN(n1) && isNaN(n2);
}
}
return false;
}
function objectEquals(left, right) {
var leftMap = left.mapValue.fields || {};
var rightMap = right.mapValue.fields || {};
if (objectSize(leftMap) !== objectSize(rightMap)) {
return false;
}
for (var key in leftMap) {
if (leftMap.hasOwnProperty(key)) {
if (rightMap[key] === undefined ||
!valueEquals(leftMap[key], rightMap[key])) {
return false;
}
}
}
return true;
}
/** Returns true if the ArrayValue contains the specified element. */
function arrayValueContains(haystack, needle) {
return ((haystack.values || []).find(function (v) { return valueEquals(v, needle); }) !== undefined);
}
function valueCompare(left, right) {
var leftType = typeOrder(left);
var rightType = typeOrder(right);
if (leftType !== rightType) {
return primitiveComparator(leftType, rightType);
}
switch (leftType) {
case 0 /* NullValue */:
return 0;
case 1 /* BooleanValue */:
return primitiveComparator(left.booleanValue, right.booleanValue);
case 2 /* NumberValue */:
return compareNumbers(left, right);
case 3 /* TimestampValue */:
return compareTimestamps(left.timestampValue, right.timestampValue);
case 4 /* ServerTimestampValue */:
return compareTimestamps(getLocalWriteTime(left), getLocalWriteTime(right));
case 5 /* StringValue */:
return primitiveComparator(left.stringValue, right.stringValue);
case 6 /* BlobValue */:
return compareBlobs(left.bytesValue, right.bytesValue);
case 7 /* RefValue */:
return compareReferences(left.referenceValue, right.referenceValue);
case 8 /* GeoPointValue */:
return compareGeoPoints(left.geoPointValue, right.geoPointValue);
case 9 /* ArrayValue */:
return compareArrays(left.arrayValue, right.arrayValue);
case 10 /* ObjectValue */:
return compareMaps(left.mapValue, right.mapValue);
default:
throw fail('Invalid value type: ' + leftType);
}
}
function compareNumbers(left, right) {
var leftNumber = normalizeNumber(left.integerValue || left.doubleValue);
var rightNumber = normalizeNumber(right.integerValue || right.doubleValue);
if (leftNumber < rightNumber) {
return -1;
}
else if (leftNumber > rightNumber) {
return 1;
}
else if (leftNumber === rightNumber) {
return 0;
}
else {
// one or both are NaN.
if (isNaN(leftNumber)) {
return isNaN(rightNumber) ? 0 : -1;
}
else {
return 1;
}
}
}
function compareTimestamps(left, right) {
if (typeof left === 'string' &&
typeof right === 'string' &&
left.length === right.length) {
return primitiveComparator(left, right);
}
var leftTimestamp = normalizeTimestamp(left);
var rightTimestamp = normalizeTimestamp(right);
var comparison = primitiveComparator(leftTimestamp.seconds, rightTimestamp.seconds);
if (comparison !== 0) {
return comparison;
}
return primitiveComparator(leftTimestamp.nanos, rightTimestamp.nanos);
}
function compareReferences(leftPath, rightPath) {
var leftSegments = leftPath.split('/');
var rightSegments = rightPath.split('/');
for (var i = 0; i < leftSegments.length && i < rightSegments.length; i++) {
var comparison = primitiveComparator(leftSegments[i], rightSegments[i]);
if (comparison !== 0) {
return comparison;
}
}
return primitiveComparator(leftSegments.length, rightSegments.length);
}
function compareGeoPoints(left, right) {
var comparison = primitiveComparator(normalizeNumber(left.latitude), normalizeNumber(right.latitude));
if (comparison !== 0) {
return comparison;
}
return primitiveComparator(normalizeNumber(left.longitude), normalizeNumber(right.longitude));
}
function compareBlobs(left, right) {
var leftBytes = normalizeByteString(left);
var rightBytes = normalizeByteString(right);
return leftBytes.compareTo(rightBytes);
}
function compareArrays(left, right) {
var leftArray = left.values || [];
var rightArray = right.values || [];
for (var i = 0; i < leftArray.length && i < rightArray.length; ++i) {
var compare = valueCompare(leftArray[i], rightArray[i]);
if (compare) {
return compare;
}
}
return primitiveComparator(leftArray.length, rightArray.length);
}
function compareMaps(left, right) {
var leftMap = left.fields || {};
var leftKeys = Object.keys(leftMap);
var rightMap = right.fields || {};
var rightKeys = Object.keys(rightMap);
// Even though MapValues are likely sorted correctly based on their insertion
// order (e.g. when received from the backend), local modifications can bring
// elements out of order. We need to re-sort the elements to ensure that
// canonical IDs are independent of insertion order.
leftKeys.sort();
rightKeys.sort();
for (var i = 0; i < leftKeys.length && i < rightKeys.length; ++i) {
var keyCompare = primitiveComparator(leftKeys[i], rightKeys[i]);
if (keyCompare !== 0) {
return keyCompare;
}
var compare = valueCompare(leftMap[leftKeys[i]], rightMap[rightKeys[i]]);
if (compare !== 0) {
return compare;
}
}
return primitiveComparator(leftKeys.length, rightKeys.length);
}
/**
* Generates the canonical ID for the provided field value (as used in Target
* serialization).
*/
function canonicalId(value) {
return canonifyValue(value);
}
function canonifyValue(value) {
if ('nullValue' in value) {
return 'null';
}
else if ('booleanValue' in value) {
return '' + value.booleanValue;
}
else if ('integerValue' in value) {
return '' + value.integerValue;
}
else if ('doubleValue' in value) {
return '' + value.doubleValue;
}
else if ('timestampValue' in value) {
return canonifyTimestamp(value.timestampValue);
}
else if ('stringValue' in value) {
return value.stringValue;
}
else if ('bytesValue' in value) {
return canonifyByteString(value.bytesValue);
}
else if ('referenceValue' in value) {
return canonifyReference(value.referenceValue);
}
else if ('geoPointValue' in value) {
return canonifyGeoPoint(value.geoPointValue);
}
else if ('arrayValue' in value) {
return canonifyArray(value.arrayValue);
}
else if ('mapValue' in value) {
return canonifyMap(value.mapValue);
}
else {
return fail('Invalid value type: ' + JSON.stringify(value));
}
}
function canonifyByteString(byteString) {
return normalizeByteString(byteString).toBase64();
}
function canonifyTimestamp(timestamp) {
var normalizedTimestamp = normalizeTimestamp(timestamp);
return "time(" + normalizedTimestamp.seconds + "," + normalizedTimestamp.nanos + ")";
}
function canonifyGeoPoint(geoPoint) {
return "geo(" + geoPoint.latitude + "," + geoPoint.longitude + ")";
}
function canonifyReference(referenceValue) {
return DocumentKey.fromName(referenceValue).toString();
}
function canonifyMap(mapValue) {
// Iteration order in JavaScript is not guaranteed. To ensure that we generate
// matching canonical IDs for identical maps, we need to sort the keys.
var sortedKeys = Object.keys(mapValue.fields || {}).sort();
var result = '{';
var first = true;
for (var _i = 0, sortedKeys_1 = sortedKeys; _i < sortedKeys_1.length; _i++) {
var key = sortedKeys_1[_i];
if (!first) {
result += ',';
}
else {
first = false;
}
result += key + ":" + canonifyValue(mapValue.fields[key]);
}
return result + '}';
}
function canonifyArray(arrayValue) {
var result = '[';
var first = true;
for (var _i = 0, _e = arrayValue.values || []; _i < _e.length; _i++) {
var value = _e[_i];
if (!first) {
result += ',';
}
else {
first = false;
}
result += canonifyValue(value);
}
return result + ']';
}
/**
* Converts the possible Proto values for a timestamp value into a "seconds and
* nanos" representation.
*/
function normalizeTimestamp(date) {
hardAssert(!!date, 'Cannot normalize null or undefined timestamp.');
// The json interface (for the browser) will return an iso timestamp string,
// while the proto js library (for node) will return a
// google.protobuf.Timestamp instance.
if (typeof date === 'string') {
// The date string can have higher precision (nanos) than the Date class
// (millis), so we do some custom parsing here.
// Parse the nanos right out of the string.
var nanos = 0;
var fraction = ISO_TIMESTAMP_REG_EXP.exec(date);
hardAssert(!!fraction, 'invalid timestamp: ' + date);
if (fraction[1]) {
// Pad the fraction out to 9 digits (nanos).
var nanoStr = fraction[1];
nanoStr = (nanoStr + '000000000').substr(0, 9);
nanos = Number(nanoStr);
}
// Parse the date to get the seconds.
var parsedDate = new Date(date);
var seconds = Math.floor(parsedDate.getTime() / 1000);
return { seconds: seconds, nanos: nanos };
}
else {
// TODO(b/37282237): Use strings for Proto3 timestamps
// assert(!this.options.useProto3Json,
// 'The timestamp instance format requires Proto JS.');
var seconds = normalizeNumber(date.seconds);
var nanos = normalizeNumber(date.nanos);
return { seconds: seconds, nanos: nanos };
}
}
/**
* Converts the possible Proto types for numbers into a JavaScript number.
* Returns 0 if the value is not numeric.
*/
function normalizeNumber(value) {
// TODO(bjornick): Handle int64 greater than 53 bits.
if (typeof value === 'number') {
return value;
}
else if (typeof value === 'string') {
return Number(value);
}
else {
return 0;
}
}
/** Converts the possible Proto types for Blobs into a ByteString. */
function normalizeByteString(blob) {
if (typeof blob === 'string') {
return ByteString.fromBase64String(blob);
}
else {
return ByteString.fromUint8Array(blob);
}
}
/** Returns a reference value for the provided database and key. */
function refValue(databaseId, key) {
return {
referenceValue: "projects/" + databaseId.projectId + "/databases/" + databaseId.database + "/documents/" + key.path.canonicalString()
};
}
/** Returns true if `value` is an IntegerValue . */
function isInteger(value) {
return !!value && 'integerValue' in value;
}
/** Returns true if `value` is a DoubleValue. */
function isDouble(value) {
return !!value && 'doubleValue' in value;
}
/** Returns true if `value` is either an IntegerValue or a DoubleValue. */
function isNumber(value) {
return isInteger(value) || isDouble(value);
}
/** Returns true if `value` is an ArrayValue. */
function isArray(value) {
return !!value && 'arrayValue' in value;
}
/** Returns true if `value` is a ReferenceValue. */
function isReferenceValue(value) {
return !!value && 'referenceValue' in value;
}
/** Returns true if `value` is a NullValue. */
function isNullValue(value) {
return !!value && 'nullValue' in value;
}
/** Returns true if `value` is NaN. */
function isNanValue(value) {
return !!value && 'doubleValue' in value && isNaN(Number(value.doubleValue));
}
/** Returns true if `value` is a MapValue. */
function isMapValue(value) {
return !!value && 'mapValue' in 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.
*/
/**
* The result of a lookup for a given path may be an existing document or a
* marker that this document does not exist at a given version.
*/
var MaybeDocument = /** @class */ (function () {
function MaybeDocument(key, version) {
this.key = key;
this.version = version;
}
return MaybeDocument;
}());
/**
* Represents a document in Firestore with a key, version, data and whether the
* data has local mutations applied to it.
*/
var Document = /** @class */ (function (_super) {
tslib.__extends(Document, _super);
function Document(key, version, objectValue, options) {
var _this = _super.call(this, key, version) || this;
_this.objectValue = objectValue;
_this.hasLocalMutations = !!options.hasLocalMutations;
_this.hasCommittedMutations = !!options.hasCommittedMutations;
return _this;
}
Document.prototype.field = function (path) {
return this.objectValue.field(path);
};
Document.prototype.data = function () {
return this.objectValue;
};
Document.prototype.toProto = function () {
return this.objectValue.proto;
};
Document.prototype.isEqual = function (other) {
return (other instanceof Document &&
this.key.isEqual(other.key) &&
this.version.isEqual(other.version) &&
this.hasLocalMutations === other.hasLocalMutations &&
this.hasCommittedMutations === other.hasCommittedMutations &&
this.objectValue.isEqual(other.objectValue));
};
Document.prototype.toString = function () {
return ("Document(" + this.key + ", " + this.version + ", " + this.objectValue.toString() + ", " +
("{hasLocalMutations: " + this.hasLocalMutations + "}), ") +
("{hasCommittedMutations: " + this.hasCommittedMutations + "})"));
};
Object.defineProperty(Document.prototype, "hasPendingWrites", {
get: function () {
return this.hasLocalMutations || this.hasCommittedMutations;
},
enumerable: true,
configurable: true
});
return Document;
}(MaybeDocument));
/**
* Compares the value for field `field` in the provided documents. Throws if
* the field does not exist in both documents.
*/
function compareDocumentsByField(field, d1, d2) {
var v1 = d1.field(field);
var v2 = d2.field(field);
if (v1 !== null && v2 !== null) {
return valueCompare(v1, v2);
}
else {
return fail("Trying to compare documents on fields that don't exist");
}
}
/**
* A class representing a deleted document.
* Version is set to 0 if we don't point to any specific time, otherwise it
* denotes time we know it didn't exist at.
*/
var NoDocument = /** @class */ (function (_super) {
tslib.__extends(NoDocument, _super);
function NoDocument(key, version, options) {
var _this = _super.call(this, key, version) || this;
_this.hasCommittedMutations = !!(options && options.hasCommittedMutations);
return _this;
}
NoDocument.prototype.toString = function () {
return "NoDocument(" + this.key + ", " + this.version + ")";
};
Object.defineProperty(NoDocument.prototype, "hasPendingWrites", {
get: function () {
return this.hasCommittedMutations;
},
enumerable: true,
configurable: true
});
NoDocument.prototype.isEqual = function (other) {
return (other instanceof NoDocument &&
other.hasCommittedMutations === this.hasCommittedMutations &&
other.version.isEqual(this.version) &&
other.key.isEqual(this.key));
};
return NoDocument;
}(MaybeDocument));
/**
* A class representing an existing document whose data is unknown (e.g. a
* document that was updated without a known base document).
*/
var UnknownDocument = /** @class */ (function (_super) {
tslib.__extends(UnknownDocument, _super);
function UnknownDocument() {
return _super !== null && _super.apply(this, arguments) || this;
}
UnknownDocument.prototype.toString = function () {
return "UnknownDocument(" + this.key + ", " + this.version + ")";
};
Object.defineProperty(UnknownDocument.prototype, "hasPendingWrites", {
get: function () {
return true;
},
enumerable: true,
configurable: true
});
UnknownDocument.prototype.isEqual = function (other) {
return (other instanceof UnknownDocument &&
other.version.isEqual(this.version) &&
other.key.isEqual(this.key));
};
return UnknownDocument;
}(MaybeDocument));
/**
* @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 ObjectValue represents a MapValue in the Firestore Proto and offers the
* ability to add and remove fields (via the ObjectValueBuilder).
*/
var ObjectValue = /** @class */ (function () {
function ObjectValue(proto) {
this.proto = proto;
debugAssert(!isServerTimestamp(proto), 'ServerTimestamps should be converted to ServerTimestampValue');
}
ObjectValue.empty = function () {
return new ObjectValue({ mapValue: {} });
};
/**
* Returns the value at the given path or null.
*
* @param path the path to search
* @return The value at the path or if there it doesn't exist.
*/
ObjectValue.prototype.field = function (path) {
if (path.isEmpty()) {
return this.proto;
}
else {
var value = this.proto;
for (var i = 0; i < path.length - 1; ++i) {
if (!value.mapValue.fields) {
return null;
}
value = value.mapValue.fields[path.get(i)];
if (!isMapValue(value)) {
return null;
}
}
value = (value.mapValue.fields || {})[path.lastSegment()];
return value || null;
}
};
ObjectValue.prototype.isEqual = function (other) {
return valueEquals(this.proto, other.proto);
};
return ObjectValue;
}());
/**
* An ObjectValueBuilder provides APIs to set and delete fields from an
* ObjectValue.
*/
var ObjectValueBuilder = /** @class */ (function () {
/**
* @param baseObject The object to mutate.
*/
function ObjectValueBuilder(baseObject) {
if (baseObject === void 0) { baseObject = ObjectValue.empty(); }
this.baseObject = baseObject;
/** A map that contains the accumulated changes in this builder. */
this.overlayMap = new Map();
}
/**
* Sets the field to the provided value.
*
* @param path The field path to set.
* @param value The value to set.
* @return The current Builder instance.
*/
ObjectValueBuilder.prototype.set = function (path, value) {
debugAssert(!path.isEmpty(), 'Cannot set field for empty path on ObjectValue');
this.setOverlay(path, value);
return this;
};
/**
* Removes the field at the specified path. If there is no field at the
* specified path, nothing is changed.
*
* @param path The field path to remove.
* @return The current Builder instance.
*/
ObjectValueBuilder.prototype.delete = function (path) {
debugAssert(!path.isEmpty(), 'Cannot delete field for empty path on ObjectValue');
this.setOverlay(path, null);
return this;
};
/**
* Adds `value` to the overlay map at `path`. Creates nested map entries if
* needed.
*/
ObjectValueBuilder.prototype.setOverlay = function (path, value) {
var currentLevel = this.overlayMap;
for (var i = 0; i < path.length - 1; ++i) {
var currentSegment = path.get(i);
var currentValue = currentLevel.get(currentSegment);
if (currentValue instanceof Map) {
// Re-use a previously created map
currentLevel = currentValue;
}
else if (currentValue &&
typeOrder(currentValue) === 10 /* ObjectValue */) {
// Convert the existing Protobuf MapValue into a map
currentValue = new Map(Object.entries(currentValue.mapValue.fields || {}));
currentLevel.set(currentSegment, currentValue);
currentLevel = currentValue;
}
else {
// Create an empty map to represent the current nesting level
currentValue = new Map();
currentLevel.set(currentSegment, currentValue);
currentLevel = currentValue;
}
}
currentLevel.set(path.lastSegment(), value);
};
/** Returns an ObjectValue with all mutations applied. */
ObjectValueBuilder.prototype.build = function () {
var mergedResult = this.applyOverlay(FieldPath.EMPTY_PATH, this.overlayMap);
if (mergedResult != null) {
return new ObjectValue(mergedResult);
}
else {
return this.baseObject;
}
};
/**
* Applies any overlays from `currentOverlays` that exist at `currentPath`
* and returns the merged data at `currentPath` (or null if there were no
* changes).
*
* @param currentPath The path at the current nesting level. Can be set to
* FieldValue.EMPTY_PATH to represent the root.
* @param currentOverlays The overlays at the current nesting level in the
* same format as `overlayMap`.
* @return The merged data at `currentPath` or null if no modifications
* were applied.
*/
ObjectValueBuilder.prototype.applyOverlay = function (currentPath, currentOverlays) {
var _this = this;
var modified = false;
var existingValue = this.baseObject.field(currentPath);
var resultAtPath = isMapValue(existingValue)
? // If there is already data at the current path, base our
Object.assign({}, existingValue.mapValue.fields) : {};
currentOverlays.forEach(function (value, pathSegment) {
if (value instanceof Map) {
var nested = _this.applyOverlay(currentPath.child(pathSegment), value);
if (nested != null) {
resultAtPath[pathSegment] = nested;
modified = true;
}
}
else if (value !== null) {
resultAtPath[pathSegment] = value;
modified = true;
}
else if (resultAtPath.hasOwnProperty(pathSegment)) {
delete resultAtPath[pathSegment];
modified = true;
}
});
return modified ? { mapValue: { fields: resultAtPath } } : null;
};
return ObjectValueBuilder;
}());
/**
* Returns a FieldMask built from all fields in a MapValue.
*/
function extractFieldMask(value) {
var fields = [];
forEach(value.fields || {}, function (key, value) {
var currentPath = new FieldPath([key]);
if (isMapValue(value)) {
var nestedMask = extractFieldMask(value.mapValue);
var nestedFields = nestedMask.fields;
if (nestedFields.length === 0) {
// Preserve the empty map by adding it to the FieldMask.
fields.push(currentPath);
}
else {
// For nested and non-empty ObjectValues, add the FieldPath of the
// leaf nodes.
for (var _i = 0, nestedFields_1 = nestedFields; _i < nestedFields_1.length; _i++) {
var nestedPath = nestedFields_1[_i];
fields.push(currentPath.child(nestedPath));
}
}
}
else {
// For nested and non-empty ObjectValues, add the FieldPath of the leaf
// nodes.
fields.push(currentPath);
}
});
return new FieldMask(fields);
}
/**
* @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.
*/
/**
* Provides a set of fields that can be used to partially patch a document.
* FieldMask is used in conjunction with ObjectValue.
* Examples:
* foo - Overwrites foo entirely with the provided value. If foo is not
* present in the companion ObjectValue, the field is deleted.
* foo.bar - Overwrites only the field bar of the object foo.
* If foo is not an object, foo is replaced with an object
* containing foo
*/
var FieldMask = /** @class */ (function () {
function FieldMask(fields) {
this.fields = fields;
// TODO(dimond): validation of FieldMask
// Sort the field mask to support `FieldMask.isEqual()` and assert below.
fields.sort(FieldPath.comparator);
debugAssert(!fields.some(function (v, i) { return i !== 0 && v.isEqual(fields[i - 1]); }), 'FieldMask contains field that is not unique: ' +
fields.find(function (v, i) { return i !== 0 && v.isEqual(fields[i - 1]); }));
}
/**
* Verifies that `fieldPath` is included by at least one field in this field
* mask.
*
* This is an O(n) operation, where `n` is the size of the field mask.
*/
FieldMask.prototype.covers = function (fieldPath) {
for (var _i = 0, _e = this.fields; _i < _e.length; _i++) {
var fieldMaskPath = _e[_i];
if (fieldMaskPath.isPrefixOf(fieldPath)) {
return true;
}
}
return false;
};
FieldMask.prototype.isEqual = function (other) {
return arrayEquals(this.fields, other.fields, function (l, r) { return l.isEqual(r); });
};
return FieldMask;
}());
/** A field path and the TransformOperation to perform upon it. */
var FieldTransform = /** @class */ (function () {
function FieldTransform(field, transform) {
this.field = field;
this.transform = transform;
}
FieldTransform.prototype.isEqual = function (other) {
return (this.field.isEqual(other.field) && this.transform.isEqual(other.transform));
};
return FieldTransform;
}());
/** The result of successfully applying a mutation to the backend. */
var MutationResult = /** @class */ (function () {
function MutationResult(
/**
* The version at which the mutation was committed:
*
* - For most operations, this is the updateTime in the WriteResult.
* - For deletes, the commitTime of the WriteResponse (because deletes are
* not stored and have no updateTime).
*
* Note that these versions can be different: No-op writes will not change
* the updateTime even though the commitTime advances.
*/
version,
/**
* The resulting fields returned from the backend after a
* TransformMutation has been committed. Contains one FieldValue for each
* FieldTransform that was in the mutation.
*
* Will be null if the mutation was not a TransformMutation.
*/
transformResults) {
this.version = version;
this.transformResults = transformResults;
}
return MutationResult;
}());
/**
* Encodes a precondition for a mutation. This follows the model that the
* backend accepts with the special case of an explicit "empty" precondition
* (meaning no precondition).
*/
var Precondition = /** @class */ (function () {
function Precondition(updateTime, exists) {
this.updateTime = updateTime;
this.exists = exists;
debugAssert(updateTime === undefined || exists === undefined, 'Precondition can specify "exists" or "updateTime" but not both');
}
/** Creates a new empty Precondition. */
Precondition.none = function () {
return new Precondition();
};
/** Creates a new Precondition with an exists flag. */
Precondition.exists = function (exists) {
return new Precondition(undefined, exists);
};
/** Creates a new Precondition based on a version a document exists at. */
Precondition.updateTime = function (version) {
return new Precondition(version);
};
Object.defineProperty(Precondition.prototype, "isNone", {
/** Returns whether this Precondition is empty. */
get: function () {
return this.updateTime === undefined && this.exists === undefined;
},
enumerable: true,
configurable: true
});
/**
* Returns true if the preconditions is valid for the given document
* (or null if no document is available).
*/
Precondition.prototype.isValidFor = function (maybeDoc) {
if (this.updateTime !== undefined) {
return (maybeDoc instanceof Document &&
maybeDoc.version.isEqual(this.updateTime));
}
else if (this.exists !== undefined) {
return this.exists === maybeDoc instanceof Document;
}
else {
debugAssert(this.isNone, 'Precondition should be empty');
return true;
}
};
Precondition.prototype.isEqual = function (other) {
return (this.exists === other.exists &&
(this.updateTime
? !!other.updateTime && this.updateTime.isEqual(other.updateTime)
: !other.updateTime));
};
return Precondition;
}());
/**
* A mutation describes a self-contained change to a document. Mutations can
* create, replace, delete, and update subsets of documents.
*
* Mutations not only act on the value of the document but also its version.
*
* For local mutations (mutations that haven't been committed yet), we preserve
* the existing version for Set, Patch, and Transform mutations. For Delete
* mutations, we reset the version to 0.
*
* Here's the expected transition table.
*
* MUTATION APPLIED TO RESULTS IN
*
* SetMutation Document(v3) Document(v3)
* SetMutation NoDocument(v3) Document(v0)
* SetMutation null Document(v0)
* PatchMutation Document(v3) Document(v3)
* PatchMutation NoDocument(v3) NoDocument(v3)
* PatchMutation null null
* TransformMutation Document(v3) Document(v3)
* TransformMutation NoDocument(v3) NoDocument(v3)
* TransformMutation null null
* DeleteMutation Document(v3) NoDocument(v0)
* DeleteMutation NoDocument(v3) NoDocument(v0)
* DeleteMutation null NoDocument(v0)
*
* For acknowledged mutations, we use the updateTime of the WriteResponse as
* the resulting version for Set, Patch, and Transform mutations. As deletes
* have no explicit update time, we use the commitTime of the WriteResponse for
* Delete mutations.
*
* If a mutation is acknowledged by the backend but fails the precondition check
* locally, we return an `UnknownDocument` and rely on Watch to send us the
* updated version.
*
* Note that TransformMutations don't create Documents (in the case of being
* applied to a NoDocument), even though they would on the backend. This is
* because the client always combines the TransformMutation with a SetMutation
* or PatchMutation and we only want to apply the transform if the prior
* mutation resulted in a Document (always true for a SetMutation, but not
* necessarily for a PatchMutation).
*
* ## Subclassing Notes
*
* Subclasses of Mutation need to implement applyToRemoteDocument() and
* applyToLocalView() to implement the actual behavior of applying the mutation
* to some source document.
*/
var Mutation = /** @class */ (function () {
function Mutation() {
}
Mutation.prototype.verifyKeyMatches = function (maybeDoc) {
if (maybeDoc != null) {
debugAssert(maybeDoc.key.isEqual(this.key), 'Can only apply a mutation to a document with the same key');
}
};
/**
* Returns the version from the given document for use as the result of a
* mutation. Mutations are defined to return the version of the base document
* only if it is an existing document. Deleted and unknown documents have a
* post-mutation version of SnapshotVersion.min().
*/
Mutation.getPostMutationVersion = function (maybeDoc) {
if (maybeDoc instanceof Document) {
return maybeDoc.version;
}
else {
return SnapshotVersion.min();
}
};
return Mutation;
}());
/**
* A mutation that creates or replaces the document at the given key with the
* object value contents.
*/
var SetMutation = /** @class */ (function (_super) {
tslib.__extends(SetMutation, _super);
function SetMutation(key, value, precondition) {
var _this = _super.call(this) || this;
_this.key = key;
_this.value = value;
_this.precondition = precondition;
_this.type = 0 /* Set */;
return _this;
}
SetMutation.prototype.applyToRemoteDocument = function (maybeDoc, mutationResult) {
this.verifyKeyMatches(maybeDoc);
debugAssert(mutationResult.transformResults == null, 'Transform results received by SetMutation.');
// Unlike applyToLocalView, if we're applying a mutation to a remote
// document the server has accepted the mutation so the precondition must
// have held.
var version = mutationResult.version;
return new Document(this.key, version, this.value, {
hasCommittedMutations: true
});
};
SetMutation.prototype.applyToLocalView = function (maybeDoc, baseDoc, localWriteTime) {
this.verifyKeyMatches(maybeDoc);
if (!this.precondition.isValidFor(maybeDoc)) {
return maybeDoc;
}
var version = Mutation.getPostMutationVersion(maybeDoc);
return new Document(this.key, version, this.value, {
hasLocalMutations: true
});
};
SetMutation.prototype.extractBaseValue = function (maybeDoc) {
return null;
};
SetMutation.prototype.isEqual = function (other) {
return (other instanceof SetMutation &&
this.key.isEqual(other.key) &&
this.value.isEqual(other.value) &&
this.precondition.isEqual(other.precondition));
};
return SetMutation;
}(Mutation));
/**
* A mutation that modifies fields of the document at the given key with the
* given values. The values are applied through a field mask:
*
* * When a field is in both the mask and the values, the corresponding field
* is updated.
* * When a field is in neither the mask nor the values, the corresponding
* field is unmodified.
* * When a field is in the mask but not in the values, the corresponding field
* is deleted.
* * When a field is not in the mask but is in the values, the values map is
* ignored.
*/
var PatchMutation = /** @class */ (function (_super) {
tslib.__extends(PatchMutation, _super);
function PatchMutation(key, data, fieldMask, precondition) {
var _this = _super.call(this) || this;
_this.key = key;
_this.data = data;
_this.fieldMask = fieldMask;
_this.precondition = precondition;
_this.type = 1 /* Patch */;
return _this;
}
PatchMutation.prototype.applyToRemoteDocument = function (maybeDoc, mutationResult) {
this.verifyKeyMatches(maybeDoc);
debugAssert(mutationResult.transformResults == null, 'Transform results received by PatchMutation.');
if (!this.precondition.isValidFor(maybeDoc)) {
// Since the mutation was not rejected, we know that the precondition
// matched on the backend. We therefore must not have the expected version
// of the document in our cache and return an UnknownDocument with the
// known updateTime.
return new UnknownDocument(this.key, mutationResult.version);
}
var newData = this.patchDocument(maybeDoc);
return new Document(this.key, mutationResult.version, newData, {
hasCommittedMutations: true
});
};
PatchMutation.prototype.applyToLocalView = function (maybeDoc, baseDoc, localWriteTime) {
this.verifyKeyMatches(maybeDoc);
if (!this.precondition.isValidFor(maybeDoc)) {
return maybeDoc;
}
var version = Mutation.getPostMutationVersion(maybeDoc);
var newData = this.patchDocument(maybeDoc);
return new Document(this.key, version, newData, {
hasLocalMutations: true
});
};
PatchMutation.prototype.extractBaseValue = function (maybeDoc) {
return null;
};
PatchMutation.prototype.isEqual = function (other) {
return (other instanceof PatchMutation &&
this.key.isEqual(other.key) &&
this.fieldMask.isEqual(other.fieldMask) &&
this.precondition.isEqual(other.precondition));
};
/**
* Patches the data of document if available or creates a new document. Note
* that this does not check whether or not the precondition of this patch
* holds.
*/
PatchMutation.prototype.patchDocument = function (maybeDoc) {
var data;
if (maybeDoc instanceof Document) {
data = maybeDoc.data();
}
else {
data = ObjectValue.empty();
}
return this.patchObject(data);
};
PatchMutation.prototype.patchObject = function (data) {
var _this = this;
var builder = new ObjectValueBuilder(data);
this.fieldMask.fields.forEach(function (fieldPath) {
if (!fieldPath.isEmpty()) {
var newValue = _this.data.field(fieldPath);
if (newValue !== null) {
builder.set(fieldPath, newValue);
}
else {
builder.delete(fieldPath);
}
}
});
return builder.build();
};
return PatchMutation;
}(Mutation));
/**
* A mutation that modifies specific fields of the document with transform
* operations. Currently the only supported transform is a server timestamp, but
* IP Address, increment(n), etc. could be supported in the future.
*
* It is somewhat similar to a PatchMutation in that it patches specific fields
* and has no effect when applied to a null or NoDocument (see comment on
* Mutation for rationale).
*/
var TransformMutation = /** @class */ (function (_super) {
tslib.__extends(TransformMutation, _super);
function TransformMutation(key, fieldTransforms) {
var _this = _super.call(this) || this;
_this.key = key;
_this.fieldTransforms = fieldTransforms;
_this.type = 2 /* Transform */;
// NOTE: We set a precondition of exists: true as a safety-check, since we
// always combine TransformMutations with a SetMutation or PatchMutation which
// (if successful) should end up with an existing document.
_this.precondition = Precondition.exists(true);
return _this;
}
TransformMutation.prototype.applyToRemoteDocument = function (maybeDoc, mutationResult) {
this.verifyKeyMatches(maybeDoc);
hardAssert(mutationResult.transformResults != null, 'Transform results missing for TransformMutation.');
if (!this.precondition.isValidFor(maybeDoc)) {
// Since the mutation was not rejected, we know that the precondition
// matched on the backend. We therefore must not have the expected version
// of the document in our cache and return an UnknownDocument with the
// known updateTime.
return new UnknownDocument(this.key, mutationResult.version);
}
var doc = this.requireDocument(maybeDoc);
var transformResults = this.serverTransformResults(maybeDoc, mutationResult.transformResults);
var version = mutationResult.version;
var newData = this.transformObject(doc.data(), transformResults);
return new Document(this.key, version, newData, {
hasCommittedMutations: true
});
};
TransformMutation.prototype.applyToLocalView = function (maybeDoc, baseDoc, localWriteTime) {
this.verifyKeyMatches(maybeDoc);
if (!this.precondition.isValidFor(maybeDoc)) {
return maybeDoc;
}
var doc = this.requireDocument(maybeDoc);
var transformResults = this.localTransformResults(localWriteTime, maybeDoc, baseDoc);
var newData = this.transformObject(doc.data(), transformResults);
return new Document(this.key, doc.version, newData, {
hasLocalMutations: true
});
};
TransformMutation.prototype.extractBaseValue = function (maybeDoc) {
var baseObject = null;
for (var _i = 0, _e = this.fieldTransforms; _i < _e.length; _i++) {
var fieldTransform = _e[_i];
var existingValue = maybeDoc instanceof Document
? maybeDoc.field(fieldTransform.field)
: undefined;
var coercedValue = fieldTransform.transform.computeBaseValue(existingValue || null);
if (coercedValue != null) {
if (baseObject == null) {
baseObject = new ObjectValueBuilder().set(fieldTransform.field, coercedValue);
}
else {
baseObject = baseObject.set(fieldTransform.field, coercedValue);
}
}
}
return baseObject ? baseObject.build() : null;
};
TransformMutation.prototype.isEqual = function (other) {
return (other instanceof TransformMutation &&
this.key.isEqual(other.key) &&
arrayEquals(this.fieldTransforms, other.fieldTransforms, function (l, r) { return l.isEqual(r); }) &&
this.precondition.isEqual(other.precondition));
};
/**
* Asserts that the given MaybeDocument is actually a Document and verifies
* that it matches the key for this mutation. Since we only support
* transformations with precondition exists this method is guaranteed to be
* safe.
*/
TransformMutation.prototype.requireDocument = function (maybeDoc) {
debugAssert(maybeDoc instanceof Document, 'Unknown MaybeDocument type ' + maybeDoc);
debugAssert(maybeDoc.key.isEqual(this.key), 'Can only transform a document with the same key');
return maybeDoc;
};
/**
* Creates a list of "transform results" (a transform result is a field value
* representing the result of applying a transform) for use after a
* TransformMutation has been acknowledged by the server.
*
* @param baseDoc The document prior to applying this mutation batch.
* @param serverTransformResults The transform results received by the server.
* @return The transform results list.
*/
TransformMutation.prototype.serverTransformResults = function (baseDoc, serverTransformResults) {
var transformResults = [];
hardAssert(this.fieldTransforms.length === serverTransformResults.length, "server transform result count (" + serverTransformResults.length + ") " +
("should match field transform count (" + this.fieldTransforms.length + ")"));
for (var i = 0; i < serverTransformResults.length; i++) {
var fieldTransform = this.fieldTransforms[i];
var transform = fieldTransform.transform;
var previousValue = null;
if (baseDoc instanceof Document) {
previousValue = baseDoc.field(fieldTransform.field);
}
transformResults.push(transform.applyToRemoteDocument(previousValue, serverTransformResults[i]));
}
return transformResults;
};
/**
* Creates a list of "transform results" (a transform result is a field value
* representing the result of applying a transform) for use when applying a
* TransformMutation locally.
*
* @param localWriteTime The local time of the transform mutation (used to
* generate ServerTimestampValues).
* @param maybeDoc The current state of the document after applying all
* previous mutations.
* @param baseDoc The document prior to applying this mutation batch.
* @return The transform results list.
*/
TransformMutation.prototype.localTransformResults = function (localWriteTime, maybeDoc, baseDoc) {
var transformResults = [];
for (var _i = 0, _e = this.fieldTransforms; _i < _e.length; _i++) {
var fieldTransform = _e[_i];
var transform = fieldTransform.transform;
var previousValue = null;
if (maybeDoc instanceof Document) {
previousValue = maybeDoc.field(fieldTransform.field);
}
if (previousValue === null && baseDoc instanceof Document) {
// If the current document does not contain a value for the mutated
// field, use the value that existed before applying this mutation
// batch. This solves an edge case where a PatchMutation clears the
// values in a nested map before the TransformMutation is applied.
previousValue = baseDoc.field(fieldTransform.field);
}
transformResults.push(transform.applyToLocalView(previousValue, localWriteTime));
}
return transformResults;
};
TransformMutation.prototype.transformObject = function (data, transformResults) {
debugAssert(transformResults.length === this.fieldTransforms.length, 'TransformResults length mismatch.');
var builder = new ObjectValueBuilder(data);
for (var i = 0; i < this.fieldTransforms.length; i++) {
var fieldTransform = this.fieldTransforms[i];
var fieldPath = fieldTransform.field;
builder.set(fieldPath, transformResults[i]);
}
return builder.build();
};
return TransformMutation;
}(Mutation));
/** A mutation that deletes the document at the given key. */
var DeleteMutation = /** @class */ (function (_super) {
tslib.__extends(DeleteMutation, _super);
function DeleteMutation(key, precondition) {
var _this = _super.call(this) || this;
_this.key = key;
_this.precondition = precondition;
_this.type = 3 /* Delete */;
return _this;
}
DeleteMutation.prototype.applyToRemoteDocument = function (maybeDoc, mutationResult) {
this.verifyKeyMatches(maybeDoc);
debugAssert(mutationResult.transformResults == null, 'Transform results received by DeleteMutation.');
// Unlike applyToLocalView, if we're applying a mutation to a remote
// document the server has accepted the mutation so the precondition must
// have held.
return new NoDocument(this.key, mutationResult.version, {
hasCommittedMutations: true
});
};
DeleteMutation.prototype.applyToLocalView = function (maybeDoc, baseDoc, localWriteTime) {
this.verifyKeyMatches(maybeDoc);
if (!this.precondition.isValidFor(maybeDoc)) {
return maybeDoc;
}
if (maybeDoc) {
debugAssert(maybeDoc.key.isEqual(this.key), 'Can only apply mutation to document with same key');
}
return new NoDocument(this.key, SnapshotVersion.min());
};
DeleteMutation.prototype.extractBaseValue = function (maybeDoc) {
return null;
};
DeleteMutation.prototype.isEqual = function (other) {
return (other instanceof DeleteMutation &&
this.key.isEqual(other.key) &&
this.precondition.isEqual(other.precondition));
};
return DeleteMutation;
}(Mutation));
/**
* A mutation that verifies the existence of the document at the given key with
* the provided precondition.
*
* The `verify` operation is only used in Transactions, and this class serves
* primarily to facilitate serialization into protos.
*/
var VerifyMutation = /** @class */ (function (_super) {
tslib.__extends(VerifyMutation, _super);
function VerifyMutation(key, precondition) {
var _this = _super.call(this) || this;
_this.key = key;
_this.precondition = precondition;
_this.type = 4 /* Verify */;
return _this;
}
VerifyMutation.prototype.applyToRemoteDocument = function (maybeDoc, mutationResult) {
fail('VerifyMutation should only be used in Transactions.');
};
VerifyMutation.prototype.applyToLocalView = function (maybeDoc, baseDoc, localWriteTime) {
fail('VerifyMutation should only be used in Transactions.');
};
VerifyMutation.prototype.extractBaseValue = function (maybeDoc) {
fail('VerifyMutation should only be used in Transactions.');
};
VerifyMutation.prototype.isEqual = function (other) {
return (other instanceof VerifyMutation &&
this.key.isEqual(other.key) &&
this.precondition.isEqual(other.precondition));
};
return VerifyMutation;
}(Mutation));
/**
* @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.
*/
var BATCHID_UNKNOWN = -1;
/**
* A batch of mutations that will be sent as one unit to the backend.
*/
var MutationBatch = /** @class */ (function () {
/**
* @param batchId The unique ID of this mutation batch.
* @param localWriteTime The original write time of this mutation.
* @param baseMutations Mutations that are used to populate the base
* values when this mutation is applied locally. This can be used to locally
* overwrite values that are persisted in the remote document cache. Base
* mutations are never sent to the backend.
* @param mutations The user-provided mutations in this mutation batch.
* User-provided mutations are applied both locally and remotely on the
* backend.
*/
function MutationBatch(batchId, localWriteTime, baseMutations, mutations) {
this.batchId = batchId;
this.localWriteTime = localWriteTime;
this.baseMutations = baseMutations;
this.mutations = mutations;
debugAssert(mutations.length > 0, 'Cannot create an empty mutation batch');
}
/**
* Applies all the mutations in this MutationBatch to the specified document
* to create a new remote document
*
* @param docKey The key of the document to apply mutations to.
* @param maybeDoc The document to apply mutations to.
* @param batchResult The result of applying the MutationBatch to the
* backend.
*/
MutationBatch.prototype.applyToRemoteDocument = function (docKey, maybeDoc, batchResult) {
if (maybeDoc) {
debugAssert(maybeDoc.key.isEqual(docKey), "applyToRemoteDocument: key " + docKey + " should match maybeDoc key\n " + maybeDoc.key);
}
var mutationResults = batchResult.mutationResults;
debugAssert(mutationResults.length === this.mutations.length, "Mismatch between mutations length\n (" + this.mutations.length + ") and mutation results length\n (" + mutationResults.length + ").");
for (var i = 0; i < this.mutations.length; i++) {
var mutation = this.mutations[i];
if (mutation.key.isEqual(docKey)) {
var mutationResult = mutationResults[i];
maybeDoc = mutation.applyToRemoteDocument(maybeDoc, mutationResult);
}
}
return maybeDoc;
};
/**
* Computes the local view of a document given all the mutations in this
* batch.
*
* @param docKey The key of the document to apply mutations to.
* @param maybeDoc The document to apply mutations to.
*/
MutationBatch.prototype.applyToLocalView = function (docKey, maybeDoc) {
if (maybeDoc) {
debugAssert(maybeDoc.key.isEqual(docKey), "applyToLocalDocument: key " + docKey + " should match maybeDoc key\n " + maybeDoc.key);
}
// First, apply the base state. This allows us to apply non-idempotent
// transform against a consistent set of values.
for (var _i = 0, _e = this.baseMutations; _i < _e.length; _i++) {
var mutation = _e[_i];
if (mutation.key.isEqual(docKey)) {
maybeDoc = mutation.applyToLocalView(maybeDoc, maybeDoc, this.localWriteTime);
}
}
var baseDoc = maybeDoc;
// Second, apply all user-provided mutations.
for (var _f = 0, _g = this.mutations; _f < _g.length; _f++) {
var mutation = _g[_f];
if (mutation.key.isEqual(docKey)) {
maybeDoc = mutation.applyToLocalView(maybeDoc, baseDoc, this.localWriteTime);
}
}
return maybeDoc;
};
/**
* Computes the local view for all provided documents given the mutations in
* this batch.
*/
MutationBatch.prototype.applyToLocalDocumentSet = function (maybeDocs) {
var _this = this;
// TODO(mrschmidt): This implementation is O(n^2). If we apply the mutations
// directly (as done in `applyToLocalView()`), we can reduce the complexity
// to O(n).
var mutatedDocuments = maybeDocs;
this.mutations.forEach(function (m) {
var mutatedDocument = _this.applyToLocalView(m.key, maybeDocs.get(m.key));
if (mutatedDocument) {
mutatedDocuments = mutatedDocuments.insert(m.key, mutatedDocument);
}
});
return mutatedDocuments;
};
MutationBatch.prototype.keys = function () {
return this.mutations.reduce(function (keys, m) { return keys.add(m.key); }, documentKeySet());
};
MutationBatch.prototype.isEqual = function (other) {
return (this.batchId === other.batchId &&
arrayEquals(this.mutations, other.mutations, function (l, r) { return l.isEqual(r); }) &&
arrayEquals(this.baseMutations, other.baseMutations, function (l, r) { return l.isEqual(r); }));
};
return MutationBatch;
}());
/** The result of applying a mutation batch to the backend. */
var MutationBatchResult = /** @class */ (function () {
function MutationBatchResult(batch, commitVersion, mutationResults, streamToken,
/**
* A pre-computed mapping from each mutated document to the resulting
* version.
*/
docVersions) {
this.batch = batch;
this.commitVersion = commitVersion;
this.mutationResults = mutationResults;
this.streamToken = streamToken;
this.docVersions = docVersions;
}
/**
* Creates a new MutationBatchResult for the given batch and results. There
* must be one result for each mutation in the batch. This static factory
* caches a document=>version mapping (docVersions).
*/
MutationBatchResult.from = function (batch, commitVersion, results, streamToken) {
hardAssert(batch.mutations.length === results.length, 'Mutations sent ' +
batch.mutations.length +
' must equal results received ' +
results.length);
var versionMap = documentVersionMap();
var mutations = batch.mutations;
for (var i = 0; i < mutations.length; i++) {
versionMap = versionMap.insert(mutations[i].key, results[i].version);
}
return new MutationBatchResult(batch, commitVersion, results, streamToken, versionMap);
};
return MutationBatchResult;
}());
/**
* @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 map implementation that uses objects as keys. Objects must implement the
* Equatable interface and must be immutable. Entries in the map are stored
* together with the key being produced from the mapKeyFn. This map
* automatically handles collisions of keys.
*/
var ObjectMap = /** @class */ (function () {
function ObjectMap(mapKeyFn) {
this.mapKeyFn = mapKeyFn;
/**
* The inner map for a key -> value pair. Due to the possibility of
* collisions we keep a list of entries that we do a linear search through
* to find an actual match. Note that collisions should be rare, so we still
* expect near constant time lookups in practice.
*/
this.inner = {};
}
/** Get a value for this key, or undefined if it does not exist. */
ObjectMap.prototype.get = function (key) {
var id = this.mapKeyFn(key);
var matches = this.inner[id];
if (matches === undefined) {
return undefined;
}
for (var _i = 0, matches_1 = matches; _i < matches_1.length; _i++) {
var _e = matches_1[_i], otherKey = _e[0], value = _e[1];
if (otherKey.isEqual(key)) {
return value;
}
}
return undefined;
};
ObjectMap.prototype.has = function (key) {
return this.get(key) !== undefined;
};
/** Put this key and value in the map. */
ObjectMap.prototype.set = function (key, value) {
var id = this.mapKeyFn(key);
var matches = this.inner[id];
if (matches === undefined) {
this.inner[id] = [[key, value]];
return;
}
for (var i = 0; i < matches.length; i++) {
if (matches[i][0].isEqual(key)) {
matches[i] = [key, value];
return;
}
}
matches.push([key, value]);
};
/**
* Remove this key from the map. Returns a boolean if anything was deleted.
*/
ObjectMap.prototype.delete = function (key) {
var id = this.mapKeyFn(key);
var matches = this.inner[id];
if (matches === undefined) {
return false;
}
for (var i = 0; i < matches.length; i++) {
if (matches[i][0].isEqual(key)) {
if (matches.length === 1) {
delete this.inner[id];
}
else {
matches.splice(i, 1);
}
return true;
}
}
return false;
};
ObjectMap.prototype.forEach = function (fn) {
forEach(this.inner, function (_, entries) {
for (var _i = 0, entries_1 = entries; _i < entries_1.length; _i++) {
var _e = entries_1[_i], k = _e[0], v = _e[1];
fn(k, v);
}
});
};
ObjectMap.prototype.isEmpty = function () {
return isEmpty(this.inner);
};
return ObjectMap;
}());
/**
* @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.
*/
/**
* PersistencePromise<> is essentially a re-implementation of Promise<> except
* it has a .next() method instead of .then() and .next() and .catch() callbacks
* are executed synchronously when a PersistencePromise resolves rather than
* asynchronously (Promise<> implementations use setImmediate() or similar).
*
* This is necessary to interoperate with IndexedDB which will automatically
* commit transactions if control is returned to the event loop without
* synchronously initiating another operation on the transaction.
*
* NOTE: .then() and .catch() only allow a single consumer, unlike normal
* Promises.
*/
var PersistencePromise = /** @class */ (function () {
function PersistencePromise(callback) {
var _this = this;
// NOTE: next/catchCallback will always point to our own wrapper functions,
// not the user's raw next() or catch() callbacks.
this.nextCallback = null;
this.catchCallback = null;
// When the operation resolves, we'll set result or error and mark isDone.
this.result = undefined;
this.error = undefined;
this.isDone = false;
// Set to true when .then() or .catch() are called and prevents additional
// chaining.
this.callbackAttached = false;
callback(function (value) {
_this.isDone = true;
_this.result = value;
if (_this.nextCallback) {
// value should be defined unless T is Void, but we can't express
// that in the type system.
_this.nextCallback(value);
}
}, function (error) {
_this.isDone = true;
_this.error = error;
if (_this.catchCallback) {
_this.catchCallback(error);
}
});
}
PersistencePromise.prototype.catch = function (fn) {
return this.next(undefined, fn);
};
PersistencePromise.prototype.next = function (nextFn, catchFn) {
var _this = this;
if (this.callbackAttached) {
fail('Called next() or catch() twice for PersistencePromise');
}
this.callbackAttached = true;
if (this.isDone) {
if (!this.error) {
return this.wrapSuccess(nextFn, this.result);
}
else {
return this.wrapFailure(catchFn, this.error);
}
}
else {
return new PersistencePromise(function (resolve, reject) {
_this.nextCallback = function (value) {
_this.wrapSuccess(nextFn, value).next(resolve, reject);
};
_this.catchCallback = function (error) {
_this.wrapFailure(catchFn, error).next(resolve, reject);
};
});
}
};
PersistencePromise.prototype.toPromise = function () {
var _this = this;
return new Promise(function (resolve, reject) {
_this.next(resolve, reject);
});
};
PersistencePromise.prototype.wrapUserFunction = function (fn) {
try {
var result = fn();
if (result instanceof PersistencePromise) {
return result;
}
else {
return PersistencePromise.resolve(result);
}
}
catch (e) {
return PersistencePromise.reject(e);
}
};
PersistencePromise.prototype.wrapSuccess = function (nextFn, value) {
if (nextFn) {
return this.wrapUserFunction(function () { return nextFn(value); });
}
else {
// If there's no nextFn, then R must be the same as T
return PersistencePromise.resolve(value);
}
};
PersistencePromise.prototype.wrapFailure = function (catchFn, error) {
if (catchFn) {
return this.wrapUserFunction(function () { return catchFn(error); });
}
else {
return PersistencePromise.reject(error);
}
};
PersistencePromise.resolve = function (result) {
return new PersistencePromise(function (resolve, reject) {
resolve(result);
});
};
PersistencePromise.reject = function (error) {
return new PersistencePromise(function (resolve, reject) {
reject(error);
});
};
PersistencePromise.waitFor = function (
// Accept all Promise types in waitFor().
// eslint-disable-next-line @typescript-eslint/no-explicit-any
all) {
return new PersistencePromise(function (resolve, reject) {
var expectedCount = 0;
var resolvedCount = 0;
var done = false;
all.forEach(function (element) {
++expectedCount;
element.next(function () {
++resolvedCount;
if (done && resolvedCount === expectedCount) {
resolve();
}
}, function (err) { return reject(err); });
});
done = true;
if (resolvedCount === expectedCount) {
resolve();
}
});
};
/**
* Given an array of predicate functions that asynchronously evaluate to a
* boolean, implements a short-circuiting `or` between the results. Predicates
* will be evaluated until one of them returns `true`, then stop. The final
* result will be whether any of them returned `true`.
*/
PersistencePromise.or = function (predicates) {
var p = PersistencePromise.resolve(false);
var _loop_1 = function (predicate) {
p = p.next(function (isTrue) {
if (isTrue) {
return PersistencePromise.resolve(isTrue);
}
else {
return predicate();
}
});
};
for (var _i = 0, predicates_1 = predicates; _i < predicates_1.length; _i++) {
var predicate = predicates_1[_i];
_loop_1(predicate);
}
return p;
};
PersistencePromise.forEach = function (collection, f) {
var _this = this;
var promises = [];
collection.forEach(function (r, s) {
promises.push(f.call(_this, r, s));
});
return this.waitFor(promises);
};
return PersistencePromise;
}());
/**
* @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 readonly view of the local state of all documents we're tracking (i.e. we
* have a cached version in remoteDocumentCache or local mutations for the
* document). The view is computed by applying the mutations in the
* MutationQueue to the RemoteDocumentCache.
*/
var LocalDocumentsView = /** @class */ (function () {
function LocalDocumentsView(remoteDocumentCache, mutationQueue, indexManager) {
this.remoteDocumentCache = remoteDocumentCache;
this.mutationQueue = mutationQueue;
this.indexManager = indexManager;
}
/**
* Get the local view of the document identified by `key`.
*
* @return Local view of the document or null if we don't have any cached
* state for it.
*/
LocalDocumentsView.prototype.getDocument = function (transaction, key) {
var _this = this;
return this.mutationQueue
.getAllMutationBatchesAffectingDocumentKey(transaction, key)
.next(function (batches) { return _this.getDocumentInternal(transaction, key, batches); });
};
/** Internal version of `getDocument` that allows reusing batches. */
LocalDocumentsView.prototype.getDocumentInternal = function (transaction, key, inBatches) {
return this.remoteDocumentCache.getEntry(transaction, key).next(function (doc) {
for (var _i = 0, inBatches_1 = inBatches; _i < inBatches_1.length; _i++) {
var batch = inBatches_1[_i];
doc = batch.applyToLocalView(key, doc);
}
return doc;
});
};
// Returns the view of the given `docs` as they would appear after applying
// all mutations in the given `batches`.
LocalDocumentsView.prototype.applyLocalMutationsToDocuments = function (transaction, docs, batches) {
var results = nullableMaybeDocumentMap();
docs.forEach(function (key, localView) {
for (var _i = 0, batches_1 = batches; _i < batches_1.length; _i++) {
var batch = batches_1[_i];
localView = batch.applyToLocalView(key, localView);
}
results = results.insert(key, localView);
});
return results;
};
/**
* Gets the local view of the documents identified by `keys`.
*
* If we don't have cached state for a document in `keys`, a NoDocument will
* be stored for that key in the resulting set.
*/
LocalDocumentsView.prototype.getDocuments = function (transaction, keys) {
var _this = this;
return this.remoteDocumentCache
.getEntries(transaction, keys)
.next(function (docs) { return _this.getLocalViewOfDocuments(transaction, docs); });
};
/**
* Similar to `getDocuments`, but creates the local view from the given
* `baseDocs` without retrieving documents from the local store.
*/
LocalDocumentsView.prototype.getLocalViewOfDocuments = function (transaction, baseDocs) {
var _this = this;
return this.mutationQueue
.getAllMutationBatchesAffectingDocumentKeys(transaction, baseDocs)
.next(function (batches) {
var docs = _this.applyLocalMutationsToDocuments(transaction, baseDocs, batches);
var results = maybeDocumentMap();
docs.forEach(function (key, maybeDoc) {
// TODO(http://b/32275378): Don't conflate missing / deleted.
if (!maybeDoc) {
maybeDoc = new NoDocument(key, SnapshotVersion.min());
}
results = results.insert(key, maybeDoc);
});
return results;
});
};
/**
* Performs a query against the local view of all documents.
*
* @param transaction The persistence transaction.
* @param query The query to match documents against.
* @param sinceReadTime If not set to SnapshotVersion.min(), return only
* documents that have been read since this snapshot version (exclusive).
*/
LocalDocumentsView.prototype.getDocumentsMatchingQuery = function (transaction, query, sinceReadTime) {
if (query.isDocumentQuery()) {
return this.getDocumentsMatchingDocumentQuery(transaction, query.path);
}
else if (query.isCollectionGroupQuery()) {
return this.getDocumentsMatchingCollectionGroupQuery(transaction, query, sinceReadTime);
}
else {
return this.getDocumentsMatchingCollectionQuery(transaction, query, sinceReadTime);
}
};
LocalDocumentsView.prototype.getDocumentsMatchingDocumentQuery = function (transaction, docPath) {
// Just do a simple document lookup.
return this.getDocument(transaction, new DocumentKey(docPath)).next(function (maybeDoc) {
var result = documentMap();
if (maybeDoc instanceof Document) {
result = result.insert(maybeDoc.key, maybeDoc);
}
return result;
});
};
LocalDocumentsView.prototype.getDocumentsMatchingCollectionGroupQuery = function (transaction, query, sinceReadTime) {
var _this = this;
debugAssert(query.path.isEmpty(), 'Currently we only support collection group queries at the root.');
var collectionId = query.collectionGroup;
var results = documentMap();
return this.indexManager
.getCollectionParents(transaction, collectionId)
.next(function (parents) {
// Perform a collection query against each parent that contains the
// collectionId and aggregate the results.
return PersistencePromise.forEach(parents, function (parent) {
var collectionQuery = query.asCollectionQueryAtPath(parent.child(collectionId));
return _this.getDocumentsMatchingCollectionQuery(transaction, collectionQuery, sinceReadTime).next(function (r) {
r.forEach(function (key, doc) {
results = results.insert(key, doc);
});
});
}).next(function () { return results; });
});
};
LocalDocumentsView.prototype.getDocumentsMatchingCollectionQuery = function (transaction, query, sinceReadTime) {
var _this = this;
// Query the remote documents and overlay mutations.
var results;
var mutationBatches;
return this.remoteDocumentCache
.getDocumentsMatchingQuery(transaction, query, sinceReadTime)
.next(function (queryResults) {
results = queryResults;
return _this.mutationQueue.getAllMutationBatchesAffectingQuery(transaction, query);
})
.next(function (matchingMutationBatches) {
mutationBatches = matchingMutationBatches;
// It is possible that a PatchMutation can make a document match a query, even if
// the version in the RemoteDocumentCache is not a match yet (waiting for server
// to ack). To handle this, we find all document keys affected by the PatchMutations
// that are not in `result` yet, and back fill them via `remoteDocumentCache.getEntries`,
// otherwise those `PatchMutations` will be ignored because no base document can be found,
// and lead to missing result for the query.
return _this.addMissingBaseDocuments(transaction, mutationBatches, results).next(function (mergedDocuments) {
results = mergedDocuments;
for (var _i = 0, mutationBatches_1 = mutationBatches; _i < mutationBatches_1.length; _i++) {
var batch = mutationBatches_1[_i];
for (var _e = 0, _f = batch.mutations; _e < _f.length; _e++) {
var mutation = _f[_e];
var key = mutation.key;
var baseDoc = results.get(key);
var mutatedDoc = mutation.applyToLocalView(baseDoc, baseDoc, batch.localWriteTime);
if (mutatedDoc instanceof Document) {
results = results.insert(key, mutatedDoc);
}
else {
results = results.remove(key);
}
}
}
});
})
.next(function () {
// Finally, filter out any documents that don't actually match
// the query.
results.forEach(function (key, doc) {
if (!query.matches(doc)) {
results = results.remove(key);
}
});
return results;
});
};
LocalDocumentsView.prototype.addMissingBaseDocuments = function (transaction, matchingMutationBatches, existingDocuments) {
var missingBaseDocEntriesForPatching = documentKeySet();
for (var _i = 0, matchingMutationBatches_1 = matchingMutationBatches; _i < matchingMutationBatches_1.length; _i++) {
var batch = matchingMutationBatches_1[_i];
for (var _e = 0, _f = batch.mutations; _e < _f.length; _e++) {
var mutation = _f[_e];
if (mutation instanceof PatchMutation &&
existingDocuments.get(mutation.key) === null) {
missingBaseDocEntriesForPatching = missingBaseDocEntriesForPatching.add(mutation.key);
}
}
}
var mergedDocuments = existingDocuments;
return this.remoteDocumentCache
.getEntries(transaction, missingBaseDocEntriesForPatching)
.next(function (missingBaseDocs) {
missingBaseDocs.forEach(function (key, doc) {
if (doc !== null && doc instanceof Document) {
mergedDocuments = mergedDocuments.insert(key, doc);
}
});
return mergedDocuments;
});
};
return LocalDocumentsView;
}());
/**
* @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.
*/
var PRIMARY_LEASE_LOST_ERROR_MSG = 'The current tab is not in the required state to perform this operation. ' +
'It might be necessary to refresh the browser tab.';
/**
* A base class representing a persistence transaction, encapsulating both the
* transaction's sequence numbers as well as a list of onCommitted listeners.
*
* When you call Persistence.runTransaction(), it will create a transaction and
* pass it to your callback. You then pass it to any method that operates
* on persistence.
*/
var PersistenceTransaction = /** @class */ (function () {
function PersistenceTransaction() {
this.onCommittedListeners = [];
}
PersistenceTransaction.prototype.addOnCommittedListener = function (listener) {
this.onCommittedListeners.push(listener);
};
PersistenceTransaction.prototype.raiseOnCommittedEvent = function () {
this.onCommittedListeners.forEach(function (listener) { return listener(); });
};
return PersistenceTransaction;
}());
/**
* @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 set of metadata that the local store tracks for each target.
*/
var TargetData = /** @class */ (function () {
function TargetData(
/** The target being listened to. */
target,
/**
* The target ID to which the target corresponds; Assigned by the
* LocalStore for user listens and by the SyncEngine for limbo watches.
*/
targetId,
/** The purpose of the target. */
purpose,
/**
* The sequence number of the last transaction during which this target data
* was modified.
*/
sequenceNumber,
/** The latest snapshot version seen for this target. */
snapshotVersion,
/**
* The maximum snapshot version at which the associated view
* contained no limbo documents.
*/
lastLimboFreeSnapshotVersion,
/**
* An opaque, server-assigned token that allows watching a target to be
* resumed after disconnecting without retransmitting all the data that
* matches the target. The resume token essentially identifies a point in
* time from which the server should resume sending results.
*/
resumeToken) {
if (snapshotVersion === void 0) { snapshotVersion = SnapshotVersion.min(); }
if (lastLimboFreeSnapshotVersion === void 0) { lastLimboFreeSnapshotVersion = SnapshotVersion.min(); }
if (resumeToken === void 0) { resumeToken = ByteString.EMPTY_BYTE_STRING; }
this.target = target;
this.targetId = targetId;
this.purpose = purpose;
this.sequenceNumber = sequenceNumber;
this.snapshotVersion = snapshotVersion;
this.lastLimboFreeSnapshotVersion = lastLimboFreeSnapshotVersion;
this.resumeToken = resumeToken;
}
/** Creates a new target data instance with an updated sequence number. */
TargetData.prototype.withSequenceNumber = function (sequenceNumber) {
return new TargetData(this.target, this.targetId, this.purpose, sequenceNumber, this.snapshotVersion, this.lastLimboFreeSnapshotVersion, this.resumeToken);
};
/**
* Creates a new target data instance with an updated resume token and
* snapshot version.
*/
TargetData.prototype.withResumeToken = function (resumeToken, snapshotVersion) {
return new TargetData(this.target, this.targetId, this.purpose, this.sequenceNumber, snapshotVersion, this.lastLimboFreeSnapshotVersion, resumeToken);
};
/**
* Creates a new target data instance with an updated last limbo free
* snapshot version number.
*/
TargetData.prototype.withLastLimboFreeSnapshotVersion = function (lastLimboFreeSnapshotVersion) {
return new TargetData(this.target, this.targetId, this.purpose, this.sequenceNumber, this.snapshotVersion, lastLimboFreeSnapshotVersion, this.resumeToken);
};
return TargetData;
}());
/**
* @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.
*/
var Deferred = /** @class */ (function () {
function Deferred() {
var _this = this;
this.promise = new Promise(function (resolve, reject) {
_this.resolve = resolve;
_this.reject = reject;
});
}
return Deferred;
}());
/**
* @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.
*/
var escapeChar = '\u0001';
var encodedSeparatorChar = '\u0001';
var encodedNul = '\u0010';
var encodedEscape = '\u0011';
/**
* Encodes a resource path into a IndexedDb-compatible string form.
*/
function encodeResourcePath(path) {
var result = '';
for (var i = 0; i < path.length; i++) {
if (result.length > 0) {
result = encodeSeparator(result);
}
result = encodeSegment(path.get(i), result);
}
return encodeSeparator(result);
}
/** Encodes a single segment of a resource path into the given result */
function encodeSegment(segment, resultBuf) {
var result = resultBuf;
var length = segment.length;
for (var i = 0; i < length; i++) {
var c = segment.charAt(i);
switch (c) {
case '\0':
result += escapeChar + encodedNul;
break;
case escapeChar:
result += escapeChar + encodedEscape;
break;
default:
result += c;
}
}
return result;
}
/** Encodes a path separator into the given result */
function encodeSeparator(result) {
return result + escapeChar + encodedSeparatorChar;
}
/**
* Decodes the given IndexedDb-compatible string form of a resource path into
* a ResourcePath instance. Note that this method is not suitable for use with
* decoding resource names from the server; those are One Platform format
* strings.
*/
function decodeResourcePath(path) {
// Event the empty path must encode as a path of at least length 2. A path
// with exactly 2 must be the empty path.
var length = path.length;
hardAssert(length >= 2, 'Invalid path ' + path);
if (length === 2) {
hardAssert(path.charAt(0) === escapeChar && path.charAt(1) === encodedSeparatorChar, 'Non-empty path ' + path + ' had length 2');
return ResourcePath.EMPTY_PATH;
}
// Escape characters cannot exist past the second-to-last position in the
// source value.
var lastReasonableEscapeIndex = length - 2;
var segments = [];
var segmentBuilder = '';
for (var start = 0; start < length;) {
// The last two characters of a valid encoded path must be a separator, so
// there must be an end to this segment.
var end = path.indexOf(escapeChar, start);
if (end < 0 || end > lastReasonableEscapeIndex) {
fail('Invalid encoded resource path: "' + path + '"');
}
var next = path.charAt(end + 1);
switch (next) {
case encodedSeparatorChar:
var currentPiece = path.substring(start, end);
var segment = void 0;
if (segmentBuilder.length === 0) {
// Avoid copying for the common case of a segment that excludes \0
// and \001
segment = currentPiece;
}
else {
segmentBuilder += currentPiece;
segment = segmentBuilder;
segmentBuilder = '';
}
segments.push(segment);
break;
case encodedNul:
segmentBuilder += path.substring(start, end);
segmentBuilder += '\0';
break;
case encodedEscape:
// The escape character can be used in the output to encode itself.
segmentBuilder += path.substring(start, end + 1);
break;
default:
fail('Invalid encoded resource path: "' + path + '"');
}
start = end + 2;
}
return new ResourcePath(segments);
}
/**
* @license
* Copyright 2019 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 in-memory implementation of IndexManager.
*/
var MemoryIndexManager = /** @class */ (function () {
function MemoryIndexManager() {
this.collectionParentIndex = new MemoryCollectionParentIndex();
}
MemoryIndexManager.prototype.addToCollectionParentIndex = function (transaction, collectionPath) {
this.collectionParentIndex.add(collectionPath);
return PersistencePromise.resolve();
};
MemoryIndexManager.prototype.getCollectionParents = function (transaction, collectionId) {
return PersistencePromise.resolve(this.collectionParentIndex.getEntries(collectionId));
};
return MemoryIndexManager;
}());
/**
* Internal implementation of the collection-parent index exposed by MemoryIndexManager.
* Also used for in-memory caching by IndexedDbIndexManager and initial index population
* in indexeddb_schema.ts
*/
var MemoryCollectionParentIndex = /** @class */ (function () {
function MemoryCollectionParentIndex() {
this.index = {};
}
// Returns false if the entry already existed.
MemoryCollectionParentIndex.prototype.add = function (collectionPath) {
debugAssert(collectionPath.length % 2 === 1, 'Expected a collection path.');
var collectionId = collectionPath.lastSegment();
var parentPath = collectionPath.popLast();
var existingParents = this.index[collectionId] ||
new SortedSet(ResourcePath.comparator);
var added = !existingParents.has(parentPath);
this.index[collectionId] = existingParents.add(parentPath);
return added;
};
MemoryCollectionParentIndex.prototype.has = function (collectionPath) {
var collectionId = collectionPath.lastSegment();
var parentPath = collectionPath.popLast();
var existingParents = this.index[collectionId];
return existingParents && existingParents.has(parentPath);
};
MemoryCollectionParentIndex.prototype.getEntries = function (collectionId) {
var parentPaths = this.index[collectionId] ||
new SortedSet(ResourcePath.comparator);
return parentPaths.toArray();
};
return MemoryCollectionParentIndex;
}());
/**
* @license
* Copyright 2019 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 persisted implementation of IndexManager.
*/
var IndexedDbIndexManager = /** @class */ (function () {
function IndexedDbIndexManager() {
/**
* An in-memory copy of the index entries we've already written since the SDK
* launched. Used to avoid re-writing the same entry repeatedly.
*
* This is *NOT* a complete cache of what's in persistence and so can never be used to
* satisfy reads.
*/
this.collectionParentsCache = new MemoryCollectionParentIndex();
}
/**
* Adds a new entry to the collection parent index.
*
* Repeated calls for the same collectionPath should be avoided within a
* transaction as IndexedDbIndexManager only caches writes once a transaction
* has been committed.
*/
IndexedDbIndexManager.prototype.addToCollectionParentIndex = function (transaction, collectionPath) {
var _this = this;
debugAssert(collectionPath.length % 2 === 1, 'Expected a collection path.');
if (!this.collectionParentsCache.has(collectionPath)) {
var collectionId = collectionPath.lastSegment();
var parentPath = collectionPath.popLast();
transaction.addOnCommittedListener(function () {
// Add the collection to the in memory cache only if the transaction was
// successfully committed.
_this.collectionParentsCache.add(collectionPath);
});
var collectionParent = {
collectionId: collectionId,
parent: encodeResourcePath(parentPath)
};
return collectionParentsStore(transaction).put(collectionParent);
}
return PersistencePromise.resolve();
};
IndexedDbIndexManager.prototype.getCollectionParents = function (transaction, collectionId) {
var parentPaths = [];
var range = IDBKeyRange.bound([collectionId, ''], [immediateSuccessor(collectionId), ''],
/*lowerOpen=*/ false,
/*upperOpen=*/ true);
return collectionParentsStore(transaction)
.loadAll(range)
.next(function (entries) {
for (var _i = 0, entries_2 = entries; _i < entries_2.length; _i++) {
var entry = entries_2[_i];
// This collectionId guard shouldn't be necessary (and isn't as long
// as we're running in a real browser), but there's a bug in
// indexeddbshim that breaks our range in our tests running in node:
// https://github.com/axemclion/IndexedDBShim/issues/334
if (entry.collectionId !== collectionId) {
break;
}
parentPaths.push(decodeResourcePath(entry.parent));
}
return parentPaths;
});
};
return IndexedDbIndexManager;
}());
/**
* Helper to get a typed SimpleDbStore for the collectionParents
* document store.
*/
function collectionParentsStore(txn) {
return IndexedDbPersistence.getStore(txn, DbCollectionParent.store);
}
/**
* @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 in-memory buffer of entries to be written to a RemoteDocumentCache.
* It can be used to batch up a set of changes to be written to the cache, but
* additionally supports reading entries back with the `getEntry()` method,
* falling back to the underlying RemoteDocumentCache if no entry is
* buffered.
*
* Entries added to the cache *must* be read first. This is to facilitate
* calculating the size delta of the pending changes.
*
* PORTING NOTE: This class was implemented then removed from other platforms.
* If byte-counting ends up being needed on the other platforms, consider
* porting this class as part of that implementation work.
*/
var RemoteDocumentChangeBuffer = /** @class */ (function () {
function RemoteDocumentChangeBuffer() {
// A mapping of document key to the new cache entry that should be written (or null if any
// existing cache entry should be removed).
this.changes = new ObjectMap(function (key) { return key.toString(); });
this.changesApplied = false;
}
Object.defineProperty(RemoteDocumentChangeBuffer.prototype, "readTime", {
get: function () {
debugAssert(this._readTime !== undefined, 'Read time is not set. All removeEntry() calls must include a readTime if `trackRemovals` is used.');
return this._readTime;
},
set: function (value) {
// Right now (for simplicity) we just track a single readTime for all the
// added entries since we expect them to all be the same, but we could
// rework to store per-entry readTimes if necessary.
debugAssert(this._readTime === undefined || this._readTime.isEqual(value), 'All changes in a RemoteDocumentChangeBuffer must have the same read time');
this._readTime = value;
},
enumerable: true,
configurable: true
});
/**
* Buffers a `RemoteDocumentCache.addEntry()` call.
*
* You can only modify documents that have already been retrieved via
* `getEntry()/getEntries()` (enforced via IndexedDbs `apply()`).
*/
RemoteDocumentChangeBuffer.prototype.addEntry = function (maybeDocument, readTime) {
this.assertNotApplied();
this.readTime = readTime;
this.changes.set(maybeDocument.key, maybeDocument);
};
/**
* Buffers a `RemoteDocumentCache.removeEntry()` call.
*
* You can only remove documents that have already been retrieved via
* `getEntry()/getEntries()` (enforced via IndexedDbs `apply()`).
*/
RemoteDocumentChangeBuffer.prototype.removeEntry = function (key, readTime) {
this.assertNotApplied();
if (readTime) {
this.readTime = readTime;
}
this.changes.set(key, null);
};
/**
* Looks up an entry in the cache. The buffered changes will first be checked,
* and if no buffered change applies, this will forward to
* `RemoteDocumentCache.getEntry()`.
*
* @param transaction The transaction in which to perform any persistence
* operations.
* @param documentKey The key of the entry to look up.
* @return The cached Document or NoDocument entry, or null if we have nothing
* cached.
*/
RemoteDocumentChangeBuffer.prototype.getEntry = function (transaction, documentKey) {
this.assertNotApplied();
var bufferedEntry = this.changes.get(documentKey);
if (bufferedEntry !== undefined) {
return PersistencePromise.resolve(bufferedEntry);
}
else {
return this.getFromCache(transaction, documentKey);
}
};
/**
* Looks up several entries in the cache, forwarding to
* `RemoteDocumentCache.getEntry()`.
*
* @param transaction The transaction in which to perform any persistence
* operations.
* @param documentKeys The keys of the entries to look up.
* @return A map of cached `Document`s or `NoDocument`s, indexed by key. If an
* entry cannot be found, the corresponding key will be mapped to a null
* value.
*/
RemoteDocumentChangeBuffer.prototype.getEntries = function (transaction, documentKeys) {
return this.getAllFromCache(transaction, documentKeys);
};
/**
* Applies buffered changes to the underlying RemoteDocumentCache, using
* the provided transaction.
*/
RemoteDocumentChangeBuffer.prototype.apply = function (transaction) {
this.assertNotApplied();
this.changesApplied = true;
return this.applyChanges(transaction);
};
/** Helper to assert this.changes is not null */
RemoteDocumentChangeBuffer.prototype.assertNotApplied = function () {
debugAssert(!this.changesApplied, 'Changes have already been applied.');
};
return RemoteDocumentChangeBuffer;
}());
/**
* @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.
*/
var IndexedDbRemoteDocumentCache = /** @class */ (function () {
/**
* @param {LocalSerializer} serializer The document serializer.
* @param {IndexManager} indexManager The query indexes that need to be maintained.
*/
function IndexedDbRemoteDocumentCache(serializer, indexManager) {
this.serializer = serializer;
this.indexManager = indexManager;
}
/**
* Adds the supplied entries to the cache.
*
* All calls of `addEntry` are required to go through the RemoteDocumentChangeBuffer
* returned by `newChangeBuffer()` to ensure proper accounting of metadata.
*/
IndexedDbRemoteDocumentCache.prototype.addEntry = function (transaction, key, doc) {
var documentStore = remoteDocumentsStore(transaction);
return documentStore.put(dbKey(key), doc);
};
/**
* Removes a document from the cache.
*
* All calls of `removeEntry` are required to go through the RemoteDocumentChangeBuffer
* returned by `newChangeBuffer()` to ensure proper accounting of metadata.
*/
IndexedDbRemoteDocumentCache.prototype.removeEntry = function (transaction, documentKey) {
var store = remoteDocumentsStore(transaction);
var key = dbKey(documentKey);
return store.delete(key);
};
/**
* Updates the current cache size.
*
* Callers to `addEntry()` and `removeEntry()` *must* call this afterwards to update the
* cache's metadata.
*/
IndexedDbRemoteDocumentCache.prototype.updateMetadata = function (transaction, sizeDelta) {
var _this = this;
return this.getMetadata(transaction).next(function (metadata) {
metadata.byteSize += sizeDelta;
return _this.setMetadata(transaction, metadata);
});
};
IndexedDbRemoteDocumentCache.prototype.getEntry = function (transaction, documentKey) {
var _this = this;
return remoteDocumentsStore(transaction)
.get(dbKey(documentKey))
.next(function (dbRemoteDoc) {
return _this.maybeDecodeDocument(dbRemoteDoc);
});
};
/**
* Looks up an entry in the cache.
*
* @param documentKey The key of the entry to look up.
* @return The cached MaybeDocument entry and its size, or null if we have nothing cached.
*/
IndexedDbRemoteDocumentCache.prototype.getSizedEntry = function (transaction, documentKey) {
var _this = this;
return remoteDocumentsStore(transaction)
.get(dbKey(documentKey))
.next(function (dbRemoteDoc) {
var doc = _this.maybeDecodeDocument(dbRemoteDoc);
return doc
? {
maybeDocument: doc,
size: dbDocumentSize(dbRemoteDoc)
}
: null;
});
};
IndexedDbRemoteDocumentCache.prototype.getEntries = function (transaction, documentKeys) {
var _this = this;
var results = nullableMaybeDocumentMap();
return this.forEachDbEntry(transaction, documentKeys, function (key, dbRemoteDoc) {
var doc = _this.maybeDecodeDocument(dbRemoteDoc);
results = results.insert(key, doc);
}).next(function () { return results; });
};
/**
* Looks up several entries in the cache.
*
* @param documentKeys The set of keys entries to look up.
* @return A map of MaybeDocuments indexed by key (if a document cannot be
* found, the key will be mapped to null) and a map of sizes indexed by
* key (zero if the key cannot be found).
*/
IndexedDbRemoteDocumentCache.prototype.getSizedEntries = function (transaction, documentKeys) {
var _this = this;
var results = nullableMaybeDocumentMap();
var sizeMap = new SortedMap(DocumentKey.comparator);
return this.forEachDbEntry(transaction, documentKeys, function (key, dbRemoteDoc) {
var doc = _this.maybeDecodeDocument(dbRemoteDoc);
if (doc) {
results = results.insert(key, doc);
sizeMap = sizeMap.insert(key, dbDocumentSize(dbRemoteDoc));
}
else {
results = results.insert(key, null);
sizeMap = sizeMap.insert(key, 0);
}
}).next(function () {
return { maybeDocuments: results, sizeMap: sizeMap };
});
};
IndexedDbRemoteDocumentCache.prototype.forEachDbEntry = function (transaction, documentKeys, callback) {
if (documentKeys.isEmpty()) {
return PersistencePromise.resolve();
}
var range = IDBKeyRange.bound(documentKeys.first().path.toArray(), documentKeys.last().path.toArray());
var keyIter = documentKeys.getIterator();
var nextKey = keyIter.getNext();
return remoteDocumentsStore(transaction)
.iterate({ range: range }, function (potentialKeyRaw, dbRemoteDoc, control) {
var potentialKey = DocumentKey.fromSegments(potentialKeyRaw);
// Go through keys not found in cache.
while (nextKey && DocumentKey.comparator(nextKey, potentialKey) < 0) {
callback(nextKey, null);
nextKey = keyIter.getNext();
}
if (nextKey && nextKey.isEqual(potentialKey)) {
// Key found in cache.
callback(nextKey, dbRemoteDoc);
nextKey = keyIter.hasNext() ? keyIter.getNext() : null;
}
// Skip to the next key (if there is one).
if (nextKey) {
control.skip(nextKey.path.toArray());
}
else {
control.done();
}
})
.next(function () {
// The rest of the keys are not in the cache. One case where `iterate`
// above won't go through them is when the cache is empty.
while (nextKey) {
callback(nextKey, null);
nextKey = keyIter.hasNext() ? keyIter.getNext() : null;
}
});
};
IndexedDbRemoteDocumentCache.prototype.getDocumentsMatchingQuery = function (transaction, query, sinceReadTime) {
var _this = this;
debugAssert(!query.isCollectionGroupQuery(), 'CollectionGroup queries should be handled in LocalDocumentsView');
var results = documentMap();
var immediateChildrenPathLength = query.path.length + 1;
var iterationOptions = {};
if (sinceReadTime.isEqual(SnapshotVersion.min())) {
// Documents are ordered by key, so we can use a prefix scan to narrow
// down the documents we need to match the query against.
var startKey = query.path.toArray();
iterationOptions.range = IDBKeyRange.lowerBound(startKey);
}
else {
// Execute an index-free query and filter by read time. This is safe
// since all document changes to queries that have a
// lastLimboFreeSnapshotVersion (`sinceReadTime`) have a read time set.
var collectionKey = query.path.toArray();
var readTimeKey = this.serializer.toDbTimestampKey(sinceReadTime);
iterationOptions.range = IDBKeyRange.lowerBound([collectionKey, readTimeKey],
/* open= */ true);
iterationOptions.index = DbRemoteDocument.collectionReadTimeIndex;
}
return remoteDocumentsStore(transaction)
.iterate(iterationOptions, function (key, dbRemoteDoc, control) {
// The query is actually returning any path that starts with the query
// path prefix which may include documents in subcollections. For
// example, a query on 'rooms' will return rooms/abc/messages/xyx but we
// shouldn't match it. Fix this by discarding rows with document keys
// more than one segment longer than the query path.
if (key.length !== immediateChildrenPathLength) {
return;
}
var maybeDoc = _this.serializer.fromDbRemoteDocument(dbRemoteDoc);
if (!query.path.isPrefixOf(maybeDoc.key.path)) {
control.done();
}
else if (maybeDoc instanceof Document && query.matches(maybeDoc)) {
results = results.insert(maybeDoc.key, maybeDoc);
}
})
.next(function () { return results; });
};
/**
* Returns the set of documents that have changed since the specified read
* time.
*/
// PORTING NOTE: This is only used for multi-tab synchronization.
IndexedDbRemoteDocumentCache.prototype.getNewDocumentChanges = function (transaction, sinceReadTime) {
var _this = this;
var changedDocs = maybeDocumentMap();
var lastReadTime = this.serializer.toDbTimestampKey(sinceReadTime);
var documentsStore = remoteDocumentsStore(transaction);
var range = IDBKeyRange.lowerBound(lastReadTime, true);
return documentsStore
.iterate({ index: DbRemoteDocument.readTimeIndex, range: range }, function (_, dbRemoteDoc) {
// Unlike `getEntry()` and others, `getNewDocumentChanges()` parses
// the documents directly since we want to keep sentinel deletes.
var doc = _this.serializer.fromDbRemoteDocument(dbRemoteDoc);
changedDocs = changedDocs.insert(doc.key, doc);
lastReadTime = dbRemoteDoc.readTime;
})
.next(function () {
return {
changedDocs: changedDocs,
readTime: _this.serializer.fromDbTimestampKey(lastReadTime)
};
});
};
/**
* Returns the read time of the most recently read document in the cache, or
* SnapshotVersion.min() if not available.
*/
// PORTING NOTE: This is only used for multi-tab synchronization.
IndexedDbRemoteDocumentCache.prototype.getLastReadTime = function (transaction) {
var _this = this;
var documentsStore = remoteDocumentsStore(transaction);
// If there are no existing entries, we return SnapshotVersion.min().
var readTime = SnapshotVersion.min();
return documentsStore
.iterate({ index: DbRemoteDocument.readTimeIndex, reverse: true }, function (key, dbRemoteDoc, control) {
if (dbRemoteDoc.readTime) {
readTime = _this.serializer.fromDbTimestampKey(dbRemoteDoc.readTime);
}
control.done();
})
.next(function () { return readTime; });
};
IndexedDbRemoteDocumentCache.prototype.newChangeBuffer = function (options) {
return new IndexedDbRemoteDocumentCache.RemoteDocumentChangeBuffer(this, !!options && options.trackRemovals);
};
IndexedDbRemoteDocumentCache.prototype.getSize = function (txn) {
return this.getMetadata(txn).next(function (metadata) { return metadata.byteSize; });
};
IndexedDbRemoteDocumentCache.prototype.getMetadata = function (txn) {
return documentGlobalStore(txn)
.get(DbRemoteDocumentGlobal.key)
.next(function (metadata) {
hardAssert(!!metadata, 'Missing document cache metadata');
return metadata;
});
};
IndexedDbRemoteDocumentCache.prototype.setMetadata = function (txn, metadata) {
return documentGlobalStore(txn).put(DbRemoteDocumentGlobal.key, metadata);
};
/**
* Decodes `remoteDoc` and returns the document (or null, if the document
* corresponds to the format used for sentinel deletes).
*/
IndexedDbRemoteDocumentCache.prototype.maybeDecodeDocument = function (dbRemoteDoc) {
if (dbRemoteDoc) {
var doc = this.serializer.fromDbRemoteDocument(dbRemoteDoc);
if (doc instanceof NoDocument &&
doc.version.isEqual(SnapshotVersion.min())) {
// The document is a sentinel removal and should only be used in the
// `getNewDocumentChanges()`.
return null;
}
return doc;
}
return null;
};
return IndexedDbRemoteDocumentCache;
}());
/**
* Handles the details of adding and updating documents in the IndexedDbRemoteDocumentCache.
*
* Unlike the MemoryRemoteDocumentChangeBuffer, the IndexedDb implementation computes the size
* delta for all submitted changes. This avoids having to re-read all documents from IndexedDb
* when we apply the changes.
*/
IndexedDbRemoteDocumentCache.RemoteDocumentChangeBuffer = /** @class */ (function (_super) {
tslib.__extends(RemoteDocumentChangeBuffer, _super);
/**
* @param documentCache The IndexedDbRemoteDocumentCache to apply the changes to.
* @param trackRemovals Whether to create sentinel deletes that can be tracked by
* `getNewDocumentChanges()`.
*/
function RemoteDocumentChangeBuffer(documentCache, trackRemovals) {
var _this = _super.call(this) || this;
_this.documentCache = documentCache;
_this.trackRemovals = trackRemovals;
// A map of document sizes prior to applying the changes in this buffer.
_this.documentSizes = new ObjectMap(function (key) { return key.toString(); });
return _this;
}
RemoteDocumentChangeBuffer.prototype.applyChanges = function (transaction) {
var _this = this;
var promises = [];
var sizeDelta = 0;
var collectionParents = new SortedSet(function (l, r) { return primitiveComparator(l.canonicalString(), r.canonicalString()); });
this.changes.forEach(function (key, maybeDocument) {
var previousSize = _this.documentSizes.get(key);
debugAssert(previousSize !== undefined, "Cannot modify a document that wasn't read (for " + key + ")");
if (maybeDocument) {
debugAssert(!_this.readTime.isEqual(SnapshotVersion.min()), 'Cannot add a document with a read time of zero');
var doc = _this.documentCache.serializer.toDbRemoteDocument(maybeDocument, _this.readTime);
collectionParents = collectionParents.add(key.path.popLast());
var size = dbDocumentSize(doc);
sizeDelta += size - previousSize;
promises.push(_this.documentCache.addEntry(transaction, key, doc));
}
else {
sizeDelta -= previousSize;
if (_this.trackRemovals) {
// In order to track removals, we store a "sentinel delete" in the
// RemoteDocumentCache. This entry is represented by a NoDocument
// with a version of 0 and ignored by `maybeDecodeDocument()` but
// preserved in `getNewDocumentChanges()`.
var deletedDoc = _this.documentCache.serializer.toDbRemoteDocument(new NoDocument(key, SnapshotVersion.min()), _this.readTime);
promises.push(_this.documentCache.addEntry(transaction, key, deletedDoc));
}
else {
promises.push(_this.documentCache.removeEntry(transaction, key));
}
}
});
collectionParents.forEach(function (parent) {
promises.push(_this.documentCache.indexManager.addToCollectionParentIndex(transaction, parent));
});
promises.push(this.documentCache.updateMetadata(transaction, sizeDelta));
return PersistencePromise.waitFor(promises);
};
RemoteDocumentChangeBuffer.prototype.getFromCache = function (transaction, documentKey) {
var _this = this;
// Record the size of everything we load from the cache so we can compute a delta later.
return this.documentCache
.getSizedEntry(transaction, documentKey)
.next(function (getResult) {
if (getResult === null) {
_this.documentSizes.set(documentKey, 0);
return null;
}
else {
_this.documentSizes.set(documentKey, getResult.size);
return getResult.maybeDocument;
}
});
};
RemoteDocumentChangeBuffer.prototype.getAllFromCache = function (transaction, documentKeys) {
var _this = this;
// Record the size of everything we load from the cache so we can compute
// a delta later.
return this.documentCache
.getSizedEntries(transaction, documentKeys)
.next(function (_e) {
var maybeDocuments = _e.maybeDocuments, sizeMap = _e.sizeMap;
// Note: `getAllFromCache` returns two maps instead of a single map from
// keys to `DocumentSizeEntry`s. This is to allow returning the
// `NullableMaybeDocumentMap` directly, without a conversion.
sizeMap.forEach(function (documentKey, size) {
_this.documentSizes.set(documentKey, size);
});
return maybeDocuments;
});
};
return RemoteDocumentChangeBuffer;
}(RemoteDocumentChangeBuffer));
function documentGlobalStore(txn) {
return IndexedDbPersistence.getStore(txn, DbRemoteDocumentGlobal.store);
}
/**
* Helper to get a typed SimpleDbStore for the remoteDocuments object store.
*/
function remoteDocumentsStore(txn) {
return IndexedDbPersistence.getStore(txn, DbRemoteDocument.store);
}
function dbKey(docKey) {
return docKey.path.toArray();
}
/**
* Retrusn an approximate size for the given document.
*/
function dbDocumentSize(doc) {
var value;
if (doc.document) {
value = doc.document;
}
else if (doc.unknownDocument) {
value = doc.unknownDocument;
}
else if (doc.noDocument) {
value = doc.noDocument;
}
else {
throw fail('Unknown remote document type');
}
return JSON.stringify(value).length;
}
/**
* @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.
*/
/** Offset to ensure non-overlapping target ids. */
var OFFSET = 2;
/**
* Generates monotonically increasing target IDs for sending targets to the
* watch stream.
*
* The client constructs two generators, one for the target cache, and one for
* for the sync engine (to generate limbo documents targets). These
* generators produce non-overlapping IDs (by using even and odd IDs
* respectively).
*
* By separating the target ID space, the query cache can generate target IDs
* that persist across client restarts, while sync engine can independently
* generate in-memory target IDs that are transient and can be reused after a
* restart.
*/
var TargetIdGenerator = /** @class */ (function () {
function TargetIdGenerator(lastId) {
this.lastId = lastId;
}
TargetIdGenerator.prototype.next = function () {
this.lastId += OFFSET;
return this.lastId;
};
TargetIdGenerator.forTargetCache = function () {
// The target cache generator must return '2' in its first call to `next()`
// as there is no differentiation in the protocol layer between an unset
// number and the number '0'. If we were to sent a target with target ID
// '0', the backend would consider it unset and replace it with its own ID.
return new TargetIdGenerator(2 - OFFSET);
};
TargetIdGenerator.forSyncEngine = function () {
// Sync engine assigns target IDs for limbo document detection.
return new TargetIdGenerator(1 - OFFSET);
};
return TargetIdGenerator;
}());
/**
* @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.
*/
var IndexedDbTargetCache = /** @class */ (function () {
function IndexedDbTargetCache(referenceDelegate, serializer) {
this.referenceDelegate = referenceDelegate;
this.serializer = serializer;
}
// PORTING NOTE: We don't cache global metadata for the target cache, since
// some of it (in particular `highestTargetId`) can be modified by secondary
// tabs. We could perhaps be more granular (and e.g. still cache
// `lastRemoteSnapshotVersion` in memory) but for simplicity we currently go
// to IndexedDb whenever we need to read metadata. We can revisit if it turns
// out to have a meaningful performance impact.
IndexedDbTargetCache.prototype.allocateTargetId = function (transaction) {
var _this = this;
return this.retrieveMetadata(transaction).next(function (metadata) {
var targetIdGenerator = new TargetIdGenerator(metadata.highestTargetId);
metadata.highestTargetId = targetIdGenerator.next();
return _this.saveMetadata(transaction, metadata).next(function () { return metadata.highestTargetId; });
});
};
IndexedDbTargetCache.prototype.getLastRemoteSnapshotVersion = function (transaction) {
return this.retrieveMetadata(transaction).next(function (metadata) {
return SnapshotVersion.fromTimestamp(new Timestamp(metadata.lastRemoteSnapshotVersion.seconds, metadata.lastRemoteSnapshotVersion.nanoseconds));
});
};
IndexedDbTargetCache.prototype.getHighestSequenceNumber = function (transaction) {
return this.retrieveMetadata(transaction).next(function (targetGlobal) { return targetGlobal.highestListenSequenceNumber; });
};
IndexedDbTargetCache.prototype.setTargetsMetadata = function (transaction, highestListenSequenceNumber, lastRemoteSnapshotVersion) {
var _this = this;
return this.retrieveMetadata(transaction).next(function (metadata) {
metadata.highestListenSequenceNumber = highestListenSequenceNumber;
if (lastRemoteSnapshotVersion) {
metadata.lastRemoteSnapshotVersion = lastRemoteSnapshotVersion.toTimestamp();
}
if (highestListenSequenceNumber > metadata.highestListenSequenceNumber) {
metadata.highestListenSequenceNumber = highestListenSequenceNumber;
}
return _this.saveMetadata(transaction, metadata);
});
};
IndexedDbTargetCache.prototype.addTargetData = function (transaction, targetData) {
var _this = this;
return this.saveTargetData(transaction, targetData).next(function () {
return _this.retrieveMetadata(transaction).next(function (metadata) {
metadata.targetCount += 1;
_this.updateMetadataFromTargetData(targetData, metadata);
return _this.saveMetadata(transaction, metadata);
});
});
};
IndexedDbTargetCache.prototype.updateTargetData = function (transaction, targetData) {
return this.saveTargetData(transaction, targetData);
};
IndexedDbTargetCache.prototype.removeTargetData = function (transaction, targetData) {
var _this = this;
return this.removeMatchingKeysForTargetId(transaction, targetData.targetId)
.next(function () { return targetsStore(transaction).delete(targetData.targetId); })
.next(function () { return _this.retrieveMetadata(transaction); })
.next(function (metadata) {
hardAssert(metadata.targetCount > 0, 'Removing from an empty target cache');
metadata.targetCount -= 1;
return _this.saveMetadata(transaction, metadata);
});
};
/**
* Drops any targets with sequence number less than or equal to the upper bound, excepting those
* present in `activeTargetIds`. Document associations for the removed targets are also removed.
* Returns the number of targets removed.
*/
IndexedDbTargetCache.prototype.removeTargets = function (txn, upperBound, activeTargetIds) {
var _this = this;
var count = 0;
var promises = [];
return targetsStore(txn)
.iterate(function (key, value) {
var targetData = _this.serializer.fromDbTarget(value);
if (targetData.sequenceNumber <= upperBound &&
activeTargetIds.get(targetData.targetId) === null) {
count++;
promises.push(_this.removeTargetData(txn, targetData));
}
})
.next(function () { return PersistencePromise.waitFor(promises); })
.next(function () { return count; });
};
/**
* Call provided function with each `TargetData` that we have cached.
*/
IndexedDbTargetCache.prototype.forEachTarget = function (txn, f) {
var _this = this;
return targetsStore(txn).iterate(function (key, value) {
var targetData = _this.serializer.fromDbTarget(value);
f(targetData);
});
};
IndexedDbTargetCache.prototype.retrieveMetadata = function (transaction) {
return globalTargetStore(transaction)
.get(DbTargetGlobal.key)
.next(function (metadata) {
hardAssert(metadata !== null, 'Missing metadata row.');
return metadata;
});
};
IndexedDbTargetCache.prototype.saveMetadata = function (transaction, metadata) {
return globalTargetStore(transaction).put(DbTargetGlobal.key, metadata);
};
IndexedDbTargetCache.prototype.saveTargetData = function (transaction, targetData) {
return targetsStore(transaction).put(this.serializer.toDbTarget(targetData));
};
/**
* In-place updates the provided metadata to account for values in the given
* TargetData. Saving is done separately. Returns true if there were any
* changes to the metadata.
*/
IndexedDbTargetCache.prototype.updateMetadataFromTargetData = function (targetData, metadata) {
var updated = false;
if (targetData.targetId > metadata.highestTargetId) {
metadata.highestTargetId = targetData.targetId;
updated = true;
}
if (targetData.sequenceNumber > metadata.highestListenSequenceNumber) {
metadata.highestListenSequenceNumber = targetData.sequenceNumber;
updated = true;
}
return updated;
};
IndexedDbTargetCache.prototype.getTargetCount = function (transaction) {
return this.retrieveMetadata(transaction).next(function (metadata) { return metadata.targetCount; });
};
IndexedDbTargetCache.prototype.getTargetData = function (transaction, target) {
var _this = this;
// Iterating by the canonicalId may yield more than one result because
// canonicalId values are not required to be unique per target. This query
// depends on the queryTargets index to be efficient.
var canonicalId = target.canonicalId();
var range = IDBKeyRange.bound([canonicalId, Number.NEGATIVE_INFINITY], [canonicalId, Number.POSITIVE_INFINITY]);
var result = null;
return targetsStore(transaction)
.iterate({ range: range, index: DbTarget.queryTargetsIndexName }, function (key, value, control) {
var found = _this.serializer.fromDbTarget(value);
// After finding a potential match, check that the target is
// actually equal to the requested target.
if (target.isEqual(found.target)) {
result = found;
control.done();
}
})
.next(function () { return result; });
};
IndexedDbTargetCache.prototype.addMatchingKeys = function (txn, keys, targetId) {
var _this = this;
// PORTING NOTE: The reverse index (documentsTargets) is maintained by
// IndexedDb.
var promises = [];
var store = documentTargetStore(txn);
keys.forEach(function (key) {
var path = encodeResourcePath(key.path);
promises.push(store.put(new DbTargetDocument(targetId, path)));
promises.push(_this.referenceDelegate.addReference(txn, targetId, key));
});
return PersistencePromise.waitFor(promises);
};
IndexedDbTargetCache.prototype.removeMatchingKeys = function (txn, keys, targetId) {
var _this = this;
// PORTING NOTE: The reverse index (documentsTargets) is maintained by
// IndexedDb.
var store = documentTargetStore(txn);
return PersistencePromise.forEach(keys, function (key) {
var path = encodeResourcePath(key.path);
return PersistencePromise.waitFor([
store.delete([targetId, path]),
_this.referenceDelegate.removeReference(txn, targetId, key)
]);
});
};
IndexedDbTargetCache.prototype.removeMatchingKeysForTargetId = function (txn, targetId) {
var store = documentTargetStore(txn);
var range = IDBKeyRange.bound([targetId], [targetId + 1],
/*lowerOpen=*/ false,
/*upperOpen=*/ true);
return store.delete(range);
};
IndexedDbTargetCache.prototype.getMatchingKeysForTargetId = function (txn, targetId) {
var range = IDBKeyRange.bound([targetId], [targetId + 1],
/*lowerOpen=*/ false,
/*upperOpen=*/ true);
var store = documentTargetStore(txn);
var result = documentKeySet();
return store
.iterate({ range: range, keysOnly: true }, function (key, _, control) {
var path = decodeResourcePath(key[1]);
var docKey = new DocumentKey(path);
result = result.add(docKey);
})
.next(function () { return result; });
};
IndexedDbTargetCache.prototype.containsKey = function (txn, key) {
var path = encodeResourcePath(key.path);
var range = IDBKeyRange.bound([path], [immediateSuccessor(path)],
/*lowerOpen=*/ false,
/*upperOpen=*/ true);
var count = 0;
return documentTargetStore(txn)
.iterate({
index: DbTargetDocument.documentTargetsIndex,
keysOnly: true,
range: range
}, function (_e, _, control) {
var targetId = _e[0], path = _e[1];
// Having a sentinel row for a document does not count as containing that document;
// For the target cache, containing the document means the document is part of some
// target.
if (targetId !== 0) {
count++;
control.done();
}
})
.next(function () { return count > 0; });
};
/**
* Looks up a TargetData entry by target ID.
*
* @param targetId The target ID of the TargetData entry to look up.
* @return The cached TargetData entry, or null if the cache has no entry for
* the target.
*/
// PORTING NOTE: Multi-tab only.
IndexedDbTargetCache.prototype.getTargetDataForTarget = function (transaction, targetId) {
var _this = this;
return targetsStore(transaction)
.get(targetId)
.next(function (found) {
if (found) {
return _this.serializer.fromDbTarget(found);
}
else {
return null;
}
});
};
return IndexedDbTargetCache;
}());
/**
* Helper to get a typed SimpleDbStore for the queries object store.
*/
function targetsStore(txn) {
return IndexedDbPersistence.getStore(txn, DbTarget.store);
}
/**
* Helper to get a typed SimpleDbStore for the target globals object store.
*/
function globalTargetStore(txn) {
return IndexedDbPersistence.getStore(txn, DbTargetGlobal.store);
}
/**
* Helper to get a typed SimpleDbStore for the document target object store.
*/
function documentTargetStore(txn) {
return IndexedDbPersistence.getStore(txn, DbTargetDocument.store);
}
/**
* @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.
*/
/** Serializer for values stored in the LocalStore. */
var LocalSerializer = /** @class */ (function () {
function LocalSerializer(remoteSerializer) {
this.remoteSerializer = remoteSerializer;
}
/** Decodes a remote document from storage locally to a Document. */
LocalSerializer.prototype.fromDbRemoteDocument = function (remoteDoc) {
if (remoteDoc.document) {
return this.remoteSerializer.fromDocument(remoteDoc.document, !!remoteDoc.hasCommittedMutations);
}
else if (remoteDoc.noDocument) {
var key = DocumentKey.fromSegments(remoteDoc.noDocument.path);
var version_1 = this.fromDbTimestamp(remoteDoc.noDocument.readTime);
return new NoDocument(key, version_1, {
hasCommittedMutations: !!remoteDoc.hasCommittedMutations
});
}
else if (remoteDoc.unknownDocument) {
var key = DocumentKey.fromSegments(remoteDoc.unknownDocument.path);
var version_2 = this.fromDbTimestamp(remoteDoc.unknownDocument.version);
return new UnknownDocument(key, version_2);
}
else {
return fail('Unexpected DbRemoteDocument');
}
};
/** Encodes a document for storage locally. */
LocalSerializer.prototype.toDbRemoteDocument = function (maybeDoc, readTime) {
var dbReadTime = this.toDbTimestampKey(readTime);
var parentPath = maybeDoc.key.path.popLast().toArray();
if (maybeDoc instanceof Document) {
var doc = this.remoteSerializer.toDocument(maybeDoc);
var hasCommittedMutations = maybeDoc.hasCommittedMutations;
return new DbRemoteDocument(
/* unknownDocument= */ null,
/* noDocument= */ null, doc, hasCommittedMutations, dbReadTime, parentPath);
}
else if (maybeDoc instanceof NoDocument) {
var path = maybeDoc.key.path.toArray();
var readTime_1 = this.toDbTimestamp(maybeDoc.version);
var hasCommittedMutations = maybeDoc.hasCommittedMutations;
return new DbRemoteDocument(
/* unknownDocument= */ null, new DbNoDocument(path, readTime_1),
/* document= */ null, hasCommittedMutations, dbReadTime, parentPath);
}
else if (maybeDoc instanceof UnknownDocument) {
var path = maybeDoc.key.path.toArray();
var readTime_2 = this.toDbTimestamp(maybeDoc.version);
return new DbRemoteDocument(new DbUnknownDocument(path, readTime_2),
/* noDocument= */ null,
/* document= */ null,
/* hasCommittedMutations= */ true, dbReadTime, parentPath);
}
else {
return fail('Unexpected MaybeDocument');
}
};
LocalSerializer.prototype.toDbTimestampKey = function (snapshotVersion) {
var timestamp = snapshotVersion.toTimestamp();
return [timestamp.seconds, timestamp.nanoseconds];
};
LocalSerializer.prototype.fromDbTimestampKey = function (dbTimestampKey) {
var timestamp = new Timestamp(dbTimestampKey[0], dbTimestampKey[1]);
return SnapshotVersion.fromTimestamp(timestamp);
};
LocalSerializer.prototype.toDbTimestamp = function (snapshotVersion) {
var timestamp = snapshotVersion.toTimestamp();
return new DbTimestamp(timestamp.seconds, timestamp.nanoseconds);
};
LocalSerializer.prototype.fromDbTimestamp = function (dbTimestamp) {
var timestamp = new Timestamp(dbTimestamp.seconds, dbTimestamp.nanoseconds);
return SnapshotVersion.fromTimestamp(timestamp);
};
/** Encodes a batch of mutations into a DbMutationBatch for local storage. */
LocalSerializer.prototype.toDbMutationBatch = function (userId, batch) {
var _this = this;
var serializedBaseMutations = batch.baseMutations.map(function (m) { return _this.remoteSerializer.toMutation(m); });
var serializedMutations = batch.mutations.map(function (m) { return _this.remoteSerializer.toMutation(m); });
return new DbMutationBatch(userId, batch.batchId, batch.localWriteTime.toMillis(), serializedBaseMutations, serializedMutations);
};
/** Decodes a DbMutationBatch into a MutationBatch */
LocalSerializer.prototype.fromDbMutationBatch = function (dbBatch) {
var _this = this;
var baseMutations = (dbBatch.baseMutations || []).map(function (m) { return _this.remoteSerializer.fromMutation(m); });
var mutations = dbBatch.mutations.map(function (m) { return _this.remoteSerializer.fromMutation(m); });
var timestamp = Timestamp.fromMillis(dbBatch.localWriteTimeMs);
return new MutationBatch(dbBatch.batchId, timestamp, baseMutations, mutations);
};
/** Decodes a DbTarget into TargetData */
LocalSerializer.prototype.fromDbTarget = function (dbTarget) {
var version = this.fromDbTimestamp(dbTarget.readTime);
var lastLimboFreeSnapshotVersion = dbTarget.lastLimboFreeSnapshotVersion !== undefined
? this.fromDbTimestamp(dbTarget.lastLimboFreeSnapshotVersion)
: SnapshotVersion.min();
var target;
if (isDocumentQuery(dbTarget.query)) {
target = this.remoteSerializer.fromDocumentsTarget(dbTarget.query);
}
else {
target = this.remoteSerializer.fromQueryTarget(dbTarget.query);
}
return new TargetData(target, dbTarget.targetId, 0 /* Listen */, dbTarget.lastListenSequenceNumber, version, lastLimboFreeSnapshotVersion, ByteString.fromBase64String(dbTarget.resumeToken));
};
/** Encodes TargetData into a DbTarget for storage locally. */
LocalSerializer.prototype.toDbTarget = function (targetData) {
debugAssert(0 /* Listen */ === targetData.purpose, 'Only queries with purpose ' +
0 /* Listen */ +
' may be stored, got ' +
targetData.purpose);
var dbTimestamp = this.toDbTimestamp(targetData.snapshotVersion);
var dbLastLimboFreeTimestamp = this.toDbTimestamp(targetData.lastLimboFreeSnapshotVersion);
var queryProto;
if (targetData.target.isDocumentQuery()) {
queryProto = this.remoteSerializer.toDocumentsTarget(targetData.target);
}
else {
queryProto = this.remoteSerializer.toQueryTarget(targetData.target);
}
// We can't store the resumeToken as a ByteString in IndexedDb, so we
// convert it to a base64 string for storage.
var resumeToken = targetData.resumeToken.toBase64();
// lastListenSequenceNumber is always 0 until we do real GC.
return new DbTarget(targetData.targetId, targetData.target.canonicalId(), dbTimestamp, resumeToken, targetData.sequenceNumber, dbLastLimboFreeTimestamp, queryProto);
};
return LocalSerializer;
}());
/**
* A helper function for figuring out what kind of query has been stored.
*/
function isDocumentQuery(dbQuery) {
return dbQuery.documents !== undefined;
}
/**
* @license
* Copyright 2018 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.
*/
var LOG_TAG$1 = 'LruGarbageCollector';
function bufferEntryComparator(_e, _f) {
var aSequence = _e[0], aIndex = _e[1];
var bSequence = _f[0], bIndex = _f[1];
var seqCmp = primitiveComparator(aSequence, bSequence);
if (seqCmp === 0) {
// This order doesn't matter, but we can bias against churn by sorting
// entries created earlier as less than newer entries.
return primitiveComparator(aIndex, bIndex);
}
else {
return seqCmp;
}
}
/**
* Used to calculate the nth sequence number. Keeps a rolling buffer of the
* lowest n values passed to `addElement`, and finally reports the largest of
* them in `maxValue`.
*/
var RollingSequenceNumberBuffer = /** @class */ (function () {
function RollingSequenceNumberBuffer(maxElements) {
this.maxElements = maxElements;
this.buffer = new SortedSet(bufferEntryComparator);
this.previousIndex = 0;
}
RollingSequenceNumberBuffer.prototype.nextIndex = function () {
return ++this.previousIndex;
};
RollingSequenceNumberBuffer.prototype.addElement = function (sequenceNumber) {
var entry = [sequenceNumber, this.nextIndex()];
if (this.buffer.size < this.maxElements) {
this.buffer = this.buffer.add(entry);
}
else {
var highestValue = this.buffer.last();
if (bufferEntryComparator(entry, highestValue) < 0) {
this.buffer = this.buffer.delete(highestValue).add(entry);
}
}
};
Object.defineProperty(RollingSequenceNumberBuffer.prototype, "maxValue", {
get: function () {
// Guaranteed to be non-empty. If we decide we are not collecting any
// sequence numbers, nthSequenceNumber below short-circuits. If we have
// decided that we are collecting n sequence numbers, it's because n is some
// percentage of the existing sequence numbers. That means we should never
// be in a situation where we are collecting sequence numbers but don't
// actually have any.
return this.buffer.last()[0];
},
enumerable: true,
configurable: true
});
return RollingSequenceNumberBuffer;
}());
var GC_DID_NOT_RUN = {
didRun: false,
sequenceNumbersCollected: 0,
targetsRemoved: 0,
documentsRemoved: 0
};
var LruParams = /** @class */ (function () {
function LruParams(
// When we attempt to collect, we will only do so if the cache size is greater than this
// threshold. Passing `COLLECTION_DISABLED` here will cause collection to always be skipped.
cacheSizeCollectionThreshold,
// The percentage of sequence numbers that we will attempt to collect
percentileToCollect,
// A cap on the total number of sequence numbers that will be collected. This prevents
// us from collecting a huge number of sequence numbers if the cache has grown very large.
maximumSequenceNumbersToCollect) {
this.cacheSizeCollectionThreshold = cacheSizeCollectionThreshold;
this.percentileToCollect = percentileToCollect;
this.maximumSequenceNumbersToCollect = maximumSequenceNumbersToCollect;
}
LruParams.withCacheSize = function (cacheSize) {
return new LruParams(cacheSize, LruParams.DEFAULT_COLLECTION_PERCENTILE, LruParams.DEFAULT_MAX_SEQUENCE_NUMBERS_TO_COLLECT);
};
return LruParams;
}());
LruParams.COLLECTION_DISABLED = -1;
LruParams.MINIMUM_CACHE_SIZE_BYTES = 1 * 1024 * 1024;
LruParams.DEFAULT_CACHE_SIZE_BYTES = 40 * 1024 * 1024;
LruParams.DEFAULT_COLLECTION_PERCENTILE = 10;
LruParams.DEFAULT_MAX_SEQUENCE_NUMBERS_TO_COLLECT = 1000;
LruParams.DEFAULT = new LruParams(LruParams.DEFAULT_CACHE_SIZE_BYTES, LruParams.DEFAULT_COLLECTION_PERCENTILE, LruParams.DEFAULT_MAX_SEQUENCE_NUMBERS_TO_COLLECT);
LruParams.DISABLED = new LruParams(LruParams.COLLECTION_DISABLED, 0, 0);
/** How long we wait to try running LRU GC after SDK initialization. */
var INITIAL_GC_DELAY_MS = 1 * 60 * 1000;
/** Minimum amount of time between GC checks, after the first one. */
var REGULAR_GC_DELAY_MS = 5 * 60 * 1000;
/**
* This class is responsible for the scheduling of LRU garbage collection. It handles checking
* whether or not GC is enabled, as well as which delay to use before the next run.
*/
var LruScheduler = /** @class */ (function () {
function LruScheduler(garbageCollector, asyncQueue) {
this.garbageCollector = garbageCollector;
this.asyncQueue = asyncQueue;
this.hasRun = false;
this.gcTask = null;
}
LruScheduler.prototype.start = function (localStore) {
debugAssert(this.gcTask === null, 'Cannot start an already started LruScheduler');
if (this.garbageCollector.params.cacheSizeCollectionThreshold !==
LruParams.COLLECTION_DISABLED) {
this.scheduleGC(localStore);
}
};
LruScheduler.prototype.stop = function () {
if (this.gcTask) {
this.gcTask.cancel();
this.gcTask = null;
}
};
Object.defineProperty(LruScheduler.prototype, "started", {
get: function () {
return this.gcTask !== null;
},
enumerable: true,
configurable: true
});
LruScheduler.prototype.scheduleGC = function (localStore) {
var _this = this;
debugAssert(this.gcTask === null, 'Cannot schedule GC while a task is pending');
var delay = this.hasRun ? REGULAR_GC_DELAY_MS : INITIAL_GC_DELAY_MS;
logDebug('LruGarbageCollector', "Garbage collection scheduled in " + delay + "ms");
this.gcTask = this.asyncQueue.enqueueAfterDelay("lru_garbage_collection" /* LruGarbageCollection */, delay, function () { return tslib.__awaiter(_this, void 0, void 0, function () {
var e_1;
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
this.gcTask = null;
this.hasRun = true;
_e.label = 1;
case 1:
_e.trys.push([1, 3, , 7]);
return [4 /*yield*/, localStore.collectGarbage(this.garbageCollector)];
case 2:
_e.sent();
return [3 /*break*/, 7];
case 3:
e_1 = _e.sent();
if (!isIndexedDbTransactionError(e_1)) return [3 /*break*/, 4];
logDebug(LOG_TAG$1, 'Ignoring IndexedDB error during garbage collection: ', e_1);
return [3 /*break*/, 6];
case 4: return [4 /*yield*/, ignoreIfPrimaryLeaseLoss(e_1)];
case 5:
_e.sent();
_e.label = 6;
case 6: return [3 /*break*/, 7];
case 7: return [4 /*yield*/, this.scheduleGC(localStore)];
case 8:
_e.sent();
return [2 /*return*/];
}
});
}); });
};
return LruScheduler;
}());
/** Implements the steps for LRU garbage collection. */
var LruGarbageCollector = /** @class */ (function () {
function LruGarbageCollector(delegate, params) {
this.delegate = delegate;
this.params = params;
}
/** Given a percentile of target to collect, returns the number of targets to collect. */
LruGarbageCollector.prototype.calculateTargetCount = function (txn, percentile) {
return this.delegate.getSequenceNumberCount(txn).next(function (targetCount) {
return Math.floor((percentile / 100.0) * targetCount);
});
};
/** Returns the nth sequence number, counting in order from the smallest. */
LruGarbageCollector.prototype.nthSequenceNumber = function (txn, n) {
var _this = this;
if (n === 0) {
return PersistencePromise.resolve(ListenSequence.INVALID);
}
var buffer = new RollingSequenceNumberBuffer(n);
return this.delegate
.forEachTarget(txn, function (target) { return buffer.addElement(target.sequenceNumber); })
.next(function () {
return _this.delegate.forEachOrphanedDocumentSequenceNumber(txn, function (sequenceNumber) { return buffer.addElement(sequenceNumber); });
})
.next(function () { return buffer.maxValue; });
};
/**
* Removes targets with a sequence number equal to or less than the given upper bound, and removes
* document associations with those targets.
*/
LruGarbageCollector.prototype.removeTargets = function (txn, upperBound, activeTargetIds) {
return this.delegate.removeTargets(txn, upperBound, activeTargetIds);
};
/**
* Removes documents that have a sequence number equal to or less than the upper bound and are not
* otherwise pinned.
*/
LruGarbageCollector.prototype.removeOrphanedDocuments = function (txn, upperBound) {
return this.delegate.removeOrphanedDocuments(txn, upperBound);
};
LruGarbageCollector.prototype.collect = function (txn, activeTargetIds) {
var _this = this;
if (this.params.cacheSizeCollectionThreshold === LruParams.COLLECTION_DISABLED) {
logDebug('LruGarbageCollector', 'Garbage collection skipped; disabled');
return PersistencePromise.resolve(GC_DID_NOT_RUN);
}
return this.getCacheSize(txn).next(function (cacheSize) {
if (cacheSize < _this.params.cacheSizeCollectionThreshold) {
logDebug('LruGarbageCollector', "Garbage collection skipped; Cache size " + cacheSize + " " +
("is lower than threshold " + _this.params.cacheSizeCollectionThreshold));
return GC_DID_NOT_RUN;
}
else {
return _this.runGarbageCollection(txn, activeTargetIds);
}
});
};
LruGarbageCollector.prototype.getCacheSize = function (txn) {
return this.delegate.getCacheSize(txn);
};
LruGarbageCollector.prototype.runGarbageCollection = function (txn, activeTargetIds) {
var _this = this;
var upperBoundSequenceNumber;
var sequenceNumbersToCollect, targetsRemoved;
// Timestamps for various pieces of the process
var countedTargetsTs, foundUpperBoundTs, removedTargetsTs, removedDocumentsTs;
var startTs = Date.now();
return this.calculateTargetCount(txn, this.params.percentileToCollect)
.next(function (sequenceNumbers) {
// Cap at the configured max
if (sequenceNumbers > _this.params.maximumSequenceNumbersToCollect) {
logDebug('LruGarbageCollector', 'Capping sequence numbers to collect down ' +
("to the maximum of " + _this.params.maximumSequenceNumbersToCollect + " ") +
("from " + sequenceNumbers));
sequenceNumbersToCollect = _this.params
.maximumSequenceNumbersToCollect;
}
else {
sequenceNumbersToCollect = sequenceNumbers;
}
countedTargetsTs = Date.now();
return _this.nthSequenceNumber(txn, sequenceNumbersToCollect);
})
.next(function (upperBound) {
upperBoundSequenceNumber = upperBound;
foundUpperBoundTs = Date.now();
return _this.removeTargets(txn, upperBoundSequenceNumber, activeTargetIds);
})
.next(function (numTargetsRemoved) {
targetsRemoved = numTargetsRemoved;
removedTargetsTs = Date.now();
return _this.removeOrphanedDocuments(txn, upperBoundSequenceNumber);
})
.next(function (documentsRemoved) {
removedDocumentsTs = Date.now();
if (getLogLevel() <= logger.LogLevel.DEBUG) {
var desc = 'LRU Garbage Collection\n' +
("\tCounted targets in " + (countedTargetsTs - startTs) + "ms\n") +
("\tDetermined least recently used " + sequenceNumbersToCollect + " in ") +
(foundUpperBoundTs - countedTargetsTs + "ms\n") +
("\tRemoved " + targetsRemoved + " targets in ") +
(removedTargetsTs - foundUpperBoundTs + "ms\n") +
("\tRemoved " + documentsRemoved + " documents in ") +
(removedDocumentsTs - removedTargetsTs + "ms\n") +
("Total Duration: " + (removedDocumentsTs - startTs) + "ms");
logDebug('LruGarbageCollector', desc);
}
return PersistencePromise.resolve({
didRun: true,
sequenceNumbersCollected: sequenceNumbersToCollect,
targetsRemoved: targetsRemoved,
documentsRemoved: documentsRemoved
});
});
};
return LruGarbageCollector;
}());
/**
* @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.
*/
var LOG_TAG$2 = 'IndexedDbPersistence';
/**
* Oldest acceptable age in milliseconds for client metadata before the client
* is considered inactive and its associated data is garbage collected.
*/
var MAX_CLIENT_AGE_MS = 30 * 60 * 1000; // 30 minutes
/**
* Oldest acceptable metadata age for clients that may participate in the
* primary lease election. Clients that have not updated their client metadata
* within 5 seconds are not eligible to receive a primary lease.
*/
var MAX_PRIMARY_ELIGIBLE_AGE_MS = 5000;
/**
* The interval at which clients will update their metadata, including
* refreshing their primary lease if held or potentially trying to acquire it if
* not held.
*
* Primary clients may opportunistically refresh their metadata earlier
* if they're already performing an IndexedDB operation.
*/
var CLIENT_METADATA_REFRESH_INTERVAL_MS = 4000;
/** User-facing error when the primary lease is required but not available. */
var PRIMARY_LEASE_EXCLUSIVE_ERROR_MSG = 'Failed to obtain exclusive access to the persistence layer. ' +
'To allow shared access, make sure to invoke ' +
'`enablePersistence()` with `synchronizeTabs:true` in all tabs.';
var UNSUPPORTED_PLATFORM_ERROR_MSG = 'This platform is either missing' +
' IndexedDB or is known to have an incomplete implementation. Offline' +
' persistence has been disabled.';
// The format of the LocalStorage key that stores zombied client is:
// firestore_zombie_<persistence_prefix>_<instance_key>
var ZOMBIED_CLIENTS_KEY_PREFIX = 'firestore_zombie';
var IndexedDbTransaction = /** @class */ (function (_super) {
tslib.__extends(IndexedDbTransaction, _super);
function IndexedDbTransaction(simpleDbTransaction, currentSequenceNumber) {
var _this = _super.call(this) || this;
_this.simpleDbTransaction = simpleDbTransaction;
_this.currentSequenceNumber = currentSequenceNumber;
return _this;
}
return IndexedDbTransaction;
}(PersistenceTransaction));
/**
* An IndexedDB-backed instance of Persistence. Data is stored persistently
* across sessions.
*
* On Web only, the Firestore SDKs support shared access to its persistence
* layer. This allows multiple browser tabs to read and write to IndexedDb and
* to synchronize state even without network connectivity. Shared access is
* currently optional and not enabled unless all clients invoke
* `enablePersistence()` with `{synchronizeTabs:true}`.
*
* In multi-tab mode, if multiple clients are active at the same time, the SDK
* will designate one client as the primary client. An effort is made to pick
* a visible, network-connected and active client, and this client is
* responsible for letting other clients know about its presence. The primary
* client writes a unique client-generated identifier (the client ID) to
* IndexedDbs owner store every 4 seconds. If the primary client fails to
* update this entry, another client can acquire the lease and take over as
* primary.
*
* Some persistence operations in the SDK are designated as primary-client only
* operations. This includes the acknowledgment of mutations and all updates of
* remote documents. The effects of these operations are written to persistence
* and then broadcast to other tabs via LocalStorage (see
* `WebStorageSharedClientState`), which then refresh their state from
* persistence.
*
* Similarly, the primary client listens to notifications sent by secondary
* clients to discover persistence changes written by secondary clients, such as
* the addition of new mutations and query targets.
*
* If multi-tab is not enabled and another tab already obtained the primary
* lease, IndexedDbPersistence enters a failed state and all subsequent
* operations will automatically fail.
*
* Additionally, there is an optimization so that when a tab is closed, the
* primary lease is released immediately (this is especially important to make
* sure that a refreshed tab is able to immediately re-acquire the primary
* lease). Unfortunately, IndexedDB cannot be reliably used in window.unload
* since it is an asynchronous API. So in addition to attempting to give up the
* lease, the leaseholder writes its client ID to a "zombiedClient" entry in
* LocalStorage which acts as an indicator that another tab should go ahead and
* take the primary lease immediately regardless of the current lease timestamp.
*
* TODO(b/114226234): Remove `synchronizeTabs` section when multi-tab is no
* longer optional.
*/
var IndexedDbPersistence = /** @class */ (function () {
function IndexedDbPersistence(allowTabSynchronization, persistenceKey, clientId, platform, lruParams, queue, serializer, sequenceNumberSyncer) {
this.allowTabSynchronization = allowTabSynchronization;
this.persistenceKey = persistenceKey;
this.clientId = clientId;
this.queue = queue;
this.sequenceNumberSyncer = sequenceNumberSyncer;
this.listenSequence = null;
this._started = false;
this.isPrimary = false;
this.networkEnabled = true;
/** Our window.unload handler, if registered. */
this.windowUnloadHandler = null;
this.inForeground = false;
/** Our 'visibilitychange' listener if registered. */
this.documentVisibilityHandler = null;
/** The client metadata refresh task. */
this.clientMetadataRefresher = null;
/** The last time we garbage collected the client metadata object store. */
this.lastGarbageCollectionTime = Number.NEGATIVE_INFINITY;
/** A listener to notify on primary state changes. */
this.primaryStateListener = function (_) { return Promise.resolve(); };
if (!IndexedDbPersistence.isAvailable()) {
throw new FirestoreError(Code.UNIMPLEMENTED, UNSUPPORTED_PLATFORM_ERROR_MSG);
}
this.referenceDelegate = new IndexedDbLruDelegate(this, lruParams);
this.dbName = persistenceKey + IndexedDbPersistence.MAIN_DATABASE;
this.serializer = new LocalSerializer(serializer);
this.document = platform.document;
this.targetCache = new IndexedDbTargetCache(this.referenceDelegate, this.serializer);
this.indexManager = new IndexedDbIndexManager();
this.remoteDocumentCache = new IndexedDbRemoteDocumentCache(this.serializer, this.indexManager);
if (platform.window && platform.window.localStorage) {
this.window = platform.window;
this.webStorage = this.window.localStorage;
}
else {
throw new FirestoreError(Code.UNIMPLEMENTED, 'IndexedDB persistence is only available on platforms that support LocalStorage.');
}
}
IndexedDbPersistence.getStore = function (txn, store) {
if (txn instanceof IndexedDbTransaction) {
return SimpleDb.getStore(txn.simpleDbTransaction, store);
}
else {
throw fail('IndexedDbPersistence must use instances of IndexedDbTransaction');
}
};
/**
* Attempt to start IndexedDb persistence.
*
* @return {Promise<void>} Whether persistence was enabled.
*/
IndexedDbPersistence.prototype.start = function () {
var _this = this;
debugAssert(!this.started, 'IndexedDbPersistence double-started!');
debugAssert(this.window !== null, "Expected 'window' to be defined");
return SimpleDb.openOrCreate(this.dbName, SCHEMA_VERSION, new SchemaConverter(this.serializer))
.then(function (db) {
_this.simpleDb = db;
// NOTE: This is expected to fail sometimes (in the case of another tab already
// having the persistence lock), so it's the first thing we should do.
return _this.updateClientMetadataAndTryBecomePrimary();
})
.then(function () {
if (!_this.isPrimary && !_this.allowTabSynchronization) {
// Fail `start()` if `synchronizeTabs` is disabled and we cannot
// obtain the primary lease.
throw new FirestoreError(Code.FAILED_PRECONDITION, PRIMARY_LEASE_EXCLUSIVE_ERROR_MSG);
}
_this.attachVisibilityHandler();
_this.attachWindowUnloadHook();
_this.scheduleClientMetadataAndPrimaryLeaseRefreshes();
return _this.runTransaction('getHighestListenSequenceNumber', 'readonly', function (txn) { return _this.targetCache.getHighestSequenceNumber(txn); });
})
.then(function (highestListenSequenceNumber) {
_this.listenSequence = new ListenSequence(highestListenSequenceNumber, _this.sequenceNumberSyncer);
})
.then(function () {
_this._started = true;
})
.catch(function (reason) {
_this.simpleDb && _this.simpleDb.close();
return Promise.reject(reason);
});
};
/**
* Registers a listener that gets called when the primary state of the
* instance changes. Upon registering, this listener is invoked immediately
* with the current primary state.
*
* PORTING NOTE: This is only used for Web multi-tab.
*/
IndexedDbPersistence.prototype.setPrimaryStateListener = function (primaryStateListener) {
var _this = this;
this.primaryStateListener = function (primaryState) { return tslib.__awaiter(_this, void 0, void 0, function () {
return tslib.__generator(this, function (_e) {
if (this.started) {
return [2 /*return*/, primaryStateListener(primaryState)];
}
return [2 /*return*/];
});
}); };
return primaryStateListener(this.isPrimary);
};
/**
* Registers a listener that gets called when the database receives a
* version change event indicating that it has deleted.
*
* PORTING NOTE: This is only used for Web multi-tab.
*/
IndexedDbPersistence.prototype.setDatabaseDeletedListener = function (databaseDeletedListener) {
var _this = this;
this.simpleDb.setVersionChangeListener(function (event) { return tslib.__awaiter(_this, void 0, void 0, function () {
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
if (!(event.newVersion === null)) return [3 /*break*/, 2];
return [4 /*yield*/, databaseDeletedListener()];
case 1:
_e.sent();
_e.label = 2;
case 2: return [2 /*return*/];
}
});
}); });
};
/**
* Adjusts the current network state in the client's metadata, potentially
* affecting the primary lease.
*
* PORTING NOTE: This is only used for Web multi-tab.
*/
IndexedDbPersistence.prototype.setNetworkEnabled = function (networkEnabled) {
var _this = this;
if (this.networkEnabled !== networkEnabled) {
this.networkEnabled = networkEnabled;
// Schedule a primary lease refresh for immediate execution. The eventual
// lease update will be propagated via `primaryStateListener`.
this.queue.enqueueAndForget(function () { return tslib.__awaiter(_this, void 0, void 0, function () {
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
if (!this.started) return [3 /*break*/, 2];
return [4 /*yield*/, this.updateClientMetadataAndTryBecomePrimary()];
case 1:
_e.sent();
_e.label = 2;
case 2: return [2 /*return*/];
}
});
}); });
}
};
/**
* Updates the client metadata in IndexedDb and attempts to either obtain or
* extend the primary lease for the local client. Asynchronously notifies the
* primary state listener if the client either newly obtained or released its
* primary lease.
*/
IndexedDbPersistence.prototype.updateClientMetadataAndTryBecomePrimary = function () {
var _this = this;
return this.runTransaction('updateClientMetadataAndTryBecomePrimary', 'readwrite', function (txn) {
var metadataStore = clientMetadataStore(txn);
return metadataStore
.put(new DbClientMetadata(_this.clientId, Date.now(), _this.networkEnabled, _this.inForeground))
.next(function () {
if (_this.isPrimary) {
return _this.verifyPrimaryLease(txn).next(function (success) {
if (!success) {
_this.isPrimary = false;
_this.queue.enqueueAndForget(function () { return _this.primaryStateListener(false); });
}
});
}
})
.next(function () { return _this.canActAsPrimary(txn); })
.next(function (canActAsPrimary) {
if (_this.isPrimary && !canActAsPrimary) {
return _this.releasePrimaryLeaseIfHeld(txn).next(function () { return false; });
}
else if (canActAsPrimary) {
return _this.acquireOrExtendPrimaryLease(txn).next(function () { return true; });
}
else {
return /* canActAsPrimary= */ false;
}
});
})
.catch(function (e) {
if (!_this.allowTabSynchronization) {
if (isIndexedDbTransactionError(e)) {
logDebug(LOG_TAG$2, 'Failed to extend owner lease: ', e);
// Proceed with the existing state. Any subsequent access to
// IndexedDB will verify the lease.
return _this.isPrimary;
}
else {
throw e;
}
}
logDebug(LOG_TAG$2, 'Releasing owner lease after error during lease refresh', e);
return /* isPrimary= */ false;
})
.then(function (isPrimary) {
if (_this.isPrimary !== isPrimary) {
_this.queue.enqueueAndForget(function () { return _this.primaryStateListener(isPrimary); });
}
_this.isPrimary = isPrimary;
});
};
IndexedDbPersistence.prototype.verifyPrimaryLease = function (txn) {
var _this = this;
var store = primaryClientStore(txn);
return store.get(DbPrimaryClient.key).next(function (primaryClient) {
return PersistencePromise.resolve(_this.isLocalClient(primaryClient));
});
};
IndexedDbPersistence.prototype.removeClientMetadata = function (txn) {
var metadataStore = clientMetadataStore(txn);
return metadataStore.delete(this.clientId);
};
/**
* If the garbage collection threshold has passed, prunes the
* RemoteDocumentChanges and the ClientMetadata store based on the last update
* time of all clients.
*/
IndexedDbPersistence.prototype.maybeGarbageCollectMultiClientState = function () {
return tslib.__awaiter(this, void 0, void 0, function () {
var inactiveClients;
var _this = this;
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
if (!(this.isPrimary &&
!this.isWithinAge(this.lastGarbageCollectionTime, MAX_CLIENT_AGE_MS))) return [3 /*break*/, 2];
this.lastGarbageCollectionTime = Date.now();
return [4 /*yield*/, this.runTransaction('maybeGarbageCollectMultiClientState', 'readwrite-primary', function (txn) {
var metadataStore = IndexedDbPersistence.getStore(txn, DbClientMetadata.store);
return metadataStore.loadAll().next(function (existingClients) {
var active = _this.filterActiveClients(existingClients, MAX_CLIENT_AGE_MS);
var inactive = existingClients.filter(function (client) { return active.indexOf(client) === -1; });
// Delete metadata for clients that are no longer considered active.
return PersistencePromise.forEach(inactive, function (inactiveClient) { return metadataStore.delete(inactiveClient.clientId); }).next(function () { return inactive; });
});
}).catch(function () {
// Ignore primary lease violations or any other type of error. The next
// primary will run `maybeGarbageCollectMultiClientState()` again.
// We don't use `ignoreIfPrimaryLeaseLoss()` since we don't want to depend
// on LocalStore.
return [];
})];
case 1:
inactiveClients = _e.sent();
// Delete potential leftover entries that may continue to mark the
// inactive clients as zombied in LocalStorage.
// Ideally we'd delete the IndexedDb and LocalStorage zombie entries for
// the client atomically, but we can't. So we opt to delete the IndexedDb
// entries first to avoid potentially reviving a zombied client.
inactiveClients.forEach(function (inactiveClient) {
_this.window.localStorage.removeItem(_this.zombiedClientLocalStorageKey(inactiveClient.clientId));
});
_e.label = 2;
case 2: return [2 /*return*/];
}
});
});
};
/**
* Schedules a recurring timer to update the client metadata and to either
* extend or acquire the primary lease if the client is eligible.
*/
IndexedDbPersistence.prototype.scheduleClientMetadataAndPrimaryLeaseRefreshes = function () {
var _this = this;
this.clientMetadataRefresher = this.queue.enqueueAfterDelay("client_metadata_refresh" /* ClientMetadataRefresh */, CLIENT_METADATA_REFRESH_INTERVAL_MS, function () {
return _this.updateClientMetadataAndTryBecomePrimary()
.then(function () { return _this.maybeGarbageCollectMultiClientState(); })
.then(function () { return _this.scheduleClientMetadataAndPrimaryLeaseRefreshes(); });
});
};
/** Checks whether `client` is the local client. */
IndexedDbPersistence.prototype.isLocalClient = function (client) {
return client ? client.ownerId === this.clientId : false;
};
/**
* Evaluate the state of all active clients and determine whether the local
* client is or can act as the holder of the primary lease. Returns whether
* the client is eligible for the lease, but does not actually acquire it.
* May return 'false' even if there is no active leaseholder and another
* (foreground) client should become leaseholder instead.
*/
IndexedDbPersistence.prototype.canActAsPrimary = function (txn) {
var _this = this;
var store = primaryClientStore(txn);
return store
.get(DbPrimaryClient.key)
.next(function (currentPrimary) {
var currentLeaseIsValid = currentPrimary !== null &&
_this.isWithinAge(currentPrimary.leaseTimestampMs, MAX_PRIMARY_ELIGIBLE_AGE_MS) &&
!_this.isClientZombied(currentPrimary.ownerId);
// A client is eligible for the primary lease if:
// - its network is enabled and the client's tab is in the foreground.
// - its network is enabled and no other client's tab is in the
// foreground.
// - every clients network is disabled and the client's tab is in the
// foreground.
// - every clients network is disabled and no other client's tab is in
// the foreground.
if (currentLeaseIsValid) {
if (_this.isLocalClient(currentPrimary) && _this.networkEnabled) {
return true;
}
if (!_this.isLocalClient(currentPrimary)) {
if (!currentPrimary.allowTabSynchronization) {
// Fail the `canActAsPrimary` check if the current leaseholder has
// not opted into multi-tab synchronization. If this happens at
// client startup, we reject the Promise returned by
// `enablePersistence()` and the user can continue to use Firestore
// with in-memory persistence.
// If this fails during a lease refresh, we will instead block the
// AsyncQueue from executing further operations. Note that this is
// acceptable since mixing & matching different `synchronizeTabs`
// settings is not supported.
//
// TODO(b/114226234): Remove this check when `synchronizeTabs` can
// no longer be turned off.
throw new FirestoreError(Code.FAILED_PRECONDITION, PRIMARY_LEASE_EXCLUSIVE_ERROR_MSG);
}
return false;
}
}
if (_this.networkEnabled && _this.inForeground) {
return true;
}
return clientMetadataStore(txn)
.loadAll()
.next(function (existingClients) {
// Process all existing clients and determine whether at least one of
// them is better suited to obtain the primary lease.
var preferredCandidate = _this.filterActiveClients(existingClients, MAX_PRIMARY_ELIGIBLE_AGE_MS).find(function (otherClient) {
if (_this.clientId !== otherClient.clientId) {
var otherClientHasBetterNetworkState = !_this.networkEnabled && otherClient.networkEnabled;
var otherClientHasBetterVisibility = !_this.inForeground && otherClient.inForeground;
var otherClientHasSameNetworkState = _this.networkEnabled === otherClient.networkEnabled;
if (otherClientHasBetterNetworkState ||
(otherClientHasBetterVisibility &&
otherClientHasSameNetworkState)) {
return true;
}
}
return false;
});
return preferredCandidate === undefined;
});
})
.next(function (canActAsPrimary) {
if (_this.isPrimary !== canActAsPrimary) {
logDebug(LOG_TAG$2, "Client " + (canActAsPrimary ? 'is' : 'is not') + " eligible for a primary lease.");
}
return canActAsPrimary;
});
};
IndexedDbPersistence.prototype.shutdown = function () {
return tslib.__awaiter(this, void 0, void 0, function () {
var _this = this;
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
// The shutdown() operations are idempotent and can be called even when
// start() aborted (e.g. because it couldn't acquire the persistence lease).
this._started = false;
this.markClientZombied();
if (this.clientMetadataRefresher) {
this.clientMetadataRefresher.cancel();
this.clientMetadataRefresher = null;
}
this.detachVisibilityHandler();
this.detachWindowUnloadHook();
return [4 /*yield*/, this.runTransaction('shutdown', 'readwrite', function (txn) {
return _this.releasePrimaryLeaseIfHeld(txn).next(function () { return _this.removeClientMetadata(txn); });
}).catch(function (e) {
logDebug(LOG_TAG$2, 'Proceeding with shutdown despite failure: ', e);
})];
case 1:
_e.sent();
this.simpleDb.close();
// Remove the entry marking the client as zombied from LocalStorage since
// we successfully deleted its metadata from IndexedDb.
this.removeClientZombiedEntry();
return [2 /*return*/];
}
});
});
};
/**
* Returns clients that are not zombied and have an updateTime within the
* provided threshold.
*/
IndexedDbPersistence.prototype.filterActiveClients = function (clients, activityThresholdMs) {
var _this = this;
return clients.filter(function (client) { return _this.isWithinAge(client.updateTimeMs, activityThresholdMs) &&
!_this.isClientZombied(client.clientId); });
};
/**
* Returns the IDs of the clients that are currently active. If multi-tab
* is not supported, returns an array that only contains the local client's
* ID.
*
* PORTING NOTE: This is only used for Web multi-tab.
*/
IndexedDbPersistence.prototype.getActiveClients = function () {
var _this = this;
return this.runTransaction('getActiveClients', 'readonly', function (txn) {
return clientMetadataStore(txn)
.loadAll()
.next(function (clients) { return _this.filterActiveClients(clients, MAX_CLIENT_AGE_MS).map(function (clientMetadata) { return clientMetadata.clientId; }); });
});
};
IndexedDbPersistence.clearPersistence = function (persistenceKey) {
return tslib.__awaiter(this, void 0, void 0, function () {
var dbName;
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
if (!IndexedDbPersistence.isAvailable()) {
return [2 /*return*/, Promise.resolve()];
}
dbName = persistenceKey + IndexedDbPersistence.MAIN_DATABASE;
return [4 /*yield*/, SimpleDb.delete(dbName)];
case 1:
_e.sent();
return [2 /*return*/];
}
});
});
};
Object.defineProperty(IndexedDbPersistence.prototype, "started", {
get: function () {
return this._started;
},
enumerable: true,
configurable: true
});
IndexedDbPersistence.prototype.getMutationQueue = function (user) {
debugAssert(this.started, 'Cannot initialize MutationQueue before persistence is started.');
return IndexedDbMutationQueue.forUser(user, this.serializer, this.indexManager, this.referenceDelegate);
};
IndexedDbPersistence.prototype.getTargetCache = function () {
debugAssert(this.started, 'Cannot initialize TargetCache before persistence is started.');
return this.targetCache;
};
IndexedDbPersistence.prototype.getRemoteDocumentCache = function () {
debugAssert(this.started, 'Cannot initialize RemoteDocumentCache before persistence is started.');
return this.remoteDocumentCache;
};
IndexedDbPersistence.prototype.getIndexManager = function () {
debugAssert(this.started, 'Cannot initialize IndexManager before persistence is started.');
return this.indexManager;
};
IndexedDbPersistence.prototype.runTransaction = function (action, mode, transactionOperation) {
var _this = this;
logDebug(LOG_TAG$2, 'Starting transaction:', action);
var simpleDbMode = mode === 'readonly' ? 'readonly' : 'readwrite';
var persistenceTransaction;
// Do all transactions as readwrite against all object stores, since we
// are the only reader/writer.
return this.simpleDb
.runTransaction(simpleDbMode, ALL_STORES, function (simpleDbTxn) {
persistenceTransaction = new IndexedDbTransaction(simpleDbTxn, _this.listenSequence
? _this.listenSequence.next()
: ListenSequence.INVALID);
if (mode === 'readwrite-primary') {
// While we merely verify that we have (or can acquire) the lease
// immediately, we wait to extend the primary lease until after
// executing transactionOperation(). This ensures that even if the
// transactionOperation takes a long time, we'll use a recent
// leaseTimestampMs in the extended (or newly acquired) lease.
return _this.verifyPrimaryLease(persistenceTransaction)
.next(function (holdsPrimaryLease) {
if (holdsPrimaryLease) {
return /* holdsPrimaryLease= */ true;
}
return _this.canActAsPrimary(persistenceTransaction);
})
.next(function (holdsPrimaryLease) {
if (!holdsPrimaryLease) {
logError("Failed to obtain primary lease for action '" + action + "'.");
_this.isPrimary = false;
_this.queue.enqueueAndForget(function () { return _this.primaryStateListener(false); });
throw new FirestoreError(Code.FAILED_PRECONDITION, PRIMARY_LEASE_LOST_ERROR_MSG);
}
return transactionOperation(persistenceTransaction);
})
.next(function (result) {
return _this.acquireOrExtendPrimaryLease(persistenceTransaction).next(function () { return result; });
});
}
else {
return _this.verifyAllowTabSynchronization(persistenceTransaction).next(function () { return transactionOperation(persistenceTransaction); });
}
})
.then(function (result) {
persistenceTransaction.raiseOnCommittedEvent();
return result;
});
};
/**
* Verifies that the current tab is the primary leaseholder or alternatively
* that the leaseholder has opted into multi-tab synchronization.
*/
// TODO(b/114226234): Remove this check when `synchronizeTabs` can no longer
// be turned off.
IndexedDbPersistence.prototype.verifyAllowTabSynchronization = function (txn) {
var _this = this;
var store = primaryClientStore(txn);
return store.get(DbPrimaryClient.key).next(function (currentPrimary) {
var currentLeaseIsValid = currentPrimary !== null &&
_this.isWithinAge(currentPrimary.leaseTimestampMs, MAX_PRIMARY_ELIGIBLE_AGE_MS) &&
!_this.isClientZombied(currentPrimary.ownerId);
if (currentLeaseIsValid && !_this.isLocalClient(currentPrimary)) {
if (!_this.allowTabSynchronization ||
!currentPrimary.allowTabSynchronization) {
throw new FirestoreError(Code.FAILED_PRECONDITION, PRIMARY_LEASE_EXCLUSIVE_ERROR_MSG);
}
}
});
};
/**
* Obtains or extends the new primary lease for the local client. This
* method does not verify that the client is eligible for this lease.
*/
IndexedDbPersistence.prototype.acquireOrExtendPrimaryLease = function (txn) {
var newPrimary = new DbPrimaryClient(this.clientId, this.allowTabSynchronization, Date.now());
return primaryClientStore(txn).put(DbPrimaryClient.key, newPrimary);
};
IndexedDbPersistence.isAvailable = function () {
return SimpleDb.isAvailable();
};
/**
* Generates a string used as a prefix when storing data in IndexedDB and
* LocalStorage.
*/
IndexedDbPersistence.buildStoragePrefix = function (databaseInfo) {
// Use two different prefix formats:
//
// * firestore / persistenceKey / projectID . databaseID / ...
// * firestore / persistenceKey / projectID / ...
//
// projectIDs are DNS-compatible names and cannot contain dots
// so there's no danger of collisions.
var database = databaseInfo.databaseId.projectId;
if (!databaseInfo.databaseId.isDefaultDatabase) {
database += '.' + databaseInfo.databaseId.database;
}
return 'firestore/' + databaseInfo.persistenceKey + '/' + database + '/';
};
/** Checks the primary lease and removes it if we are the current primary. */
IndexedDbPersistence.prototype.releasePrimaryLeaseIfHeld = function (txn) {
var _this = this;
var store = primaryClientStore(txn);
return store.get(DbPrimaryClient.key).next(function (primaryClient) {
if (_this.isLocalClient(primaryClient)) {
logDebug(LOG_TAG$2, 'Releasing primary lease.');
return store.delete(DbPrimaryClient.key);
}
else {
return PersistencePromise.resolve();
}
});
};
/** Verifies that `updateTimeMs` is within `maxAgeMs`. */
IndexedDbPersistence.prototype.isWithinAge = function (updateTimeMs, maxAgeMs) {
var now = Date.now();
var minAcceptable = now - maxAgeMs;
var maxAcceptable = now;
if (updateTimeMs < minAcceptable) {
return false;
}
else if (updateTimeMs > maxAcceptable) {
logError("Detected an update time that is in the future: " + updateTimeMs + " > " + maxAcceptable);
return false;
}
return true;
};
IndexedDbPersistence.prototype.attachVisibilityHandler = function () {
var _this = this;
if (this.document !== null &&
typeof this.document.addEventListener === 'function') {
this.documentVisibilityHandler = function () {
_this.queue.enqueueAndForget(function () {
_this.inForeground = _this.document.visibilityState === 'visible';
return _this.updateClientMetadataAndTryBecomePrimary();
});
};
this.document.addEventListener('visibilitychange', this.documentVisibilityHandler);
this.inForeground = this.document.visibilityState === 'visible';
}
};
IndexedDbPersistence.prototype.detachVisibilityHandler = function () {
if (this.documentVisibilityHandler) {
debugAssert(this.document !== null &&
typeof this.document.addEventListener === 'function', "Expected 'document.addEventListener' to be a function");
this.document.removeEventListener('visibilitychange', this.documentVisibilityHandler);
this.documentVisibilityHandler = null;
}
};
/**
* Attaches a window.unload handler that will synchronously write our
* clientId to a "zombie client id" location in LocalStorage. This can be used
* by tabs trying to acquire the primary lease to determine that the lease
* is no longer valid even if the timestamp is recent. This is particularly
* important for the refresh case (so the tab correctly re-acquires the
* primary lease). LocalStorage is used for this rather than IndexedDb because
* it is a synchronous API and so can be used reliably from an unload
* handler.
*/
IndexedDbPersistence.prototype.attachWindowUnloadHook = function () {
var _this = this;
if (typeof this.window.addEventListener === 'function') {
this.windowUnloadHandler = function () {
// Note: In theory, this should be scheduled on the AsyncQueue since it
// accesses internal state. We execute this code directly during shutdown
// to make sure it gets a chance to run.
_this.markClientZombied();
_this.queue.enqueueAndForget(function () {
// Attempt graceful shutdown (including releasing our primary lease),
// but there's no guarantee it will complete.
return _this.shutdown();
});
};
this.window.addEventListener('unload', this.windowUnloadHandler);
}
};
IndexedDbPersistence.prototype.detachWindowUnloadHook = function () {
if (this.windowUnloadHandler) {
debugAssert(typeof this.window.removeEventListener === 'function', "Expected 'window.removeEventListener' to be a function");
this.window.removeEventListener('unload', this.windowUnloadHandler);
this.windowUnloadHandler = null;
}
};
/**
* Returns whether a client is "zombied" based on its LocalStorage entry.
* Clients become zombied when their tab closes without running all of the
* cleanup logic in `shutdown()`.
*/
IndexedDbPersistence.prototype.isClientZombied = function (clientId) {
try {
var isZombied = this.webStorage.getItem(this.zombiedClientLocalStorageKey(clientId)) !==
null;
logDebug(LOG_TAG$2, "Client '" + clientId + "' " + (isZombied ? 'is' : 'is not') + " zombied in LocalStorage");
return isZombied;
}
catch (e) {
// Gracefully handle if LocalStorage isn't working.
logError(LOG_TAG$2, 'Failed to get zombied client id.', e);
return false;
}
};
/**
* Record client as zombied (a client that had its tab closed). Zombied
* clients are ignored during primary tab selection.
*/
IndexedDbPersistence.prototype.markClientZombied = function () {
try {
this.webStorage.setItem(this.zombiedClientLocalStorageKey(this.clientId), String(Date.now()));
}
catch (e) {
// Gracefully handle if LocalStorage isn't available / working.
logError('Failed to set zombie client id.', e);
}
};
/** Removes the zombied client entry if it exists. */
IndexedDbPersistence.prototype.removeClientZombiedEntry = function () {
try {
this.webStorage.removeItem(this.zombiedClientLocalStorageKey(this.clientId));
}
catch (e) {
// Ignore
}
};
IndexedDbPersistence.prototype.zombiedClientLocalStorageKey = function (clientId) {
return ZOMBIED_CLIENTS_KEY_PREFIX + "_" + this.persistenceKey + "_" + clientId;
};
return IndexedDbPersistence;
}());
/**
* The name of the main (and currently only) IndexedDB database. this name is
* appended to the prefix provided to the IndexedDbPersistence constructor.
*/
IndexedDbPersistence.MAIN_DATABASE = 'main';
/**
* Helper to get a typed SimpleDbStore for the primary client object store.
*/
function primaryClientStore(txn) {
return IndexedDbPersistence.getStore(txn, DbPrimaryClient.store);
}
/**
* Helper to get a typed SimpleDbStore for the client metadata object store.
*/
function clientMetadataStore(txn) {
return IndexedDbPersistence.getStore(txn, DbClientMetadata.store);
}
/** Provides LRU functionality for IndexedDB persistence. */
var IndexedDbLruDelegate = /** @class */ (function () {
function IndexedDbLruDelegate(db, params) {
this.db = db;
this.garbageCollector = new LruGarbageCollector(this, params);
}
IndexedDbLruDelegate.prototype.getSequenceNumberCount = function (txn) {
var docCountPromise = this.orphanedDocumentCount(txn);
var targetCountPromise = this.db.getTargetCache().getTargetCount(txn);
return targetCountPromise.next(function (targetCount) { return docCountPromise.next(function (docCount) { return targetCount + docCount; }); });
};
IndexedDbLruDelegate.prototype.orphanedDocumentCount = function (txn) {
var orphanedCount = 0;
return this.forEachOrphanedDocumentSequenceNumber(txn, function (_) {
orphanedCount++;
}).next(function () { return orphanedCount; });
};
IndexedDbLruDelegate.prototype.forEachTarget = function (txn, f) {
return this.db.getTargetCache().forEachTarget(txn, f);
};
IndexedDbLruDelegate.prototype.forEachOrphanedDocumentSequenceNumber = function (txn, f) {
return this.forEachOrphanedDocument(txn, function (docKey, sequenceNumber) { return f(sequenceNumber); });
};
IndexedDbLruDelegate.prototype.addReference = function (txn, targetId, key) {
return writeSentinelKey(txn, key);
};
IndexedDbLruDelegate.prototype.removeReference = function (txn, targetId, key) {
return writeSentinelKey(txn, key);
};
IndexedDbLruDelegate.prototype.removeTargets = function (txn, upperBound, activeTargetIds) {
return this.db
.getTargetCache()
.removeTargets(txn, upperBound, activeTargetIds);
};
IndexedDbLruDelegate.prototype.markPotentiallyOrphaned = function (txn, key) {
return writeSentinelKey(txn, key);
};
/**
* Returns true if anything would prevent this document from being garbage
* collected, given that the document in question is not present in any
* targets and has a sequence number less than or equal to the upper bound for
* the collection run.
*/
IndexedDbLruDelegate.prototype.isPinned = function (txn, docKey) {
return mutationQueuesContainKey(txn, docKey);
};
IndexedDbLruDelegate.prototype.removeOrphanedDocuments = function (txn, upperBound) {
var _this = this;
var documentCache = this.db.getRemoteDocumentCache();
var changeBuffer = documentCache.newChangeBuffer();
var promises = [];
var documentCount = 0;
var iteration = this.forEachOrphanedDocument(txn, function (docKey, sequenceNumber) {
if (sequenceNumber <= upperBound) {
var p = _this.isPinned(txn, docKey).next(function (isPinned) {
if (!isPinned) {
documentCount++;
// Our size accounting requires us to read all documents before
// removing them.
return changeBuffer.getEntry(txn, docKey).next(function () {
changeBuffer.removeEntry(docKey);
return documentTargetStore(txn).delete(sentinelKey(docKey));
});
}
});
promises.push(p);
}
});
return iteration
.next(function () { return PersistencePromise.waitFor(promises); })
.next(function () { return changeBuffer.apply(txn); })
.next(function () { return documentCount; });
};
IndexedDbLruDelegate.prototype.removeTarget = function (txn, targetData) {
var updated = targetData.withSequenceNumber(txn.currentSequenceNumber);
return this.db.getTargetCache().updateTargetData(txn, updated);
};
IndexedDbLruDelegate.prototype.updateLimboDocument = function (txn, key) {
return writeSentinelKey(txn, key);
};
/**
* Call provided function for each document in the cache that is 'orphaned'. Orphaned
* means not a part of any target, so the only entry in the target-document index for
* that document will be the sentinel row (targetId 0), which will also have the sequence
* number for the last time the document was accessed.
*/
IndexedDbLruDelegate.prototype.forEachOrphanedDocument = function (txn, f) {
var store = documentTargetStore(txn);
var nextToReport = ListenSequence.INVALID;
var nextPath;
return store
.iterate({
index: DbTargetDocument.documentTargetsIndex
}, function (_e, _f) {
var targetId = _e[0], docKey = _e[1];
var path = _f.path, sequenceNumber = _f.sequenceNumber;
if (targetId === 0) {
// if nextToReport is valid, report it, this is a new key so the
// last one must not be a member of any targets.
if (nextToReport !== ListenSequence.INVALID) {
f(new DocumentKey(decodeResourcePath(nextPath)), nextToReport);
}
// set nextToReport to be this sequence number. It's the next one we
// might report, if we don't find any targets for this document.
// Note that the sequence number must be defined when the targetId
// is 0.
nextToReport = sequenceNumber;
nextPath = path;
}
else {
// set nextToReport to be invalid, we know we don't need to report
// this one since we found a target for it.
nextToReport = ListenSequence.INVALID;
}
})
.next(function () {
// Since we report sequence numbers after getting to the next key, we
// need to check if the last key we iterated over was an orphaned
// document and report it.
if (nextToReport !== ListenSequence.INVALID) {
f(new DocumentKey(decodeResourcePath(nextPath)), nextToReport);
}
});
};
IndexedDbLruDelegate.prototype.getCacheSize = function (txn) {
return this.db.getRemoteDocumentCache().getSize(txn);
};
return IndexedDbLruDelegate;
}());
function sentinelKey(key) {
return [0, encodeResourcePath(key.path)];
}
/**
* @return A value suitable for writing a sentinel row in the target-document
* store.
*/
function sentinelRow(key, sequenceNumber) {
return new DbTargetDocument(0, encodeResourcePath(key.path), sequenceNumber);
}
function writeSentinelKey(txn, key) {
return documentTargetStore(txn).put(sentinelRow(key, txn.currentSequenceNumber));
}
/**
* @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 mutation queue for a specific user, backed by IndexedDB. */
var IndexedDbMutationQueue = /** @class */ (function () {
function IndexedDbMutationQueue(
/**
* The normalized userId (e.g. null UID => "" userId) used to store /
* retrieve mutations.
*/
userId, serializer, indexManager, referenceDelegate) {
this.userId = userId;
this.serializer = serializer;
this.indexManager = indexManager;
this.referenceDelegate = referenceDelegate;
/**
* Caches the document keys for pending mutation batches. If the mutation
* has been removed from IndexedDb, the cached value may continue to
* be used to retrieve the batch's document keys. To remove a cached value
* locally, `removeCachedMutationKeys()` should be invoked either directly
* or through `removeMutationBatches()`.
*
* With multi-tab, when the primary client acknowledges or rejects a mutation,
* this cache is used by secondary clients to invalidate the local
* view of the documents that were previously affected by the mutation.
*/
// PORTING NOTE: Multi-tab only.
this.documentKeysByBatchId = {};
}
/**
* Creates a new mutation queue for the given user.
* @param user The user for which to create a mutation queue.
* @param serializer The serializer to use when persisting to IndexedDb.
*/
IndexedDbMutationQueue.forUser = function (user, serializer, indexManager, referenceDelegate) {
// TODO(mcg): Figure out what constraints there are on userIDs
// In particular, are there any reserved characters? are empty ids allowed?
// For the moment store these together in the same mutations table assuming
// that empty userIDs aren't allowed.
hardAssert(user.uid !== '', 'UserID must not be an empty string.');
var userId = user.isAuthenticated() ? user.uid : '';
return new IndexedDbMutationQueue(userId, serializer, indexManager, referenceDelegate);
};
IndexedDbMutationQueue.prototype.checkEmpty = function (transaction) {
var empty = true;
var range = IDBKeyRange.bound([this.userId, Number.NEGATIVE_INFINITY], [this.userId, Number.POSITIVE_INFINITY]);
return mutationsStore(transaction)
.iterate({ index: DbMutationBatch.userMutationsIndex, range: range }, function (key, value, control) {
empty = false;
control.done();
})
.next(function () { return empty; });
};
IndexedDbMutationQueue.prototype.acknowledgeBatch = function (transaction, batch, streamToken) {
return this.getMutationQueueMetadata(transaction).next(function (metadata) {
// We can't store the resumeToken as a ByteString in IndexedDB, so we
// convert it to a Base64 string for storage.
metadata.lastStreamToken = streamToken.toBase64();
return mutationQueuesStore(transaction).put(metadata);
});
};
IndexedDbMutationQueue.prototype.getLastStreamToken = function (transaction) {
return this.getMutationQueueMetadata(transaction).next(function (metadata) { return ByteString.fromBase64String(metadata.lastStreamToken); });
};
IndexedDbMutationQueue.prototype.setLastStreamToken = function (transaction, streamToken) {
return this.getMutationQueueMetadata(transaction).next(function (metadata) {
// We can't store the resumeToken as a ByteString in IndexedDB, so we
// convert it to a Base64 string for storage.
metadata.lastStreamToken = streamToken.toBase64();
return mutationQueuesStore(transaction).put(metadata);
});
};
IndexedDbMutationQueue.prototype.addMutationBatch = function (transaction, localWriteTime, baseMutations, mutations) {
var _this = this;
var documentStore = documentMutationsStore(transaction);
var mutationStore = mutationsStore(transaction);
// The IndexedDb implementation in Chrome (and Firefox) does not handle
// compound indices that include auto-generated keys correctly. To ensure
// that the index entry is added correctly in all browsers, we perform two
// writes: The first write is used to retrieve the next auto-generated Batch
// ID, and the second write populates the index and stores the actual
// mutation batch.
// See: https://bugs.chromium.org/p/chromium/issues/detail?id=701972
// We write an empty object to obtain key
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return mutationStore.add({}).next(function (batchId) {
hardAssert(typeof batchId === 'number', 'Auto-generated key is not a number');
var batch = new MutationBatch(batchId, localWriteTime, baseMutations, mutations);
var dbBatch = _this.serializer.toDbMutationBatch(_this.userId, batch);
var promises = [];
var collectionParents = new SortedSet(function (l, r) { return primitiveComparator(l.canonicalString(), r.canonicalString()); });
for (var _i = 0, mutations_1 = mutations; _i < mutations_1.length; _i++) {
var mutation = mutations_1[_i];
var indexKey = DbDocumentMutation.key(_this.userId, mutation.key.path, batchId);
collectionParents = collectionParents.add(mutation.key.path.popLast());
promises.push(mutationStore.put(dbBatch));
promises.push(documentStore.put(indexKey, DbDocumentMutation.PLACEHOLDER));
}
collectionParents.forEach(function (parent) {
promises.push(_this.indexManager.addToCollectionParentIndex(transaction, parent));
});
transaction.addOnCommittedListener(function () {
_this.documentKeysByBatchId[batchId] = batch.keys();
});
return PersistencePromise.waitFor(promises).next(function () { return batch; });
});
};
IndexedDbMutationQueue.prototype.lookupMutationBatch = function (transaction, batchId) {
var _this = this;
return mutationsStore(transaction)
.get(batchId)
.next(function (dbBatch) {
if (dbBatch) {
hardAssert(dbBatch.userId === _this.userId, "Unexpected user '" + dbBatch.userId + "' for mutation batch " + batchId);
return _this.serializer.fromDbMutationBatch(dbBatch);
}
return null;
});
};
/**
* Returns the document keys for the mutation batch with the given batchId.
* For primary clients, this method returns `null` after
* `removeMutationBatches()` has been called. Secondary clients return a
* cached result until `removeCachedMutationKeys()` is invoked.
*/
// PORTING NOTE: Multi-tab only.
IndexedDbMutationQueue.prototype.lookupMutationKeys = function (transaction, batchId) {
var _this = this;
if (this.documentKeysByBatchId[batchId]) {
return PersistencePromise.resolve(this.documentKeysByBatchId[batchId]);
}
else {
return this.lookupMutationBatch(transaction, batchId).next(function (batch) {
if (batch) {
var keys = batch.keys();
_this.documentKeysByBatchId[batchId] = keys;
return keys;
}
else {
return null;
}
});
}
};
IndexedDbMutationQueue.prototype.getNextMutationBatchAfterBatchId = function (transaction, batchId) {
var _this = this;
var nextBatchId = batchId + 1;
var range = IDBKeyRange.lowerBound([this.userId, nextBatchId]);
var foundBatch = null;
return mutationsStore(transaction)
.iterate({ index: DbMutationBatch.userMutationsIndex, range: range }, function (key, dbBatch, control) {
if (dbBatch.userId === _this.userId) {
hardAssert(dbBatch.batchId >= nextBatchId, 'Should have found mutation after ' + nextBatchId);
foundBatch = _this.serializer.fromDbMutationBatch(dbBatch);
}
control.done();
})
.next(function () { return foundBatch; });
};
IndexedDbMutationQueue.prototype.getHighestUnacknowledgedBatchId = function (transaction) {
var range = IDBKeyRange.upperBound([
this.userId,
Number.POSITIVE_INFINITY
]);
var batchId = BATCHID_UNKNOWN;
return mutationsStore(transaction)
.iterate({ index: DbMutationBatch.userMutationsIndex, range: range, reverse: true }, function (key, dbBatch, control) {
batchId = dbBatch.batchId;
control.done();
})
.next(function () { return batchId; });
};
IndexedDbMutationQueue.prototype.getAllMutationBatches = function (transaction) {
var _this = this;
var range = IDBKeyRange.bound([this.userId, BATCHID_UNKNOWN], [this.userId, Number.POSITIVE_INFINITY]);
return mutationsStore(transaction)
.loadAll(DbMutationBatch.userMutationsIndex, range)
.next(function (dbBatches) { return dbBatches.map(function (dbBatch) { return _this.serializer.fromDbMutationBatch(dbBatch); }); });
};
IndexedDbMutationQueue.prototype.getAllMutationBatchesAffectingDocumentKey = function (transaction, documentKey) {
var _this = this;
// Scan the document-mutation index starting with a prefix starting with
// the given documentKey.
var indexPrefix = DbDocumentMutation.prefixForPath(this.userId, documentKey.path);
var indexStart = IDBKeyRange.lowerBound(indexPrefix);
var results = [];
return documentMutationsStore(transaction)
.iterate({ range: indexStart }, function (indexKey, _, control) {
var userID = indexKey[0], encodedPath = indexKey[1], batchId = indexKey[2];
// Only consider rows matching exactly the specific key of
// interest. Note that because we order by path first, and we
// order terminators before path separators, we'll encounter all
// the index rows for documentKey contiguously. In particular, all
// the rows for documentKey will occur before any rows for
// documents nested in a subcollection beneath documentKey so we
// can stop as soon as we hit any such row.
var path = decodeResourcePath(encodedPath);
if (userID !== _this.userId || !documentKey.path.isEqual(path)) {
control.done();
return;
}
// Look up the mutation batch in the store.
return mutationsStore(transaction)
.get(batchId)
.next(function (mutation) {
if (!mutation) {
throw fail('Dangling document-mutation reference found: ' +
indexKey +
' which points to ' +
batchId);
}
hardAssert(mutation.userId === _this.userId, "Unexpected user '" + mutation.userId + "' for mutation batch " + batchId);
results.push(_this.serializer.fromDbMutationBatch(mutation));
});
})
.next(function () { return results; });
};
IndexedDbMutationQueue.prototype.getAllMutationBatchesAffectingDocumentKeys = function (transaction, documentKeys) {
var _this = this;
var uniqueBatchIDs = new SortedSet(primitiveComparator);
var promises = [];
documentKeys.forEach(function (documentKey) {
var indexStart = DbDocumentMutation.prefixForPath(_this.userId, documentKey.path);
var range = IDBKeyRange.lowerBound(indexStart);
var promise = documentMutationsStore(transaction).iterate({ range: range }, function (indexKey, _, control) {
var userID = indexKey[0], encodedPath = indexKey[1], batchID = indexKey[2];
// Only consider rows matching exactly the specific key of
// interest. Note that because we order by path first, and we
// order terminators before path separators, we'll encounter all
// the index rows for documentKey contiguously. In particular, all
// the rows for documentKey will occur before any rows for
// documents nested in a subcollection beneath documentKey so we
// can stop as soon as we hit any such row.
var path = decodeResourcePath(encodedPath);
if (userID !== _this.userId || !documentKey.path.isEqual(path)) {
control.done();
return;
}
uniqueBatchIDs = uniqueBatchIDs.add(batchID);
});
promises.push(promise);
});
return PersistencePromise.waitFor(promises).next(function () { return _this.lookupMutationBatches(transaction, uniqueBatchIDs); });
};
IndexedDbMutationQueue.prototype.getAllMutationBatchesAffectingQuery = function (transaction, query) {
var _this = this;
debugAssert(!query.isDocumentQuery(), "Document queries shouldn't go down this path");
debugAssert(!query.isCollectionGroupQuery(), 'CollectionGroup queries should be handled in LocalDocumentsView');
var queryPath = query.path;
var immediateChildrenLength = queryPath.length + 1;
// TODO(mcg): Actually implement a single-collection query
//
// This is actually executing an ancestor query, traversing the whole
// subtree below the collection which can be horrifically inefficient for
// some structures. The right way to solve this is to implement the full
// value index, but that's not in the cards in the near future so this is
// the best we can do for the moment.
//
// Since we don't yet index the actual properties in the mutations, our
// current approach is to just return all mutation batches that affect
// documents in the collection being queried.
var indexPrefix = DbDocumentMutation.prefixForPath(this.userId, queryPath);
var indexStart = IDBKeyRange.lowerBound(indexPrefix);
// Collect up unique batchIDs encountered during a scan of the index. Use a
// SortedSet to accumulate batch IDs so they can be traversed in order in a
// scan of the main table.
var uniqueBatchIDs = new SortedSet(primitiveComparator);
return documentMutationsStore(transaction)
.iterate({ range: indexStart }, function (indexKey, _, control) {
var userID = indexKey[0], encodedPath = indexKey[1], batchID = indexKey[2];
var path = decodeResourcePath(encodedPath);
if (userID !== _this.userId || !queryPath.isPrefixOf(path)) {
control.done();
return;
}
// Rows with document keys more than one segment longer than the
// query path can't be matches. For example, a query on 'rooms'
// can't match the document /rooms/abc/messages/xyx.
// TODO(mcg): we'll need a different scanner when we implement
// ancestor queries.
if (path.length !== immediateChildrenLength) {
return;
}
uniqueBatchIDs = uniqueBatchIDs.add(batchID);
})
.next(function () { return _this.lookupMutationBatches(transaction, uniqueBatchIDs); });
};
IndexedDbMutationQueue.prototype.lookupMutationBatches = function (transaction, batchIDs) {
var _this = this;
var results = [];
var promises = [];
// TODO(rockwood): Implement this using iterate.
batchIDs.forEach(function (batchId) {
promises.push(mutationsStore(transaction)
.get(batchId)
.next(function (mutation) {
if (mutation === null) {
throw fail('Dangling document-mutation reference found, ' +
'which points to ' +
batchId);
}
hardAssert(mutation.userId === _this.userId, "Unexpected user '" + mutation.userId + "' for mutation batch " + batchId);
results.push(_this.serializer.fromDbMutationBatch(mutation));
}));
});
return PersistencePromise.waitFor(promises).next(function () { return results; });
};
IndexedDbMutationQueue.prototype.removeMutationBatch = function (transaction, batch) {
var _this = this;
return removeMutationBatch(transaction.simpleDbTransaction, this.userId, batch).next(function (removedDocuments) {
transaction.addOnCommittedListener(function () {
_this.removeCachedMutationKeys(batch.batchId);
});
return PersistencePromise.forEach(removedDocuments, function (key) {
return _this.referenceDelegate.markPotentiallyOrphaned(transaction, key);
});
});
};
/**
* Clears the cached keys for a mutation batch. This method should be
* called by secondary clients after they process mutation updates.
*
* Note that this method does not have to be called from primary clients as
* the corresponding cache entries are cleared when an acknowledged or
* rejected batch is removed from the mutation queue.
*/
// PORTING NOTE: Multi-tab only
IndexedDbMutationQueue.prototype.removeCachedMutationKeys = function (batchId) {
delete this.documentKeysByBatchId[batchId];
};
IndexedDbMutationQueue.prototype.performConsistencyCheck = function (txn) {
var _this = this;
return this.checkEmpty(txn).next(function (empty) {
if (!empty) {
return PersistencePromise.resolve();
}
// Verify that there are no entries in the documentMutations index if
// the queue is empty.
var startRange = IDBKeyRange.lowerBound(DbDocumentMutation.prefixForUser(_this.userId));
var danglingMutationReferences = [];
return documentMutationsStore(txn)
.iterate({ range: startRange }, function (key, _, control) {
var userID = key[0];
if (userID !== _this.userId) {
control.done();
return;
}
else {
var path = decodeResourcePath(key[1]);
danglingMutationReferences.push(path);
}
})
.next(function () {
hardAssert(danglingMutationReferences.length === 0, 'Document leak -- detected dangling mutation references when queue is empty. ' +
'Dangling keys: ' +
danglingMutationReferences.map(function (p) { return p.canonicalString(); }));
});
});
};
IndexedDbMutationQueue.prototype.containsKey = function (txn, key) {
return mutationQueueContainsKey(txn, this.userId, key);
};
// PORTING NOTE: Multi-tab only (state is held in memory in other clients).
/** Returns the mutation queue's metadata from IndexedDb. */
IndexedDbMutationQueue.prototype.getMutationQueueMetadata = function (transaction) {
var _this = this;
return mutationQueuesStore(transaction)
.get(this.userId)
.next(function (metadata) {
return (metadata ||
new DbMutationQueue(_this.userId, BATCHID_UNKNOWN,
/*lastStreamToken=*/ ''));
});
};
return IndexedDbMutationQueue;
}());
/**
* @return true if the mutation queue for the given user contains a pending
* mutation for the given key.
*/
function mutationQueueContainsKey(txn, userId, key) {
var indexKey = DbDocumentMutation.prefixForPath(userId, key.path);
var encodedPath = indexKey[1];
var startRange = IDBKeyRange.lowerBound(indexKey);
var containsKey = false;
return documentMutationsStore(txn)
.iterate({ range: startRange, keysOnly: true }, function (key, value, control) {
var userID = key[0], keyPath = key[1], /*batchID*/ _ = key[2];
if (userID === userId && keyPath === encodedPath) {
containsKey = true;
}
control.done();
})
.next(function () { return containsKey; });
}
/** Returns true if any mutation queue contains the given document. */
function mutationQueuesContainKey(txn, docKey) {
var found = false;
return mutationQueuesStore(txn)
.iterateSerial(function (userId) {
return mutationQueueContainsKey(txn, userId, docKey).next(function (containsKey) {
if (containsKey) {
found = true;
}
return PersistencePromise.resolve(!containsKey);
});
})
.next(function () { return found; });
}
/**
* Delete a mutation batch and the associated document mutations.
* @return A PersistencePromise of the document mutations that were removed.
*/
function removeMutationBatch(txn, userId, batch) {
var mutationStore = txn.store(DbMutationBatch.store);
var indexTxn = txn.store(DbDocumentMutation.store);
var promises = [];
var range = IDBKeyRange.only(batch.batchId);
var numDeleted = 0;
var removePromise = mutationStore.iterate({ range: range }, function (key, value, control) {
numDeleted++;
return control.delete();
});
promises.push(removePromise.next(function () {
hardAssert(numDeleted === 1, 'Dangling document-mutation reference found: Missing batch ' +
batch.batchId);
}));
var removedDocuments = [];
for (var _i = 0, _e = batch.mutations; _i < _e.length; _i++) {
var mutation = _e[_i];
var indexKey = DbDocumentMutation.key(userId, mutation.key.path, batch.batchId);
promises.push(indexTxn.delete(indexKey));
removedDocuments.push(mutation.key);
}
return PersistencePromise.waitFor(promises).next(function () { return removedDocuments; });
}
/**
* Helper to get a typed SimpleDbStore for the mutations object store.
*/
function mutationsStore(txn) {
return IndexedDbPersistence.getStore(txn, DbMutationBatch.store);
}
/**
* Helper to get a typed SimpleDbStore for the mutationQueues object store.
*/
function documentMutationsStore(txn) {
return IndexedDbPersistence.getStore(txn, DbDocumentMutation.store);
}
/**
* Helper to get a typed SimpleDbStore for the mutationQueues object store.
*/
function mutationQueuesStore(txn) {
return IndexedDbPersistence.getStore(txn, DbMutationQueue.store);
}
/**
* @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.
*/
/**
* Schema Version for the Web client:
* 1. Initial version including Mutation Queue, Query Cache, and Remote
* Document Cache
* 2. Used to ensure a targetGlobal object exists and add targetCount to it. No
* longer required because migration 3 unconditionally clears it.
* 3. Dropped and re-created Query Cache to deal with cache corruption related
* to limbo resolution. Addresses
* https://github.com/firebase/firebase-ios-sdk/issues/1548
* 4. Multi-Tab Support.
* 5. Removal of held write acks.
* 6. Create document global for tracking document cache size.
* 7. Ensure every cached document has a sentinel row with a sequence number.
* 8. Add collection-parent index for Collection Group queries.
* 9. Change RemoteDocumentChanges store to be keyed by readTime rather than
* an auto-incrementing ID. This is required for Index-Free queries.
* 10. Rewrite the canonical IDs to the explicit Protobuf-based format.
*/
var SCHEMA_VERSION = 10;
/** Performs database creation and schema upgrades. */
var SchemaConverter = /** @class */ (function () {
function SchemaConverter(serializer) {
this.serializer = serializer;
}
/**
* Performs database creation and schema upgrades.
*
* Note that in production, this method is only ever used to upgrade the schema
* to SCHEMA_VERSION. Different values of toVersion are only used for testing
* and local feature development.
*/
SchemaConverter.prototype.createOrUpgrade = function (db, txn, fromVersion, toVersion) {
var _this = this;
hardAssert(fromVersion < toVersion &&
fromVersion >= 0 &&
toVersion <= SCHEMA_VERSION, "Unexpected schema upgrade from v" + fromVersion + " to v" + toVersion + ".");
var simpleDbTransaction = new SimpleDbTransaction(txn);
if (fromVersion < 1 && toVersion >= 1) {
createPrimaryClientStore(db);
createMutationQueue(db);
createQueryCache(db);
createRemoteDocumentCache(db);
}
// Migration 2 to populate the targetGlobal object no longer needed since
// migration 3 unconditionally clears it.
var p = PersistencePromise.resolve();
if (fromVersion < 3 && toVersion >= 3) {
// Brand new clients don't need to drop and recreate--only clients that
// potentially have corrupt data.
if (fromVersion !== 0) {
dropQueryCache(db);
createQueryCache(db);
}
p = p.next(function () { return writeEmptyTargetGlobalEntry(simpleDbTransaction); });
}
if (fromVersion < 4 && toVersion >= 4) {
if (fromVersion !== 0) {
// Schema version 3 uses auto-generated keys to generate globally unique
// mutation batch IDs (this was previously ensured internally by the
// client). To migrate to the new schema, we have to read all mutations
// and write them back out. We preserve the existing batch IDs to guarantee
// consistency with other object stores. Any further mutation batch IDs will
// be auto-generated.
p = p.next(function () { return upgradeMutationBatchSchemaAndMigrateData(db, simpleDbTransaction); });
}
p = p.next(function () {
createClientMetadataStore(db);
});
}
if (fromVersion < 5 && toVersion >= 5) {
p = p.next(function () { return _this.removeAcknowledgedMutations(simpleDbTransaction); });
}
if (fromVersion < 6 && toVersion >= 6) {
p = p.next(function () {
createDocumentGlobalStore(db);
return _this.addDocumentGlobal(simpleDbTransaction);
});
}
if (fromVersion < 7 && toVersion >= 7) {
p = p.next(function () { return _this.ensureSequenceNumbers(simpleDbTransaction); });
}
if (fromVersion < 8 && toVersion >= 8) {
p = p.next(function () { return _this.createCollectionParentIndex(db, simpleDbTransaction); });
}
if (fromVersion < 9 && toVersion >= 9) {
p = p.next(function () {
// Multi-Tab used to manage its own changelog, but this has been moved
// to the DbRemoteDocument object store itself. Since the previous change
// log only contained transient data, we can drop its object store.
dropRemoteDocumentChangesStore(db);
createRemoteDocumentReadTimeIndex(txn);
});
}
if (fromVersion < 10 && toVersion >= 10) {
p = p.next(function () { return _this.rewriteCanonicalIds(simpleDbTransaction); });
}
return p;
};
SchemaConverter.prototype.addDocumentGlobal = function (txn) {
var byteCount = 0;
return txn
.store(DbRemoteDocument.store)
.iterate(function (_, doc) {
byteCount += dbDocumentSize(doc);
})
.next(function () {
var metadata = new DbRemoteDocumentGlobal(byteCount);
return txn
.store(DbRemoteDocumentGlobal.store)
.put(DbRemoteDocumentGlobal.key, metadata);
});
};
SchemaConverter.prototype.removeAcknowledgedMutations = function (txn) {
var _this = this;
var queuesStore = txn.store(DbMutationQueue.store);
var mutationsStore = txn.store(DbMutationBatch.store);
return queuesStore.loadAll().next(function (queues) {
return PersistencePromise.forEach(queues, function (queue) {
var range = IDBKeyRange.bound([queue.userId, BATCHID_UNKNOWN], [queue.userId, queue.lastAcknowledgedBatchId]);
return mutationsStore
.loadAll(DbMutationBatch.userMutationsIndex, range)
.next(function (dbBatches) {
return PersistencePromise.forEach(dbBatches, function (dbBatch) {
hardAssert(dbBatch.userId === queue.userId, "Cannot process batch " + dbBatch.batchId + " from unexpected user");
var batch = _this.serializer.fromDbMutationBatch(dbBatch);
return removeMutationBatch(txn, queue.userId, batch).next(function () { });
});
});
});
});
};
/**
* Ensures that every document in the remote document cache has a corresponding sentinel row
* with a sequence number. Missing rows are given the most recently used sequence number.
*/
SchemaConverter.prototype.ensureSequenceNumbers = function (txn) {
var documentTargetStore = txn.store(DbTargetDocument.store);
var documentsStore = txn.store(DbRemoteDocument.store);
var globalTargetStore = txn.store(DbTargetGlobal.store);
return globalTargetStore.get(DbTargetGlobal.key).next(function (metadata) {
debugAssert(!!metadata, 'Metadata should have been written during the version 3 migration');
var writeSentinelKey = function (path) {
return documentTargetStore.put(new DbTargetDocument(0, encodeResourcePath(path), metadata.highestListenSequenceNumber));
};
var promises = [];
return documentsStore
.iterate(function (key, doc) {
var path = new ResourcePath(key);
var docSentinelKey = sentinelKey$1(path);
promises.push(documentTargetStore.get(docSentinelKey).next(function (maybeSentinel) {
if (!maybeSentinel) {
return writeSentinelKey(path);
}
else {
return PersistencePromise.resolve();
}
}));
})
.next(function () { return PersistencePromise.waitFor(promises); });
});
};
SchemaConverter.prototype.createCollectionParentIndex = function (db, txn) {
// Create the index.
db.createObjectStore(DbCollectionParent.store, {
keyPath: DbCollectionParent.keyPath
});
var collectionParentsStore = txn.store(DbCollectionParent.store);
// Helper to add an index entry iff we haven't already written it.
var cache = new MemoryCollectionParentIndex();
var addEntry = function (collectionPath) {
if (cache.add(collectionPath)) {
var collectionId = collectionPath.lastSegment();
var parentPath = collectionPath.popLast();
return collectionParentsStore.put({
collectionId: collectionId,
parent: encodeResourcePath(parentPath)
});
}
};
// Index existing remote documents.
return txn
.store(DbRemoteDocument.store)
.iterate({ keysOnly: true }, function (pathSegments, _) {
var path = new ResourcePath(pathSegments);
return addEntry(path.popLast());
})
.next(function () {
// Index existing mutations.
return txn
.store(DbDocumentMutation.store)
.iterate({ keysOnly: true }, function (_e, _) {
var userID = _e[0], encodedPath = _e[1], batchId = _e[2];
var path = decodeResourcePath(encodedPath);
return addEntry(path.popLast());
});
});
};
SchemaConverter.prototype.rewriteCanonicalIds = function (txn) {
var _this = this;
var targetStore = txn.store(DbTarget.store);
return targetStore.iterate(function (key, originalDbTarget) {
var originalTargetData = _this.serializer.fromDbTarget(originalDbTarget);
var updatedDbTarget = _this.serializer.toDbTarget(originalTargetData);
return targetStore.put(updatedDbTarget);
});
};
return SchemaConverter;
}());
function sentinelKey$1(path) {
return [0, encodeResourcePath(path)];
}
/**
* Wrapper class to store timestamps (seconds and nanos) in IndexedDb objects.
*/
var DbTimestamp = /** @class */ (function () {
function DbTimestamp(seconds, nanoseconds) {
this.seconds = seconds;
this.nanoseconds = nanoseconds;
}
return DbTimestamp;
}());
/**
* A singleton object to be stored in the 'owner' store in IndexedDb.
*
* A given database can have a single primary tab assigned at a given time. That
* tab must validate that it is still holding the primary lease before every
* operation that requires locked access. The primary tab should regularly
* write an updated timestamp to this lease to prevent other tabs from
* "stealing" the primary lease
*/
var DbPrimaryClient = /** @class */ (function () {
function DbPrimaryClient(ownerId,
/** Whether to allow shared access from multiple tabs. */
allowTabSynchronization, leaseTimestampMs) {
this.ownerId = ownerId;
this.allowTabSynchronization = allowTabSynchronization;
this.leaseTimestampMs = leaseTimestampMs;
}
return DbPrimaryClient;
}());
/**
* Name of the IndexedDb object store.
*
* Note that the name 'owner' is chosen to ensure backwards compatibility with
* older clients that only supported single locked access to the persistence
* layer.
*/
DbPrimaryClient.store = 'owner';
/**
* The key string used for the single object that exists in the
* DbPrimaryClient store.
*/
DbPrimaryClient.key = 'owner';
function createPrimaryClientStore(db) {
db.createObjectStore(DbPrimaryClient.store);
}
/**
* An object to be stored in the 'mutationQueues' store in IndexedDb.
*
* Each user gets a single queue of MutationBatches to apply to the server.
* DbMutationQueue tracks the metadata about the queue.
*/
var DbMutationQueue = /** @class */ (function () {
function DbMutationQueue(
/**
* The normalized user ID to which this queue belongs.
*/
userId,
/**
* An identifier for the highest numbered batch that has been acknowledged
* by the server. All MutationBatches in this queue with batchIds less
* than or equal to this value are considered to have been acknowledged by
* the server.
*
* NOTE: this is deprecated and no longer used by the code.
*/
lastAcknowledgedBatchId,
/**
* A stream token that was previously sent by the server.
*
* See StreamingWriteRequest in datastore.proto for more details about
* usage.
*
* After sending this token, earlier tokens may not be used anymore so
* only a single stream token is retained.
*/
lastStreamToken) {
this.userId = userId;
this.lastAcknowledgedBatchId = lastAcknowledgedBatchId;
this.lastStreamToken = lastStreamToken;
}
return DbMutationQueue;
}());
/** Name of the IndexedDb object store. */
DbMutationQueue.store = 'mutationQueues';
/** Keys are automatically assigned via the userId property. */
DbMutationQueue.keyPath = 'userId';
/**
* An object to be stored in the 'mutations' store in IndexedDb.
*
* Represents a batch of user-level mutations intended to be sent to the server
* in a single write. Each user-level batch gets a separate DbMutationBatch
* with a new batchId.
*/
var DbMutationBatch = /** @class */ (function () {
function DbMutationBatch(
/**
* The normalized user ID to which this batch belongs.
*/
userId,
/**
* An identifier for this batch, allocated using an auto-generated key.
*/
batchId,
/**
* The local write time of the batch, stored as milliseconds since the
* epoch.
*/
localWriteTimeMs,
/**
* A list of "mutations" that represent a partial base state from when this
* write batch was initially created. During local application of the write
* batch, these baseMutations are applied prior to the real writes in order
* to override certain document fields from the remote document cache. This
* is necessary in the case of non-idempotent writes (e.g. `increment()`
* transforms) to make sure that the local view of the modified documents
* doesn't flicker if the remote document cache receives the result of the
* non-idempotent write before the write is removed from the queue.
*
* These mutations are never sent to the backend.
*/
baseMutations,
/**
* A list of mutations to apply. All mutations will be applied atomically.
*
* Mutations are serialized via JsonProtoSerializer.toMutation().
*/
mutations) {
this.userId = userId;
this.batchId = batchId;
this.localWriteTimeMs = localWriteTimeMs;
this.baseMutations = baseMutations;
this.mutations = mutations;
}
return DbMutationBatch;
}());
/** Name of the IndexedDb object store. */
DbMutationBatch.store = 'mutations';
/** Keys are automatically assigned via the userId, batchId properties. */
DbMutationBatch.keyPath = 'batchId';
/** The index name for lookup of mutations by user. */
DbMutationBatch.userMutationsIndex = 'userMutationsIndex';
/** The user mutations index is keyed by [userId, batchId] pairs. */
DbMutationBatch.userMutationsKeyPath = ['userId', 'batchId'];
function createMutationQueue(db) {
db.createObjectStore(DbMutationQueue.store, {
keyPath: DbMutationQueue.keyPath
});
var mutationBatchesStore = db.createObjectStore(DbMutationBatch.store, {
keyPath: DbMutationBatch.keyPath,
autoIncrement: true
});
mutationBatchesStore.createIndex(DbMutationBatch.userMutationsIndex, DbMutationBatch.userMutationsKeyPath, { unique: true });
db.createObjectStore(DbDocumentMutation.store);
}
/**
* Upgrade function to migrate the 'mutations' store from V1 to V3. Loads
* and rewrites all data.
*/
function upgradeMutationBatchSchemaAndMigrateData(db, txn) {
var v1MutationsStore = txn.store(DbMutationBatch.store);
return v1MutationsStore.loadAll().next(function (existingMutations) {
db.deleteObjectStore(DbMutationBatch.store);
var mutationsStore = db.createObjectStore(DbMutationBatch.store, {
keyPath: DbMutationBatch.keyPath,
autoIncrement: true
});
mutationsStore.createIndex(DbMutationBatch.userMutationsIndex, DbMutationBatch.userMutationsKeyPath, { unique: true });
var v3MutationsStore = txn.store(DbMutationBatch.store);
var writeAll = existingMutations.map(function (mutation) { return v3MutationsStore.put(mutation); });
return PersistencePromise.waitFor(writeAll);
});
}
/**
* An object to be stored in the 'documentMutations' store in IndexedDb.
*
* A manually maintained index of all the mutation batches that affect a given
* document key. The rows in this table are references based on the contents of
* DbMutationBatch.mutations.
*/
var DbDocumentMutation = /** @class */ (function () {
function DbDocumentMutation() {
}
/**
* Creates a [userId] key for use in the DbDocumentMutations index to iterate
* over all of a user's document mutations.
*/
DbDocumentMutation.prefixForUser = function (userId) {
return [userId];
};
/**
* Creates a [userId, encodedPath] key for use in the DbDocumentMutations
* index to iterate over all at document mutations for a given path or lower.
*/
DbDocumentMutation.prefixForPath = function (userId, path) {
return [userId, encodeResourcePath(path)];
};
/**
* Creates a full index key of [userId, encodedPath, batchId] for inserting
* and deleting into the DbDocumentMutations index.
*/
DbDocumentMutation.key = function (userId, path, batchId) {
return [userId, encodeResourcePath(path), batchId];
};
return DbDocumentMutation;
}());
DbDocumentMutation.store = 'documentMutations';
/**
* Because we store all the useful information for this store in the key,
* there is no useful information to store as the value. The raw (unencoded)
* path cannot be stored because IndexedDb doesn't store prototype
* information.
*/
DbDocumentMutation.PLACEHOLDER = new DbDocumentMutation();
function createRemoteDocumentCache(db) {
db.createObjectStore(DbRemoteDocument.store);
}
/**
* Represents the known absence of a document at a particular version.
* Stored in IndexedDb as part of a DbRemoteDocument object.
*/
var DbNoDocument = /** @class */ (function () {
function DbNoDocument(path, readTime) {
this.path = path;
this.readTime = readTime;
}
return DbNoDocument;
}());
/**
* Represents a document that is known to exist but whose data is unknown.
* Stored in IndexedDb as part of a DbRemoteDocument object.
*/
var DbUnknownDocument = /** @class */ (function () {
function DbUnknownDocument(path, version) {
this.path = path;
this.version = version;
}
return DbUnknownDocument;
}());
/**
* An object to be stored in the 'remoteDocuments' store in IndexedDb.
* It represents either:
*
* - A complete document.
* - A "no document" representing a document that is known not to exist (at
* some version).
* - An "unknown document" representing a document that is known to exist (at
* some version) but whose contents are unknown.
*
* Note: This is the persisted equivalent of a MaybeDocument and could perhaps
* be made more general if necessary.
*/
var DbRemoteDocument = /** @class */ (function () {
// TODO: We are currently storing full document keys almost three times
// (once as part of the primary key, once - partly - as `parentPath` and once
// inside the encoded documents). During our next migration, we should
// rewrite the primary key as parentPath + document ID which would allow us
// to drop one value.
function DbRemoteDocument(
/**
* Set to an instance of DbUnknownDocument if the data for a document is
* not known, but it is known that a document exists at the specified
* version (e.g. it had a successful update applied to it)
*/
unknownDocument,
/**
* Set to an instance of a DbNoDocument if it is known that no document
* exists.
*/
noDocument,
/**
* Set to an instance of a Document if there's a cached version of the
* document.
*/
document,
/**
* Documents that were written to the remote document store based on
* a write acknowledgment are marked with `hasCommittedMutations`. These
* documents are potentially inconsistent with the backend's copy and use
* the write's commit version as their document version.
*/
hasCommittedMutations,
/**
* When the document was read from the backend. Undefined for data written
* prior to schema version 9.
*/
readTime,
/**
* The path of the collection this document is part of. Undefined for data
* written prior to schema version 9.
*/
parentPath) {
this.unknownDocument = unknownDocument;
this.noDocument = noDocument;
this.document = document;
this.hasCommittedMutations = hasCommittedMutations;
this.readTime = readTime;
this.parentPath = parentPath;
}
return DbRemoteDocument;
}());
DbRemoteDocument.store = 'remoteDocuments';
/**
* An index that provides access to all entries sorted by read time (which
* corresponds to the last modification time of each row).
*
* This index is used to provide a changelog for Multi-Tab.
*/
DbRemoteDocument.readTimeIndex = 'readTimeIndex';
DbRemoteDocument.readTimeIndexPath = 'readTime';
/**
* An index that provides access to documents in a collection sorted by read
* time.
*
* This index is used to allow the RemoteDocumentCache to fetch newly changed
* documents in a collection.
*/
DbRemoteDocument.collectionReadTimeIndex = 'collectionReadTimeIndex';
DbRemoteDocument.collectionReadTimeIndexPath = ['parentPath', 'readTime'];
/**
* Contains a single entry that has metadata about the remote document cache.
*/
var DbRemoteDocumentGlobal = /** @class */ (function () {
/**
* @param byteSize Approximately the total size in bytes of all the documents in the document
* cache.
*/
function DbRemoteDocumentGlobal(byteSize) {
this.byteSize = byteSize;
}
return DbRemoteDocumentGlobal;
}());
DbRemoteDocumentGlobal.store = 'remoteDocumentGlobal';
DbRemoteDocumentGlobal.key = 'remoteDocumentGlobalKey';
function createDocumentGlobalStore(db) {
db.createObjectStore(DbRemoteDocumentGlobal.store);
}
/**
* An object to be stored in the 'targets' store in IndexedDb.
*
* This is based on and should be kept in sync with the proto used in the iOS
* client.
*
* Each query the client listens to against the server is tracked on disk so
* that the query can be efficiently resumed on restart.
*/
var DbTarget = /** @class */ (function () {
function DbTarget(
/**
* An auto-generated sequential numeric identifier for the query.
*
* Queries are stored using their canonicalId as the key, but these
* canonicalIds can be quite long so we additionally assign a unique
* queryId which can be used by referenced data structures (e.g.
* indexes) to minimize the on-disk cost.
*/
targetId,
/**
* The canonical string representing this query. This is not unique.
*/
canonicalId,
/**
* The last readTime received from the Watch Service for this query.
*
* This is the same value as TargetChange.read_time in the protos.
*/
readTime,
/**
* An opaque, server-assigned token that allows watching a query to be
* resumed after disconnecting without retransmitting all the data
* that matches the query. The resume token essentially identifies a
* point in time from which the server should resume sending results.
*
* This is related to the snapshotVersion in that the resumeToken
* effectively also encodes that value, but the resumeToken is opaque
* and sometimes encodes additional information.
*
* A consequence of this is that the resumeToken should be used when
* asking the server to reason about where this client is in the watch
* stream, but the client should use the snapshotVersion for its own
* purposes.
*
* This is the same value as TargetChange.resume_token in the protos.
*/
resumeToken,
/**
* A sequence number representing the last time this query was
* listened to, used for garbage collection purposes.
*
* Conventionally this would be a timestamp value, but device-local
* clocks are unreliable and they must be able to create new listens
* even while disconnected. Instead this should be a monotonically
* increasing number that's incremented on each listen call.
*
* This is different from the queryId since the queryId is an
* immutable identifier assigned to the Query on first use while
* lastListenSequenceNumber is updated every time the query is
* listened to.
*/
lastListenSequenceNumber,
/**
* Denotes the maximum snapshot version at which the associated query view
* contained no limbo documents. Undefined for data written prior to
* schema version 9.
*/
lastLimboFreeSnapshotVersion,
/**
* The query for this target.
*
* Because canonical ids are not unique we must store the actual query. We
* use the proto to have an object we can persist without having to
* duplicate translation logic to and from a `Query` object.
*/
query) {
this.targetId = targetId;
this.canonicalId = canonicalId;
this.readTime = readTime;
this.resumeToken = resumeToken;
this.lastListenSequenceNumber = lastListenSequenceNumber;
this.lastLimboFreeSnapshotVersion = lastLimboFreeSnapshotVersion;
this.query = query;
}
return DbTarget;
}());
DbTarget.store = 'targets';
/** Keys are automatically assigned via the targetId property. */
DbTarget.keyPath = 'targetId';
/** The name of the queryTargets index. */
DbTarget.queryTargetsIndexName = 'queryTargetsIndex';
/**
* The index of all canonicalIds to the targets that they match. This is not
* a unique mapping because canonicalId does not promise a unique name for all
* possible queries, so we append the targetId to make the mapping unique.
*/
DbTarget.queryTargetsKeyPath = ['canonicalId', 'targetId'];
/**
* An object representing an association between a target and a document, or a
* sentinel row marking the last sequence number at which a document was used.
* Each document cached must have a corresponding sentinel row before lru
* garbage collection is enabled.
*
* The target associations and sentinel rows are co-located so that orphaned
* documents and their sequence numbers can be identified efficiently via a scan
* of this store.
*/
var DbTargetDocument = /** @class */ (function () {
function DbTargetDocument(
/**
* The targetId identifying a target or 0 for a sentinel row.
*/
targetId,
/**
* The path to the document, as encoded in the key.
*/
path,
/**
* If this is a sentinel row, this should be the sequence number of the last
* time the document specified by `path` was used. Otherwise, it should be
* `undefined`.
*/
sequenceNumber) {
this.targetId = targetId;
this.path = path;
this.sequenceNumber = sequenceNumber;
debugAssert((targetId === 0) === (sequenceNumber !== undefined), 'A target-document row must either have targetId == 0 and a defined sequence number, or a non-zero targetId and no sequence number');
}
return DbTargetDocument;
}());
/** Name of the IndexedDb object store. */
DbTargetDocument.store = 'targetDocuments';
/** Keys are automatically assigned via the targetId, path properties. */
DbTargetDocument.keyPath = ['targetId', 'path'];
/** The index name for the reverse index. */
DbTargetDocument.documentTargetsIndex = 'documentTargetsIndex';
/** We also need to create the reverse index for these properties. */
DbTargetDocument.documentTargetsKeyPath = ['path', 'targetId'];
/**
* A record of global state tracked across all Targets, tracked separately
* to avoid the need for extra indexes.
*
* This should be kept in-sync with the proto used in the iOS client.
*/
var DbTargetGlobal = /** @class */ (function () {
function DbTargetGlobal(
/**
* The highest numbered target id across all targets.
*
* See DbTarget.targetId.
*/
highestTargetId,
/**
* The highest numbered lastListenSequenceNumber across all targets.
*
* See DbTarget.lastListenSequenceNumber.
*/
highestListenSequenceNumber,
/**
* A global snapshot version representing the last consistent snapshot we
* received from the backend. This is monotonically increasing and any
* snapshots received from the backend prior to this version (e.g. for
* targets resumed with a resumeToken) should be suppressed (buffered)
* until the backend has caught up to this snapshot version again. This
* prevents our cache from ever going backwards in time.
*/
lastRemoteSnapshotVersion,
/**
* The number of targets persisted.
*/
targetCount) {
this.highestTargetId = highestTargetId;
this.highestListenSequenceNumber = highestListenSequenceNumber;
this.lastRemoteSnapshotVersion = lastRemoteSnapshotVersion;
this.targetCount = targetCount;
}
return DbTargetGlobal;
}());
/**
* The key string used for the single object that exists in the
* DbTargetGlobal store.
*/
DbTargetGlobal.key = 'targetGlobalKey';
DbTargetGlobal.store = 'targetGlobal';
/**
* An object representing an association between a Collection id (e.g. 'messages')
* to a parent path (e.g. '/chats/123') that contains it as a (sub)collection.
* This is used to efficiently find all collections to query when performing
* a Collection Group query.
*/
var DbCollectionParent = /** @class */ (function () {
function DbCollectionParent(
/**
* The collectionId (e.g. 'messages')
*/
collectionId,
/**
* The path to the parent (either a document location or an empty path for
* a root-level collection).
*/
parent) {
this.collectionId = collectionId;
this.parent = parent;
}
return DbCollectionParent;
}());
/** Name of the IndexedDb object store. */
DbCollectionParent.store = 'collectionParents';
/** Keys are automatically assigned via the collectionId, parent properties. */
DbCollectionParent.keyPath = ['collectionId', 'parent'];
function createQueryCache(db) {
var targetDocumentsStore = db.createObjectStore(DbTargetDocument.store, {
keyPath: DbTargetDocument.keyPath
});
targetDocumentsStore.createIndex(DbTargetDocument.documentTargetsIndex, DbTargetDocument.documentTargetsKeyPath, { unique: true });
var targetStore = db.createObjectStore(DbTarget.store, {
keyPath: DbTarget.keyPath
});
// NOTE: This is unique only because the TargetId is the suffix.
targetStore.createIndex(DbTarget.queryTargetsIndexName, DbTarget.queryTargetsKeyPath, { unique: true });
db.createObjectStore(DbTargetGlobal.store);
}
function dropQueryCache(db) {
db.deleteObjectStore(DbTargetDocument.store);
db.deleteObjectStore(DbTarget.store);
db.deleteObjectStore(DbTargetGlobal.store);
}
function dropRemoteDocumentChangesStore(db) {
if (db.objectStoreNames.contains('remoteDocumentChanges')) {
db.deleteObjectStore('remoteDocumentChanges');
}
}
/**
* Creates the target global singleton row.
*
* @param {IDBTransaction} txn The version upgrade transaction for indexeddb
*/
function writeEmptyTargetGlobalEntry(txn) {
var globalStore = txn.store(DbTargetGlobal.store);
var metadata = new DbTargetGlobal(
/*highestTargetId=*/ 0,
/*lastListenSequenceNumber=*/ 0, SnapshotVersion.min().toTimestamp(),
/*targetCount=*/ 0);
return globalStore.put(DbTargetGlobal.key, metadata);
}
/**
* Creates indices on the RemoteDocuments store used for both multi-tab
* and Index-Free queries.
*/
function createRemoteDocumentReadTimeIndex(txn) {
var remoteDocumentStore = txn.objectStore(DbRemoteDocument.store);
remoteDocumentStore.createIndex(DbRemoteDocument.readTimeIndex, DbRemoteDocument.readTimeIndexPath, { unique: false });
remoteDocumentStore.createIndex(DbRemoteDocument.collectionReadTimeIndex, DbRemoteDocument.collectionReadTimeIndexPath, { unique: false });
}
/**
* A record of the metadata state of each client.
*
* PORTING NOTE: This is used to synchronize multi-tab state and does not need
* to be ported to iOS or Android.
*/
var DbClientMetadata = /** @class */ (function () {
function DbClientMetadata(
// Note: Previous schema versions included a field
// "lastProcessedDocumentChangeId". Don't use anymore.
/** The auto-generated client id assigned at client startup. */
clientId,
/** The last time this state was updated. */
updateTimeMs,
/** Whether the client's network connection is enabled. */
networkEnabled,
/** Whether this client is running in a foreground tab. */
inForeground) {
this.clientId = clientId;
this.updateTimeMs = updateTimeMs;
this.networkEnabled = networkEnabled;
this.inForeground = inForeground;
}
return DbClientMetadata;
}());
/** Name of the IndexedDb object store. */
DbClientMetadata.store = 'clientMetadata';
/** Keys are automatically assigned via the clientId properties. */
DbClientMetadata.keyPath = 'clientId';
function createClientMetadataStore(db) {
db.createObjectStore(DbClientMetadata.store, {
keyPath: DbClientMetadata.keyPath
});
}
// Visible for testing
var V1_STORES = [
DbMutationQueue.store,
DbMutationBatch.store,
DbDocumentMutation.store,
DbRemoteDocument.store,
DbTarget.store,
DbPrimaryClient.store,
DbTargetGlobal.store,
DbTargetDocument.store
];
// V2 is no longer usable (see comment at top of file)
// Visible for testing
var V3_STORES = V1_STORES;
// Visible for testing
// Note: DbRemoteDocumentChanges is no longer used and dropped with v9.
var V4_STORES = tslib.__spreadArrays(V3_STORES, [DbClientMetadata.store]);
// V5 does not change the set of stores.
var V6_STORES = tslib.__spreadArrays(V4_STORES, [DbRemoteDocumentGlobal.store]);
// V7 does not change the set of stores.
var V8_STORES = tslib.__spreadArrays(V6_STORES, [DbCollectionParent.store]);
// V9 does not change the set of stores.
// V10 does not change the set of stores.
/**
* The list of all default IndexedDB stores used throughout the SDK. This is
* used when creating transactions so that access across all stores is done
* atomically.
*/
var ALL_STORES = V8_STORES;
/**
* @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.
*/
// References to `window` are guarded by SimpleDb.isAvailable()
/* eslint-disable no-restricted-globals */
var LOG_TAG$3 = 'SimpleDb';
/**
* The maximum number of retry attempts for an IndexedDb transaction that fails
* with a DOMException.
*/
var TRANSACTION_RETRY_COUNT = 3;
/**
* Provides a wrapper around IndexedDb with a simplified interface that uses
* Promise-like return values to chain operations. Real promises cannot be used
* since .then() continuations are executed asynchronously (e.g. via
* .setImmediate), which would cause IndexedDB to end the transaction.
* See PersistencePromise for more details.
*/
var SimpleDb = /** @class */ (function () {
function SimpleDb(db) {
this.db = db;
var iOSVersion = SimpleDb.getIOSVersion(util.getUA());
// NOTE: According to https://bugs.webkit.org/show_bug.cgi?id=197050, the
// bug we're checking for should exist in iOS >= 12.2 and < 13, but for
// whatever reason it's much harder to hit after 12.2 so we only proactively
// log on 12.2.
if (iOSVersion === 12.2) {
logError('Firestore persistence suffers from a bug in iOS 12.2 ' +
'Safari that may cause your app to stop working. See ' +
'https://stackoverflow.com/q/56496296/110915 for details ' +
'and a potential workaround.');
}
}
/**
* Opens the specified database, creating or upgrading it if necessary.
*
* Note that `version` must not be a downgrade. IndexedDB does not support downgrading the schema
* version. We currently do not support any way to do versioning outside of IndexedDB's versioning
* mechanism, as only version-upgrade transactions are allowed to do things like create
* objectstores.
*/
SimpleDb.openOrCreate = function (name, version, schemaConverter) {
debugAssert(SimpleDb.isAvailable(), 'IndexedDB not supported in current environment.');
logDebug(LOG_TAG$3, 'Opening database:', name);
return new PersistencePromise(function (resolve, reject) {
// TODO(mikelehen): Investigate browser compatibility.
// https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB
// suggests IE9 and older WebKit browsers handle upgrade
// differently. They expect setVersion, as described here:
// https://developer.mozilla.org/en-US/docs/Web/API/IDBVersionChangeRequest/setVersion
var request = window.indexedDB.open(name, version);
request.onsuccess = function (event) {
var db = event.target.result;
resolve(new SimpleDb(db));
};
request.onblocked = function () {
reject(new FirestoreError(Code.FAILED_PRECONDITION, 'Cannot upgrade IndexedDB schema while another tab is open. ' +
'Close all tabs that access Firestore and reload this page to proceed.'));
};
request.onerror = function (event) {
var error = event.target.error;
if (error.name === 'VersionError') {
reject(new FirestoreError(Code.FAILED_PRECONDITION, 'A newer version of the Firestore SDK was previously used and so the persisted ' +
'data is not compatible with the version of the SDK you are now using. The SDK ' +
'will operate with persistence disabled. If you need persistence, please ' +
're-upgrade to a newer version of the SDK or else clear the persisted IndexedDB ' +
'data for your app to start fresh.'));
}
else {
reject(error);
}
};
request.onupgradeneeded = function (event) {
logDebug(LOG_TAG$3, 'Database "' + name + '" requires upgrade from version:', event.oldVersion);
var db = event.target.result;
schemaConverter
.createOrUpgrade(db, request.transaction, event.oldVersion, SCHEMA_VERSION)
.next(function () {
logDebug(LOG_TAG$3, 'Database upgrade to version ' + SCHEMA_VERSION + ' complete');
});
};
}).toPromise();
};
/** Deletes the specified database. */
SimpleDb.delete = function (name) {
logDebug(LOG_TAG$3, 'Removing database:', name);
return wrapRequest(window.indexedDB.deleteDatabase(name)).toPromise();
};
/** Returns true if IndexedDB is available in the current environment. */
SimpleDb.isAvailable = function () {
if (typeof window === 'undefined' || window.indexedDB == null) {
return false;
}
if (SimpleDb.isMockPersistence()) {
return true;
}
// In some Node environments, `window` is defined, but `window.navigator` is
// not. We don't support IndexedDB persistence in Node if the
// isMockPersistence() check above returns false.
if (window.navigator === undefined) {
return false;
}
// We extensively use indexed array values and compound keys,
// which IE and Edge do not support. However, they still have indexedDB
// defined on the window, so we need to check for them here and make sure
// to return that persistence is not enabled for those browsers.
// For tracking support of this feature, see here:
// https://developer.microsoft.com/en-us/microsoft-edge/platform/status/indexeddbarraysandmultientrysupport/
// Check the UA string to find out the browser.
var ua = util.getUA();
// IE 10
// ua = 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Trident/6.0)';
// IE 11
// ua = 'Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko';
// Edge
// ua = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML,
// like Gecko) Chrome/39.0.2171.71 Safari/537.36 Edge/12.0';
// iOS Safari: Disable for users running iOS version < 10.
var iOSVersion = SimpleDb.getIOSVersion(ua);
var isUnsupportedIOS = 0 < iOSVersion && iOSVersion < 10;
// Android browser: Disable for userse running version < 4.5.
var androidVersion = SimpleDb.getAndroidVersion(ua);
var isUnsupportedAndroid = 0 < androidVersion && androidVersion < 4.5;
if (ua.indexOf('MSIE ') > 0 ||
ua.indexOf('Trident/') > 0 ||
ua.indexOf('Edge/') > 0 ||
isUnsupportedIOS ||
isUnsupportedAndroid) {
return false;
}
else {
return true;
}
};
/**
* Returns true if the backing IndexedDB store is the Node IndexedDBShim
* (see https://github.com/axemclion/IndexedDBShim).
*/
SimpleDb.isMockPersistence = function () {
var _a;
return (typeof process !== 'undefined' &&
((_a = process.env) === null || _a === void 0 ? void 0 : _a.USE_MOCK_PERSISTENCE) === 'YES');
};
/** Helper to get a typed SimpleDbStore from a transaction. */
SimpleDb.getStore = function (txn, store) {
return txn.store(store);
};
// visible for testing
/** Parse User Agent to determine iOS version. Returns -1 if not found. */
SimpleDb.getIOSVersion = function (ua) {
var iOSVersionRegex = ua.match(/i(?:phone|pad|pod) os ([\d_]+)/i);
var version = iOSVersionRegex
? iOSVersionRegex[1]
.split('_')
.slice(0, 2)
.join('.')
: '-1';
return Number(version);
};
// visible for testing
/** Parse User Agent to determine Android version. Returns -1 if not found. */
SimpleDb.getAndroidVersion = function (ua) {
var androidVersionRegex = ua.match(/Android ([\d.]+)/i);
var version = androidVersionRegex
? androidVersionRegex[1]
.split('.')
.slice(0, 2)
.join('.')
: '-1';
return Number(version);
};
SimpleDb.prototype.setVersionChangeListener = function (versionChangeListener) {
this.db.onversionchange = function (event) {
return versionChangeListener(event);
};
};
SimpleDb.prototype.runTransaction = function (mode, objectStores, transactionFn) {
return tslib.__awaiter(this, void 0, void 0, function () {
var readonly, attemptNumber, _loop_2, this_1, state_1;
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
readonly = mode === 'readonly';
attemptNumber = 0;
_loop_2 = function () {
var transaction, transactionFnResult, error_1, retryable;
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
++attemptNumber;
transaction = SimpleDbTransaction.open(this_1.db, readonly ? 'readonly' : 'readwrite', objectStores);
_e.label = 1;
case 1:
_e.trys.push([1, 3, , 4]);
transactionFnResult = transactionFn(transaction)
.catch(function (error) {
// Abort the transaction if there was an error.
transaction.abort(error);
// We cannot actually recover, and calling `abort()` will cause the transaction's
// completion promise to be rejected. This in turn means that we won't use
// `transactionFnResult` below. We return a rejection here so that we don't add the
// possibility of returning `void` to the type of `transactionFnResult`.
return PersistencePromise.reject(error);
})
.toPromise();
// As noted above, errors are propagated by aborting the transaction. So
// we swallow any error here to avoid the browser logging it as unhandled.
transactionFnResult.catch(function () { });
// Wait for the transaction to complete (i.e. IndexedDb's onsuccess event to
// fire), but still return the original transactionFnResult back to the
// caller.
return [4 /*yield*/, transaction.completionPromise];
case 2:
// Wait for the transaction to complete (i.e. IndexedDb's onsuccess event to
// fire), but still return the original transactionFnResult back to the
// caller.
_e.sent();
return [2 /*return*/, { value: transactionFnResult }];
case 3:
error_1 = _e.sent();
retryable = error_1.name !== 'FirebaseError' &&
attemptNumber < TRANSACTION_RETRY_COUNT;
logDebug(LOG_TAG$3, 'Transaction failed with error: %s. Retrying: %s.', error_1.message, retryable);
if (!retryable) {
return [2 /*return*/, { value: Promise.reject(error_1) }];
}
return [3 /*break*/, 4];
case 4: return [2 /*return*/];
}
});
};
this_1 = this;
_e.label = 1;
case 1:
return [5 /*yield**/, _loop_2()];
case 2:
state_1 = _e.sent();
if (typeof state_1 === "object")
return [2 /*return*/, state_1.value];
return [3 /*break*/, 1];
case 3: return [2 /*return*/];
}
});
});
};
SimpleDb.prototype.close = function () {
this.db.close();
};
return SimpleDb;
}());
/**
* A controller for iterating over a key range or index. It allows an iterate
* callback to delete the currently-referenced object, or jump to a new key
* within the key range or index.
*/
var IterationController = /** @class */ (function () {
function IterationController(dbCursor) {
this.dbCursor = dbCursor;
this.shouldStop = false;
this.nextKey = null;
}
Object.defineProperty(IterationController.prototype, "isDone", {
get: function () {
return this.shouldStop;
},
enumerable: true,
configurable: true
});
Object.defineProperty(IterationController.prototype, "skipToKey", {
get: function () {
return this.nextKey;
},
enumerable: true,
configurable: true
});
Object.defineProperty(IterationController.prototype, "cursor", {
set: function (value) {
this.dbCursor = value;
},
enumerable: true,
configurable: true
});
/**
* This function can be called to stop iteration at any point.
*/
IterationController.prototype.done = function () {
this.shouldStop = true;
};
/**
* This function can be called to skip to that next key, which could be
* an index or a primary key.
*/
IterationController.prototype.skip = function (key) {
this.nextKey = key;
};
/**
* Delete the current cursor value from the object store.
*
* NOTE: You CANNOT do this with a keysOnly query.
*/
IterationController.prototype.delete = function () {
return wrapRequest(this.dbCursor.delete());
};
return IterationController;
}());
/** An error that wraps exceptions that thrown during IndexedDB execution. */
var IndexedDbTransactionError = /** @class */ (function (_super) {
tslib.__extends(IndexedDbTransactionError, _super);
function IndexedDbTransactionError(cause) {
var _this = _super.call(this, Code.UNAVAILABLE, 'IndexedDB transaction failed: ' + cause) || this;
_this.name = 'IndexedDbTransactionError';
return _this;
}
return IndexedDbTransactionError;
}(FirestoreError));
/** Verifies whether `e` is an IndexedDbTransactionError. */
function isIndexedDbTransactionError(e) {
// Use name equality, as instanceof checks on errors don't work with errors
// that wrap other errors.
return e.name === 'IndexedDbTransactionError';
}
/**
* Wraps an IDBTransaction and exposes a store() method to get a handle to a
* specific object store.
*/
var SimpleDbTransaction = /** @class */ (function () {
function SimpleDbTransaction(transaction) {
var _this = this;
this.transaction = transaction;
this.aborted = false;
/**
* A promise that resolves with the result of the IndexedDb transaction.
*/
this.completionDeferred = new Deferred();
this.transaction.oncomplete = function () {
_this.completionDeferred.resolve();
};
this.transaction.onabort = function () {
if (transaction.error) {
_this.completionDeferred.reject(new IndexedDbTransactionError(transaction.error));
}
else {
_this.completionDeferred.resolve();
}
};
this.transaction.onerror = function (event) {
var error = checkForAndReportiOSError(event.target.error);
_this.completionDeferred.reject(new IndexedDbTransactionError(error));
};
}
SimpleDbTransaction.open = function (db, mode, objectStoreNames) {
return new SimpleDbTransaction(db.transaction(objectStoreNames, mode));
};
Object.defineProperty(SimpleDbTransaction.prototype, "completionPromise", {
get: function () {
return this.completionDeferred.promise;
},
enumerable: true,
configurable: true
});
SimpleDbTransaction.prototype.abort = function (error) {
if (error) {
this.completionDeferred.reject(error);
}
if (!this.aborted) {
logDebug(LOG_TAG$3, 'Aborting transaction:', error ? error.message : 'Client-initiated abort');
this.aborted = true;
this.transaction.abort();
}
};
/**
* Returns a SimpleDbStore<KeyType, ValueType> for the specified store. All
* operations performed on the SimpleDbStore happen within the context of this
* transaction and it cannot be used anymore once the transaction is
* completed.
*
* Note that we can't actually enforce that the KeyType and ValueType are
* correct, but they allow type safety through the rest of the consuming code.
*/
SimpleDbTransaction.prototype.store = function (storeName) {
var store = this.transaction.objectStore(storeName);
debugAssert(!!store, 'Object store not part of transaction: ' + storeName);
return new SimpleDbStore(store);
};
return SimpleDbTransaction;
}());
/**
* A wrapper around an IDBObjectStore providing an API that:
*
* 1) Has generic KeyType / ValueType parameters to provide strongly-typed
* methods for acting against the object store.
* 2) Deals with IndexedDB's onsuccess / onerror event callbacks, making every
* method return a PersistencePromise instead.
* 3) Provides a higher-level API to avoid needing to do excessive wrapping of
* intermediate IndexedDB types (IDBCursorWithValue, etc.)
*/
var SimpleDbStore = /** @class */ (function () {
function SimpleDbStore(store) {
this.store = store;
}
SimpleDbStore.prototype.put = function (keyOrValue, value) {
var request;
if (value !== undefined) {
logDebug(LOG_TAG$3, 'PUT', this.store.name, keyOrValue, value);
request = this.store.put(value, keyOrValue);
}
else {
logDebug(LOG_TAG$3, 'PUT', this.store.name, '<auto-key>', keyOrValue);
request = this.store.put(keyOrValue);
}
return wrapRequest(request);
};
/**
* Adds a new value into an Object Store and returns the new key. Similar to
* IndexedDb's `add()`, this method will fail on primary key collisions.
*
* @param value The object to write.
* @return The key of the value to add.
*/
SimpleDbStore.prototype.add = function (value) {
logDebug(LOG_TAG$3, 'ADD', this.store.name, value, value);
var request = this.store.add(value);
return wrapRequest(request);
};
/**
* Gets the object with the specified key from the specified store, or null
* if no object exists with the specified key.
*
* @key The key of the object to get.
* @return The object with the specified key or null if no object exists.
*/
SimpleDbStore.prototype.get = function (key) {
var _this = this;
var request = this.store.get(key);
// We're doing an unsafe cast to ValueType.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return wrapRequest(request).next(function (result) {
// Normalize nonexistence to null.
if (result === undefined) {
result = null;
}
logDebug(LOG_TAG$3, 'GET', _this.store.name, key, result);
return result;
});
};
SimpleDbStore.prototype.delete = function (key) {
logDebug(LOG_TAG$3, 'DELETE', this.store.name, key);
var request = this.store.delete(key);
return wrapRequest(request);
};
/**
* If we ever need more of the count variants, we can add overloads. For now,
* all we need is to count everything in a store.
*
* Returns the number of rows in the store.
*/
SimpleDbStore.prototype.count = function () {
logDebug(LOG_TAG$3, 'COUNT', this.store.name);
var request = this.store.count();
return wrapRequest(request);
};
SimpleDbStore.prototype.loadAll = function (indexOrRange, range) {
var cursor = this.cursor(this.options(indexOrRange, range));
var results = [];
return this.iterateCursor(cursor, function (key, value) {
results.push(value);
}).next(function () {
return results;
});
};
SimpleDbStore.prototype.deleteAll = function (indexOrRange, range) {
logDebug(LOG_TAG$3, 'DELETE ALL', this.store.name);
var options = this.options(indexOrRange, range);
options.keysOnly = false;
var cursor = this.cursor(options);
return this.iterateCursor(cursor, function (key, value, control) {
// NOTE: Calling delete() on a cursor is documented as more efficient than
// calling delete() on an object store with a single key
// (https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/delete),
// however, this requires us *not* to use a keysOnly cursor
// (https://developer.mozilla.org/en-US/docs/Web/API/IDBCursor/delete). We
// may want to compare the performance of each method.
return control.delete();
});
};
SimpleDbStore.prototype.iterate = function (optionsOrCallback, callback) {
var options;
if (!callback) {
options = {};
callback = optionsOrCallback;
}
else {
options = optionsOrCallback;
}
var cursor = this.cursor(options);
return this.iterateCursor(cursor, callback);
};
/**
* Iterates over a store, but waits for the given callback to complete for
* each entry before iterating the next entry. This allows the callback to do
* asynchronous work to determine if this iteration should continue.
*
* The provided callback should return `true` to continue iteration, and
* `false` otherwise.
*/
SimpleDbStore.prototype.iterateSerial = function (callback) {
var cursorRequest = this.cursor({});
return new PersistencePromise(function (resolve, reject) {
cursorRequest.onerror = function (event) {
var error = checkForAndReportiOSError(event.target.error);
reject(error);
};
cursorRequest.onsuccess = function (event) {
var cursor = event.target.result;
if (!cursor) {
resolve();
return;
}
callback(cursor.primaryKey, cursor.value).next(function (shouldContinue) {
if (shouldContinue) {
cursor.continue();
}
else {
resolve();
}
});
};
});
};
SimpleDbStore.prototype.iterateCursor = function (cursorRequest, fn) {
var results = [];
return new PersistencePromise(function (resolve, reject) {
cursorRequest.onerror = function (event) {
reject(event.target.error);
};
cursorRequest.onsuccess = function (event) {
var cursor = event.target.result;
if (!cursor) {
resolve();
return;
}
var controller = new IterationController(cursor);
var userResult = fn(cursor.primaryKey, cursor.value, controller);
if (userResult instanceof PersistencePromise) {
var userPromise = userResult.catch(function (err) {
controller.done();
return PersistencePromise.reject(err);
});
results.push(userPromise);
}
if (controller.isDone) {
resolve();
}
else if (controller.skipToKey === null) {
cursor.continue();
}
else {
cursor.continue(controller.skipToKey);
}
};
}).next(function () {
return PersistencePromise.waitFor(results);
});
};
SimpleDbStore.prototype.options = function (indexOrRange, range) {
var indexName = undefined;
if (indexOrRange !== undefined) {
if (typeof indexOrRange === 'string') {
indexName = indexOrRange;
}
else {
debugAssert(range === undefined, '3rd argument must not be defined if 2nd is a range.');
range = indexOrRange;
}
}
return { index: indexName, range: range };
};
SimpleDbStore.prototype.cursor = function (options) {
var direction = 'next';
if (options.reverse) {
direction = 'prev';
}
if (options.index) {
var index = this.store.index(options.index);
if (options.keysOnly) {
return index.openKeyCursor(options.range, direction);
}
else {
return index.openCursor(options.range, direction);
}
}
else {
return this.store.openCursor(options.range, direction);
}
};
return SimpleDbStore;
}());
/**
* Wraps an IDBRequest in a PersistencePromise, using the onsuccess / onerror
* handlers to resolve / reject the PersistencePromise as appropriate.
*/
function wrapRequest(request) {
return new PersistencePromise(function (resolve, reject) {
request.onsuccess = function (event) {
var result = event.target.result;
resolve(result);
};
request.onerror = function (event) {
var error = checkForAndReportiOSError(event.target.error);
reject(error);
};
});
}
// Guard so we only report the error once.
var reportedIOSError = false;
function checkForAndReportiOSError(error) {
var iOSVersion = SimpleDb.getIOSVersion(util.getUA());
if (iOSVersion >= 12.2 && iOSVersion < 13) {
var IOS_ERROR = 'An internal error was encountered in the Indexed Database server';
if (error.message.indexOf(IOS_ERROR) >= 0) {
// Wrap error in a more descriptive one.
var newError_1 = new FirestoreError('internal', "IOS_INDEXEDDB_BUG1: IndexedDb has thrown '" + IOS_ERROR + "'. This is likely " +
"due to an unavoidable bug in iOS. See https://stackoverflow.com/q/56496296/110915 " +
"for details and a potential workaround.");
if (!reportedIOSError) {
reportedIOSError = true;
// Throw a global exception outside of this promise chain, for the user to
// potentially catch.
setTimeout(function () {
throw newError_1;
}, 0);
}
return newError_1;
}
}
return 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.
*/
var LOG_TAG$4 = 'LocalStore';
/**
* Local storage in the Firestore client. Coordinates persistence components
* like the mutation queue and remote document cache to present a
* latency-compensated view of stored data.
*
* The LocalStore is responsible for accepting mutations from the Sync Engine.
* Writes from the client are put into a queue as provisional Mutations until
* they are processed by the RemoteStore and confirmed as having been written
* to the server.
*
* The local store provides the local version of documents that have been
* modified locally. It maintains the constraint:
*
* LocalDocument = RemoteDocument + Active(LocalMutations)
*
* (Active mutations are those that are enqueued and have not been previously
* acknowledged or rejected).
*
* The RemoteDocument ("ground truth") state is provided via the
* applyChangeBatch method. It will be some version of a server-provided
* document OR will be a server-provided document PLUS acknowledged mutations:
*
* RemoteDocument' = RemoteDocument + Acknowledged(LocalMutations)
*
* Note that this "dirty" version of a RemoteDocument will not be identical to a
* server base version, since it has LocalMutations added to it pending getting
* an authoritative copy from the server.
*
* Since LocalMutations can be rejected by the server, we have to be able to
* revert a LocalMutation that has already been applied to the LocalDocument
* (typically done by replaying all remaining LocalMutations to the
* RemoteDocument to re-apply).
*
* The LocalStore is responsible for the garbage collection of the documents it
* contains. For now, it every doc referenced by a view, the mutation queue, or
* the RemoteStore.
*
* It also maintains the persistence of mapping queries to resume tokens and
* target ids. It needs to know this data about queries to properly know what
* docs it would be allowed to garbage collect.
*
* The LocalStore must be able to efficiently execute queries against its local
* cache of the documents, to provide the initial set of results before any
* remote changes have been received.
*
* Note: In TypeScript, most methods return Promises since the implementation
* may rely on fetching data from IndexedDB which is async.
* These Promises will only be rejected on an I/O error or other internal
* (unexpected) failure (e.g. failed assert) and always represent an
* unrecoverable error (should be caught / reported by the async_queue).
*/
var LocalStore = /** @class */ (function () {
function LocalStore(
/** Manages our in-memory or durable persistence. */
persistence, queryEngine, initialUser) {
this.persistence = persistence;
this.queryEngine = queryEngine;
/**
* Maps a targetID to data about its target.
*
* PORTING NOTE: We are using an immutable data structure on Web to make re-runs
* of `applyRemoteEvent()` idempotent.
*/
this.targetDataByTarget = new SortedMap(primitiveComparator);
/** Maps a target to its targetID. */
// TODO(wuandy): Evaluate if TargetId can be part of Target.
this.targetIdByTarget = new ObjectMap(function (t) { return t.canonicalId(); });
/**
* The read time of the last entry processed by `getNewDocumentChanges()`.
*
* PORTING NOTE: This is only used for multi-tab synchronization.
*/
this.lastDocumentChangeReadTime = SnapshotVersion.min();
debugAssert(persistence.started, 'LocalStore was passed an unstarted persistence implementation');
this.mutationQueue = persistence.getMutationQueue(initialUser);
this.remoteDocuments = persistence.getRemoteDocumentCache();
this.targetCache = persistence.getTargetCache();
this.localDocuments = new LocalDocumentsView(this.remoteDocuments, this.mutationQueue, this.persistence.getIndexManager());
this.queryEngine.setLocalDocumentsView(this.localDocuments);
}
/** Starts the LocalStore. */
LocalStore.prototype.start = function () {
return Promise.resolve();
};
/**
* Tells the LocalStore that the currently authenticated user has changed.
*
* In response the local store switches the mutation queue to the new user and
* returns any resulting document changes.
*/
// PORTING NOTE: Android and iOS only return the documents affected by the
// change.
LocalStore.prototype.handleUserChange = function (user) {
return tslib.__awaiter(this, void 0, void 0, function () {
var newMutationQueue, newLocalDocuments, result;
var _this = this;
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
newMutationQueue = this.mutationQueue;
newLocalDocuments = this.localDocuments;
return [4 /*yield*/, this.persistence.runTransaction('Handle user change', 'readonly', function (txn) {
// Swap out the mutation queue, grabbing the pending mutation batches
// before and after.
var oldBatches;
return _this.mutationQueue
.getAllMutationBatches(txn)
.next(function (promisedOldBatches) {
oldBatches = promisedOldBatches;
newMutationQueue = _this.persistence.getMutationQueue(user);
// Recreate our LocalDocumentsView using the new
// MutationQueue.
newLocalDocuments = new LocalDocumentsView(_this.remoteDocuments, newMutationQueue, _this.persistence.getIndexManager());
return newMutationQueue.getAllMutationBatches(txn);
})
.next(function (newBatches) {
var removedBatchIds = [];
var addedBatchIds = [];
// Union the old/new changed keys.
var changedKeys = documentKeySet();
for (var _i = 0, oldBatches_1 = oldBatches; _i < oldBatches_1.length; _i++) {
var batch = oldBatches_1[_i];
removedBatchIds.push(batch.batchId);
for (var _e = 0, _f = batch.mutations; _e < _f.length; _e++) {
var mutation = _f[_e];
changedKeys = changedKeys.add(mutation.key);
}
}
for (var _g = 0, newBatches_1 = newBatches; _g < newBatches_1.length; _g++) {
var batch = newBatches_1[_g];
addedBatchIds.push(batch.batchId);
for (var _h = 0, _j = batch.mutations; _h < _j.length; _h++) {
var mutation = _j[_h];
changedKeys = changedKeys.add(mutation.key);
}
}
// Return the set of all (potentially) changed documents and the list
// of mutation batch IDs that were affected by change.
return newLocalDocuments
.getDocuments(txn, changedKeys)
.next(function (affectedDocuments) {
return {
affectedDocuments: affectedDocuments,
removedBatchIds: removedBatchIds,
addedBatchIds: addedBatchIds
};
});
});
})];
case 1:
result = _e.sent();
this.mutationQueue = newMutationQueue;
this.localDocuments = newLocalDocuments;
this.queryEngine.setLocalDocumentsView(this.localDocuments);
return [2 /*return*/, result];
}
});
});
};
/* Accept locally generated Mutations and commit them to storage. */
LocalStore.prototype.localWrite = function (mutations) {
var _this = this;
var localWriteTime = Timestamp.now();
var keys = mutations.reduce(function (keys, m) { return keys.add(m.key); }, documentKeySet());
var existingDocs;
return this.persistence
.runTransaction('Locally write mutations', 'readwrite', function (txn) {
// Load and apply all existing mutations. This lets us compute the
// current base state for all non-idempotent transforms before applying
// any additional user-provided writes.
return _this.localDocuments.getDocuments(txn, keys).next(function (docs) {
existingDocs = docs;
// For non-idempotent mutations (such as `FieldValue.increment()`),
// we record the base state in a separate patch mutation. This is
// later used to guarantee consistent values and prevents flicker
// even if the backend sends us an update that already includes our
// transform.
var baseMutations = [];
for (var _i = 0, mutations_2 = mutations; _i < mutations_2.length; _i++) {
var mutation = mutations_2[_i];
var baseValue = mutation.extractBaseValue(existingDocs.get(mutation.key));
if (baseValue != null) {
// NOTE: The base state should only be applied if there's some
// existing document to override, so use a Precondition of
// exists=true
baseMutations.push(new PatchMutation(mutation.key, baseValue, extractFieldMask(baseValue.proto.mapValue), Precondition.exists(true)));
}
}
return _this.mutationQueue.addMutationBatch(txn, localWriteTime, baseMutations, mutations);
});
})
.then(function (batch) {
var changes = batch.applyToLocalDocumentSet(existingDocs);
return { batchId: batch.batchId, changes: changes };
});
};
/**
* Acknowledge the given batch.
*
* On the happy path when a batch is acknowledged, the local store will
*
* + remove the batch from the mutation queue;
* + apply the changes to the remote document cache;
* + recalculate the latency compensated view implied by those changes (there
* may be mutations in the queue that affect the documents but haven't been
* acknowledged yet); and
* + give the changed documents back the sync engine
*
* @returns The resulting (modified) documents.
*/
LocalStore.prototype.acknowledgeBatch = function (batchResult) {
var _this = this;
return this.persistence.runTransaction('Acknowledge batch', 'readwrite-primary', function (txn) {
var affected = batchResult.batch.keys();
var documentBuffer = _this.remoteDocuments.newChangeBuffer({
trackRemovals: true // Make sure document removals show up in `getNewDocumentChanges()`
});
return _this.mutationQueue
.acknowledgeBatch(txn, batchResult.batch, batchResult.streamToken)
.next(function () { return _this.applyWriteToRemoteDocuments(txn, batchResult, documentBuffer); })
.next(function () { return documentBuffer.apply(txn); })
.next(function () { return _this.mutationQueue.performConsistencyCheck(txn); })
.next(function () { return _this.localDocuments.getDocuments(txn, affected); });
});
};
/**
* Remove mutations from the MutationQueue for the specified batch;
* LocalDocuments will be recalculated.
*
* @returns The resulting modified documents.
*/
LocalStore.prototype.rejectBatch = function (batchId) {
var _this = this;
return this.persistence.runTransaction('Reject batch', 'readwrite-primary', function (txn) {
var affectedKeys;
return _this.mutationQueue
.lookupMutationBatch(txn, batchId)
.next(function (batch) {
hardAssert(batch !== null, 'Attempt to reject nonexistent batch!');
affectedKeys = batch.keys();
return _this.mutationQueue.removeMutationBatch(txn, batch);
})
.next(function () {
return _this.mutationQueue.performConsistencyCheck(txn);
})
.next(function () {
return _this.localDocuments.getDocuments(txn, affectedKeys);
});
});
};
/**
* Returns the largest (latest) batch id in mutation queue that is pending server response.
* Returns `BATCHID_UNKNOWN` if the queue is empty.
*/
LocalStore.prototype.getHighestUnacknowledgedBatchId = function () {
var _this = this;
return this.persistence.runTransaction('Get highest unacknowledged batch id', 'readonly', function (txn) {
return _this.mutationQueue.getHighestUnacknowledgedBatchId(txn);
});
};
/** Returns the last recorded stream token for the current user. */
LocalStore.prototype.getLastStreamToken = function () {
var _this = this;
return this.persistence.runTransaction('Get last stream token', 'readonly', function (txn) {
return _this.mutationQueue.getLastStreamToken(txn);
});
};
/**
* Sets the stream token for the current user without acknowledging any
* mutation batch. This is usually only useful after a stream handshake or in
* response to an error that requires clearing the stream token.
*/
LocalStore.prototype.setLastStreamToken = function (streamToken) {
var _this = this;
return this.persistence.runTransaction('Set last stream token', 'readwrite-primary', function (txn) {
return _this.mutationQueue.setLastStreamToken(txn, streamToken);
});
};
/**
* Returns the last consistent snapshot processed (used by the RemoteStore to
* determine whether to buffer incoming snapshots from the backend).
*/
LocalStore.prototype.getLastRemoteSnapshotVersion = function () {
var _this = this;
return this.persistence.runTransaction('Get last remote snapshot version', 'readonly', function (txn) { return _this.targetCache.getLastRemoteSnapshotVersion(txn); });
};
/**
* Update the "ground-state" (remote) documents. We assume that the remote
* event reflects any write batches that have been acknowledged or rejected
* (i.e. we do not re-apply local mutations to updates from this event).
*
* LocalDocuments are re-calculated if there are remaining mutations in the
* queue.
*/
LocalStore.prototype.applyRemoteEvent = function (remoteEvent) {
var _this = this;
var remoteVersion = remoteEvent.snapshotVersion;
var newTargetDataByTargetMap = this.targetDataByTarget;
return this.persistence
.runTransaction('Apply remote event', 'readwrite-primary', function (txn) {
var documentBuffer = _this.remoteDocuments.newChangeBuffer({
trackRemovals: true // Make sure document removals show up in `getNewDocumentChanges()`
});
// Reset newTargetDataByTargetMap in case this transaction gets re-run.
newTargetDataByTargetMap = _this.targetDataByTarget;
var promises = [];
remoteEvent.targetChanges.forEach(function (change, targetId) {
var oldTargetData = newTargetDataByTargetMap.get(targetId);
if (!oldTargetData) {
return;
}
// Only update the remote keys if the target is still active. This
// ensures that we can persist the updated target data along with
// the updated assignment.
promises.push(_this.targetCache
.removeMatchingKeys(txn, change.removedDocuments, targetId)
.next(function () {
return _this.targetCache.addMatchingKeys(txn, change.addedDocuments, targetId);
}));
var resumeToken = change.resumeToken;
// Update the resume token if the change includes one.
if (resumeToken.approximateByteSize() > 0) {
var newTargetData = oldTargetData
.withResumeToken(resumeToken, remoteVersion)
.withSequenceNumber(txn.currentSequenceNumber);
newTargetDataByTargetMap = newTargetDataByTargetMap.insert(targetId, newTargetData);
// Update the target data if there are target changes (or if
// sufficient time has passed since the last update).
if (LocalStore.shouldPersistTargetData(oldTargetData, newTargetData, change)) {
promises.push(_this.targetCache.updateTargetData(txn, newTargetData));
}
}
});
var changedDocs = maybeDocumentMap();
var updatedKeys = documentKeySet();
remoteEvent.documentUpdates.forEach(function (key, doc) {
updatedKeys = updatedKeys.add(key);
});
// Each loop iteration only affects its "own" doc, so it's safe to get all the remote
// documents in advance in a single call.
promises.push(documentBuffer.getEntries(txn, updatedKeys).next(function (existingDocs) {
remoteEvent.documentUpdates.forEach(function (key, doc) {
var existingDoc = existingDocs.get(key);
// Note: The order of the steps below is important, since we want
// to ensure that rejected limbo resolutions (which fabricate
// NoDocuments with SnapshotVersion.min()) never add documents to
// cache.
if (doc instanceof NoDocument &&
doc.version.isEqual(SnapshotVersion.min())) {
// NoDocuments with SnapshotVersion.min() are used in manufactured
// events. We remove these documents from cache since we lost
// access.
documentBuffer.removeEntry(key, remoteVersion);
changedDocs = changedDocs.insert(key, doc);
}
else if (existingDoc == null ||
doc.version.compareTo(existingDoc.version) > 0 ||
(doc.version.compareTo(existingDoc.version) === 0 &&
existingDoc.hasPendingWrites)) {
debugAssert(!SnapshotVersion.min().isEqual(remoteVersion), 'Cannot add a document when the remote version is zero');
documentBuffer.addEntry(doc, remoteVersion);
changedDocs = changedDocs.insert(key, doc);
}
else {
logDebug(LOG_TAG$4, 'Ignoring outdated watch update for ', key, '. Current version:', existingDoc.version, ' Watch version:', doc.version);
}
if (remoteEvent.resolvedLimboDocuments.has(key)) {
promises.push(_this.persistence.referenceDelegate.updateLimboDocument(txn, key));
}
});
}));
// HACK: The only reason we allow a null snapshot version is so that we
// can synthesize remote events when we get permission denied errors while
// trying to resolve the state of a locally cached document that is in
// limbo.
if (!remoteVersion.isEqual(SnapshotVersion.min())) {
var updateRemoteVersion = _this.targetCache
.getLastRemoteSnapshotVersion(txn)
.next(function (lastRemoteSnapshotVersion) {
debugAssert(remoteVersion.compareTo(lastRemoteSnapshotVersion) >= 0, 'Watch stream reverted to previous snapshot?? ' +
remoteVersion +
' < ' +
lastRemoteSnapshotVersion);
return _this.targetCache.setTargetsMetadata(txn, txn.currentSequenceNumber, remoteVersion);
});
promises.push(updateRemoteVersion);
}
return PersistencePromise.waitFor(promises)
.next(function () { return documentBuffer.apply(txn); })
.next(function () {
return _this.localDocuments.getLocalViewOfDocuments(txn, changedDocs);
});
})
.then(function (changedDocs) {
_this.targetDataByTarget = newTargetDataByTargetMap;
return changedDocs;
});
};
/**
* Returns true if the newTargetData should be persisted during an update of
* an active target. TargetData should always be persisted when a target is
* being released and should not call this function.
*
* While the target is active, TargetData updates can be omitted when nothing
* about the target has changed except metadata like the resume token or
* snapshot version. Occasionally it's worth the extra write to prevent these
* values from getting too stale after a crash, but this doesn't have to be
* too frequent.
*/
LocalStore.shouldPersistTargetData = function (oldTargetData, newTargetData, change) {
hardAssert(newTargetData.resumeToken.approximateByteSize() > 0, 'Attempted to persist target data with no resume token');
// Always persist target data if we don't already have a resume token.
if (oldTargetData.resumeToken.approximateByteSize() === 0) {
return true;
}
// Don't allow resume token changes to be buffered indefinitely. This
// allows us to be reasonably up-to-date after a crash and avoids needing
// to loop over all active queries on shutdown. Especially in the browser
// we may not get time to do anything interesting while the current tab is
// closing.
var timeDelta = newTargetData.snapshotVersion.toMicroseconds() -
oldTargetData.snapshotVersion.toMicroseconds();
if (timeDelta >= this.RESUME_TOKEN_MAX_AGE_MICROS) {
return true;
}
// Otherwise if the only thing that has changed about a target is its resume
// token it's not worth persisting. Note that the RemoteStore keeps an
// in-memory view of the currently active targets which includes the current
// resume token, so stream failure or user changes will still use an
// up-to-date resume token regardless of what we do here.
var changes = change.addedDocuments.size +
change.modifiedDocuments.size +
change.removedDocuments.size;
return changes > 0;
};
/**
* Notify local store of the changed views to locally pin documents.
*/
LocalStore.prototype.notifyLocalViewChanges = function (viewChanges) {
return tslib.__awaiter(this, void 0, void 0, function () {
var e_2, _i, viewChanges_1, viewChange, targetId, targetData, lastLimboFreeSnapshotVersion, updatedTargetData;
var _this = this;
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
_e.trys.push([0, 2, , 3]);
return [4 /*yield*/, this.persistence.runTransaction('notifyLocalViewChanges', 'readwrite', function (txn) {
return PersistencePromise.forEach(viewChanges, function (viewChange) {
return PersistencePromise.forEach(viewChange.addedKeys, function (key) { return _this.persistence.referenceDelegate.addReference(txn, viewChange.targetId, key); }).next(function () { return PersistencePromise.forEach(viewChange.removedKeys, function (key) { return _this.persistence.referenceDelegate.removeReference(txn, viewChange.targetId, key); }); });
});
})];
case 1:
_e.sent();
return [3 /*break*/, 3];
case 2:
e_2 = _e.sent();
if (isIndexedDbTransactionError(e_2)) {
// If `notifyLocalViewChanges` fails, we did not advance the sequence
// number for the documents that were included in this transaction.
// This might trigger them to be deleted earlier than they otherwise
// would have, but it should not invalidate the integrity of the data.
logDebug(LOG_TAG$4, 'Failed to update sequence numbers: ' + e_2);
}
else {
throw e_2;
}
return [3 /*break*/, 3];
case 3:
for (_i = 0, viewChanges_1 = viewChanges; _i < viewChanges_1.length; _i++) {
viewChange = viewChanges_1[_i];
targetId = viewChange.targetId;
if (!viewChange.fromCache) {
targetData = this.targetDataByTarget.get(targetId);
debugAssert(targetData !== null, "Can't set limbo-free snapshot version for unknown target: " + targetId);
lastLimboFreeSnapshotVersion = targetData.snapshotVersion;
updatedTargetData = targetData.withLastLimboFreeSnapshotVersion(lastLimboFreeSnapshotVersion);
this.targetDataByTarget = this.targetDataByTarget.insert(targetId, updatedTargetData);
}
}
return [2 /*return*/];
}
});
});
};
/**
* Gets the mutation batch after the passed in batchId in the mutation queue
* or null if empty.
* @param afterBatchId If provided, the batch to search after.
* @returns The next mutation or null if there wasn't one.
*/
LocalStore.prototype.nextMutationBatch = function (afterBatchId) {
var _this = this;
return this.persistence.runTransaction('Get next mutation batch', 'readonly', function (txn) {
if (afterBatchId === undefined) {
afterBatchId = BATCHID_UNKNOWN;
}
return _this.mutationQueue.getNextMutationBatchAfterBatchId(txn, afterBatchId);
});
};
/**
* Read the current value of a Document with a given key or null if not
* found - used for testing.
*/
LocalStore.prototype.readDocument = function (key) {
var _this = this;
return this.persistence.runTransaction('read document', 'readonly', function (txn) {
return _this.localDocuments.getDocument(txn, key);
});
};
/**
* Assigns the given target an internal ID so that its results can be pinned so
* they don't get GC'd. A target must be allocated in the local store before
* the store can be used to manage its view.
*
* Allocating an already allocated `Target` will return the existing `TargetData`
* for that `Target`.
*/
LocalStore.prototype.allocateTarget = function (target) {
var _this = this;
return this.persistence
.runTransaction('Allocate target', 'readwrite', function (txn) {
var targetData;
return _this.targetCache
.getTargetData(txn, target)
.next(function (cached) {
if (cached) {
// This target has been listened to previously, so reuse the
// previous targetID.
// TODO(mcg): freshen last accessed date?
targetData = cached;
return PersistencePromise.resolve(targetData);
}
else {
return _this.targetCache.allocateTargetId(txn).next(function (targetId) {
targetData = new TargetData(target, targetId, 0 /* Listen */, txn.currentSequenceNumber);
return _this.targetCache
.addTargetData(txn, targetData)
.next(function () { return targetData; });
});
}
});
})
.then(function (targetData) {
if (_this.targetDataByTarget.get(targetData.targetId) === null) {
_this.targetDataByTarget = _this.targetDataByTarget.insert(targetData.targetId, targetData);
_this.targetIdByTarget.set(target, targetData.targetId);
}
return targetData;
});
};
/**
* Returns the TargetData as seen by the LocalStore, including updates that may
* have not yet been persisted to the TargetCache.
*/
// Visible for testing.
LocalStore.prototype.getTargetData = function (transaction, target) {
var targetId = this.targetIdByTarget.get(target);
if (targetId !== undefined) {
return PersistencePromise.resolve(this.targetDataByTarget.get(targetId));
}
else {
return this.targetCache.getTargetData(transaction, target);
}
};
/**
* Unpin all the documents associated with the given target. If
* `keepPersistedTargetData` is set to false and Eager GC enabled, the method
* directly removes the associated target data from the target cache.
*
* Releasing a non-existing `Target` is a no-op.
*/
// PORTING NOTE: `keepPersistedTargetData` is multi-tab only.
LocalStore.prototype.releaseTarget = function (targetId, keepPersistedTargetData) {
var _this = this;
var targetData = this.targetDataByTarget.get(targetId);
debugAssert(targetData !== null, "Tried to release nonexistent target: " + targetId);
var mode = keepPersistedTargetData ? 'readwrite' : 'readwrite-primary';
return this.persistence
.runTransaction('Release target', mode, function (txn) {
if (!keepPersistedTargetData) {
return _this.persistence.referenceDelegate.removeTarget(txn, targetData);
}
else {
return PersistencePromise.resolve();
}
})
.then(function () {
_this.targetDataByTarget = _this.targetDataByTarget.remove(targetId);
_this.targetIdByTarget.delete(targetData.target);
});
};
/**
* Runs the specified query against the local store and returns the results,
* potentially taking advantage of query data from previous executions (such
* as the set of remote keys).
*
* @param usePreviousResults Whether results from previous executions can
* be used to optimize this query execution.
*/
LocalStore.prototype.executeQuery = function (query, usePreviousResults) {
var _this = this;
var lastLimboFreeSnapshotVersion = SnapshotVersion.min();
var remoteKeys = documentKeySet();
return this.persistence.runTransaction('Execute query', 'readonly', function (txn) {
return _this.getTargetData(txn, query.toTarget())
.next(function (targetData) {
if (targetData) {
lastLimboFreeSnapshotVersion =
targetData.lastLimboFreeSnapshotVersion;
return _this.targetCache
.getMatchingKeysForTargetId(txn, targetData.targetId)
.next(function (result) {
remoteKeys = result;
});
}
})
.next(function () { return _this.queryEngine.getDocumentsMatchingQuery(txn, query, usePreviousResults
? lastLimboFreeSnapshotVersion
: SnapshotVersion.min(), usePreviousResults ? remoteKeys : documentKeySet()); })
.next(function (documents) {
return { documents: documents, remoteKeys: remoteKeys };
});
});
};
LocalStore.prototype.applyWriteToRemoteDocuments = function (txn, batchResult, documentBuffer) {
var _this = this;
var batch = batchResult.batch;
var docKeys = batch.keys();
var promiseChain = PersistencePromise.resolve();
docKeys.forEach(function (docKey) {
promiseChain = promiseChain
.next(function () {
return documentBuffer.getEntry(txn, docKey);
})
.next(function (remoteDoc) {
var doc = remoteDoc;
var ackVersion = batchResult.docVersions.get(docKey);
hardAssert(ackVersion !== null, 'ackVersions should contain every doc in the write.');
if (!doc || doc.version.compareTo(ackVersion) < 0) {
doc = batch.applyToRemoteDocument(docKey, doc, batchResult);
if (!doc) {
debugAssert(!remoteDoc, 'Mutation batch ' +
batch +
' applied to document ' +
remoteDoc +
' resulted in null');
}
else {
// We use the commitVersion as the readTime rather than the
// document's updateTime since the updateTime is not advanced
// for updates that do not modify the underlying document.
documentBuffer.addEntry(doc, batchResult.commitVersion);
}
}
});
});
return promiseChain.next(function () { return _this.mutationQueue.removeMutationBatch(txn, batch); });
};
LocalStore.prototype.collectGarbage = function (garbageCollector) {
var _this = this;
return this.persistence.runTransaction('Collect garbage', 'readwrite-primary', function (txn) { return garbageCollector.collect(txn, _this.targetDataByTarget); });
};
return LocalStore;
}());
/**
* The maximum time to leave a resume token buffered without writing it out.
* This value is arbitrary: it's long enough to avoid several writes
* (possibly indefinitely if updates come more frequently than this) but
* short enough that restarting after crashing will still have a pretty
* recent resume token.
*/
LocalStore.RESUME_TOKEN_MAX_AGE_MICROS = 5 * 60 * 1e6;
/**
* An implementation of LocalStore that provides additional functionality
* for MultiTabSyncEngine.
*/
// PORTING NOTE: Web only.
var MultiTabLocalStore = /** @class */ (function (_super) {
tslib.__extends(MultiTabLocalStore, _super);
function MultiTabLocalStore(persistence, queryEngine, initialUser) {
var _this = _super.call(this, persistence, queryEngine, initialUser) || this;
_this.persistence = persistence;
_this.mutationQueue = persistence.getMutationQueue(initialUser);
_this.remoteDocuments = persistence.getRemoteDocumentCache();
_this.targetCache = persistence.getTargetCache();
return _this;
}
/** Starts the LocalStore. */
MultiTabLocalStore.prototype.start = function () {
return this.synchronizeLastDocumentChangeReadTime();
};
/** Returns the local view of the documents affected by a mutation batch. */
MultiTabLocalStore.prototype.lookupMutationDocuments = function (batchId) {
var _this = this;
return this.persistence.runTransaction('Lookup mutation documents', 'readonly', function (txn) {
return _this.mutationQueue
.lookupMutationKeys(txn, batchId)
.next(function (keys) {
if (keys) {
return _this.localDocuments.getDocuments(txn, keys);
}
else {
return PersistencePromise.resolve(null);
}
});
});
};
MultiTabLocalStore.prototype.removeCachedMutationBatchMetadata = function (batchId) {
this.mutationQueue.removeCachedMutationKeys(batchId);
};
MultiTabLocalStore.prototype.setNetworkEnabled = function (networkEnabled) {
this.persistence.setNetworkEnabled(networkEnabled);
};
MultiTabLocalStore.prototype.getActiveClients = function () {
return this.persistence.getActiveClients();
};
MultiTabLocalStore.prototype.getTarget = function (targetId) {
var _this = this;
var cachedTargetData = this.targetDataByTarget.get(targetId);
if (cachedTargetData) {
return Promise.resolve(cachedTargetData.target);
}
else {
return this.persistence.runTransaction('Get target data', 'readonly', function (txn) {
return _this.targetCache
.getTargetDataForTarget(txn, targetId)
.next(function (targetData) { return (targetData ? targetData.target : null); });
});
}
};
/**
* Returns the set of documents that have been updated since the last call.
* If this is the first call, returns the set of changes since client
* initialization. Further invocations will return document changes since
* the point of rejection.
*/
MultiTabLocalStore.prototype.getNewDocumentChanges = function () {
var _this = this;
return this.persistence
.runTransaction('Get new document changes', 'readonly', function (txn) { return _this.remoteDocuments.getNewDocumentChanges(txn, _this.lastDocumentChangeReadTime); })
.then(function (_e) {
var changedDocs = _e.changedDocs, readTime = _e.readTime;
_this.lastDocumentChangeReadTime = readTime;
return changedDocs;
});
};
/**
* Reads the newest document change from persistence and forwards the internal
* synchronization marker so that calls to `getNewDocumentChanges()`
* only return changes that happened after client initialization.
*/
MultiTabLocalStore.prototype.synchronizeLastDocumentChangeReadTime = function () {
return tslib.__awaiter(this, void 0, void 0, function () {
var _e;
var _this = this;
return tslib.__generator(this, function (_f) {
switch (_f.label) {
case 0:
_e = this;
return [4 /*yield*/, this.persistence.runTransaction('Synchronize last document change read time', 'readonly', function (txn) { return _this.remoteDocuments.getLastReadTime(txn); })];
case 1:
_e.lastDocumentChangeReadTime = _f.sent();
return [2 /*return*/];
}
});
});
};
return MultiTabLocalStore;
}(LocalStore));
/**
* Verifies the error thrown by a LocalStore operation. If a LocalStore
* operation fails because the primary lease has been taken by another client,
* we ignore the error (the persistence layer will immediately call
* `applyPrimaryLease` to propagate the primary state change). All other errors
* are re-thrown.
*
* @param err An error returned by a LocalStore operation.
* @return A Promise that resolves after we recovered, or the original error.
*/
function ignoreIfPrimaryLeaseLoss(err) {
return tslib.__awaiter(this, void 0, void 0, function () {
return tslib.__generator(this, function (_e) {
if (err.code === Code.FAILED_PRECONDITION &&
err.message === PRIMARY_LEASE_LOST_ERROR_MSG) {
logDebug(LOG_TAG$4, 'Unexpectedly lost primary lease');
}
else {
throw err;
}
return [2 /*return*/];
});
});
}
/**
* @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 set of changes to what documents are currently in view and out of view for
* a given query. These changes are sent to the LocalStore by the View (via
* the SyncEngine) and are used to pin / unpin documents as appropriate.
*/
var LocalViewChanges = /** @class */ (function () {
function LocalViewChanges(targetId, fromCache, addedKeys, removedKeys) {
this.targetId = targetId;
this.fromCache = fromCache;
this.addedKeys = addedKeys;
this.removedKeys = removedKeys;
}
LocalViewChanges.fromSnapshot = function (targetId, viewSnapshot) {
var addedKeys = documentKeySet();
var removedKeys = documentKeySet();
for (var _i = 0, _e = viewSnapshot.docChanges; _i < _e.length; _i++) {
var docChange = _e[_i];
switch (docChange.type) {
case 0 /* Added */:
addedKeys = addedKeys.add(docChange.doc.key);
break;
case 1 /* Removed */:
removedKeys = removedKeys.add(docChange.doc.key);
break;
// do nothing
}
}
return new LocalViewChanges(targetId, viewSnapshot.fromCache, addedKeys, removedKeys);
};
return LocalViewChanges;
}());
/**
* @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 collection of references to a document from some kind of numbered entity
* (either a target ID or batch ID). As references are added to or removed from
* the set corresponding events are emitted to a registered garbage collector.
*
* Each reference is represented by a DocumentReference object. Each of them
* contains enough information to uniquely identify the reference. They are all
* stored primarily in a set sorted by key. A document is considered garbage if
* there's no references in that set (this can be efficiently checked thanks to
* sorting by key).
*
* ReferenceSet also keeps a secondary set that contains references sorted by
* IDs. This one is used to efficiently implement removal of all references by
* some target ID.
*/
var ReferenceSet = /** @class */ (function () {
function ReferenceSet() {
// A set of outstanding references to a document sorted by key.
this.refsByKey = new SortedSet(DocReference.compareByKey);
// A set of outstanding references to a document sorted by target id.
this.refsByTarget = new SortedSet(DocReference.compareByTargetId);
}
/** Returns true if the reference set contains no references. */
ReferenceSet.prototype.isEmpty = function () {
return this.refsByKey.isEmpty();
};
/** Adds a reference to the given document key for the given ID. */
ReferenceSet.prototype.addReference = function (key, id) {
var ref = new DocReference(key, id);
this.refsByKey = this.refsByKey.add(ref);
this.refsByTarget = this.refsByTarget.add(ref);
};
/** Add references to the given document keys for the given ID. */
ReferenceSet.prototype.addReferences = function (keys, id) {
var _this = this;
keys.forEach(function (key) { return _this.addReference(key, id); });
};
/**
* Removes a reference to the given document key for the given
* ID.
*/
ReferenceSet.prototype.removeReference = function (key, id) {
this.removeRef(new DocReference(key, id));
};
ReferenceSet.prototype.removeReferences = function (keys, id) {
var _this = this;
keys.forEach(function (key) { return _this.removeReference(key, id); });
};
/**
* Clears all references with a given ID. Calls removeRef() for each key
* removed.
*/
ReferenceSet.prototype.removeReferencesForId = function (id) {
var _this = this;
var emptyKey = DocumentKey.EMPTY;
var startRef = new DocReference(emptyKey, id);
var endRef = new DocReference(emptyKey, id + 1);
var keys = [];
this.refsByTarget.forEachInRange([startRef, endRef], function (ref) {
_this.removeRef(ref);
keys.push(ref.key);
});
return keys;
};
ReferenceSet.prototype.removeAllReferences = function () {
var _this = this;
this.refsByKey.forEach(function (ref) { return _this.removeRef(ref); });
};
ReferenceSet.prototype.removeRef = function (ref) {
this.refsByKey = this.refsByKey.delete(ref);
this.refsByTarget = this.refsByTarget.delete(ref);
};
ReferenceSet.prototype.referencesForId = function (id) {
var emptyKey = DocumentKey.EMPTY;
var startRef = new DocReference(emptyKey, id);
var endRef = new DocReference(emptyKey, id + 1);
var keys = documentKeySet();
this.refsByTarget.forEachInRange([startRef, endRef], function (ref) {
keys = keys.add(ref.key);
});
return keys;
};
ReferenceSet.prototype.containsKey = function (key) {
var ref = new DocReference(key, 0);
var firstRef = this.refsByKey.firstAfterOrEqual(ref);
return firstRef !== null && key.isEqual(firstRef.key);
};
return ReferenceSet;
}());
var DocReference = /** @class */ (function () {
function DocReference(key, targetOrBatchId) {
this.key = key;
this.targetOrBatchId = targetOrBatchId;
}
/** Compare by key then by ID */
DocReference.compareByKey = function (left, right) {
return (DocumentKey.comparator(left.key, right.key) ||
primitiveComparator(left.targetOrBatchId, right.targetOrBatchId));
};
/** Compare by ID then by key */
DocReference.compareByTargetId = function (left, right) {
return (primitiveComparator(left.targetOrBatchId, right.targetOrBatchId) ||
DocumentKey.comparator(left.key, right.key));
};
return DocReference;
}());
/**
* @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 event from the RemoteStore. It is split into targetChanges (changes to the
* state or the set of documents in our watched targets) and documentUpdates
* (changes to the actual documents).
*/
var RemoteEvent = /** @class */ (function () {
function RemoteEvent(
/**
* The snapshot version this event brings us up to, or MIN if not set.
*/
snapshotVersion,
/**
* A map from target to changes to the target. See TargetChange.
*/
targetChanges,
/**
* A set of targets that is known to be inconsistent. Listens for these
* targets should be re-established without resume tokens.
*/
targetMismatches,
/**
* A set of which documents have changed or been deleted, along with the
* doc's new values (if not deleted).
*/
documentUpdates,
/**
* A set of which document updates are due only to limbo resolution targets.
*/
resolvedLimboDocuments) {
this.snapshotVersion = snapshotVersion;
this.targetChanges = targetChanges;
this.targetMismatches = targetMismatches;
this.documentUpdates = documentUpdates;
this.resolvedLimboDocuments = resolvedLimboDocuments;
}
/**
* HACK: Views require RemoteEvents in order to determine whether the view is
* CURRENT, but secondary tabs don't receive remote events. So this method is
* used to create a synthesized RemoteEvent that can be used to apply a
* CURRENT status change to a View, for queries executed in a different tab.
*/
// PORTING NOTE: Multi-tab only
RemoteEvent.createSynthesizedRemoteEventForCurrentChange = function (targetId, current) {
var targetChanges = new Map();
targetChanges.set(targetId, TargetChange.createSynthesizedTargetChangeForCurrentChange(targetId, current));
return new RemoteEvent(SnapshotVersion.min(), targetChanges, targetIdSet(), maybeDocumentMap(), documentKeySet());
};
return RemoteEvent;
}());
/**
* A TargetChange specifies the set of changes for a specific target as part of
* a RemoteEvent. These changes track which documents are added, modified or
* removed, as well as the target's resume token and whether the target is
* marked CURRENT.
* The actual changes *to* documents are not part of the TargetChange since
* documents may be part of multiple targets.
*/
var TargetChange = /** @class */ (function () {
function TargetChange(
/**
* An opaque, server-assigned token that allows watching a query to be resumed
* after disconnecting without retransmitting all the data that matches the
* query. The resume token essentially identifies a point in time from which
* the server should resume sending results.
*/
resumeToken,
/**
* The "current" (synced) status of this target. Note that "current"
* has special meaning in the RPC protocol that implies that a target is
* both up-to-date and consistent with the rest of the watch stream.
*/
current,
/**
* The set of documents that were newly assigned to this target as part of
* this remote event.
*/
addedDocuments,
/**
* The set of documents that were already assigned to this target but received
* an update during this remote event.
*/
modifiedDocuments,
/**
* The set of documents that were removed from this target as part of this
* remote event.
*/
removedDocuments) {
this.resumeToken = resumeToken;
this.current = current;
this.addedDocuments = addedDocuments;
this.modifiedDocuments = modifiedDocuments;
this.removedDocuments = removedDocuments;
}
/**
* This method is used to create a synthesized TargetChanges that can be used to
* apply a CURRENT status change to a View (for queries executed in a different
* tab) or for new queries (to raise snapshots with correct CURRENT status).
*/
TargetChange.createSynthesizedTargetChangeForCurrentChange = function (targetId, current) {
return new TargetChange(ByteString.EMPTY_BYTE_STRING, current, documentKeySet(), documentKeySet(), documentKeySet());
};
return TargetChange;
}());
/**
* @license
* Copyright 2019 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 Target represents the WatchTarget representation of a Query, which is used
* by the LocalStore and the RemoteStore to keep track of and to execute
* backend queries. While a Query can represent multiple Targets, each Targets
* maps to a single WatchTarget in RemoteStore and a single TargetData entry
* in persistence.
*/
var Target = /** @class */ (function () {
/**
* Initializes a Target with a path and optional additional query constraints.
* Path must currently be empty if this is a collection group query.
*
* NOTE: you should always construct `Target` from `Query.toTarget` instead of
* using this constructor, because `Query` provides an implicit `orderBy`
* property.
*/
function Target(path, collectionGroup, orderBy, filters, limit, startAt, endAt) {
if (collectionGroup === void 0) { collectionGroup = null; }
if (orderBy === void 0) { orderBy = []; }
if (filters === void 0) { filters = []; }
if (limit === void 0) { limit = null; }
if (startAt === void 0) { startAt = null; }
if (endAt === void 0) { endAt = null; }
this.path = path;
this.collectionGroup = collectionGroup;
this.orderBy = orderBy;
this.filters = filters;
this.limit = limit;
this.startAt = startAt;
this.endAt = endAt;
this.memoizedCanonicalId = null;
}
Target.prototype.canonicalId = function () {
if (this.memoizedCanonicalId === null) {
var canonicalId_1 = this.path.canonicalString();
if (this.collectionGroup !== null) {
canonicalId_1 += '|cg:' + this.collectionGroup;
}
canonicalId_1 += '|f:';
canonicalId_1 += this.filters.map(function (f) { return f.canonicalId(); }).join(',');
canonicalId_1 += '|ob:';
canonicalId_1 += this.orderBy.map(function (o) { return o.canonicalId(); }).join(',');
if (!isNullOrUndefined(this.limit)) {
canonicalId_1 += '|l:';
canonicalId_1 += this.limit;
}
if (this.startAt) {
canonicalId_1 += '|lb:';
canonicalId_1 += this.startAt.canonicalId();
}
if (this.endAt) {
canonicalId_1 += '|ub:';
canonicalId_1 += this.endAt.canonicalId();
}
this.memoizedCanonicalId = canonicalId_1;
}
return this.memoizedCanonicalId;
};
Target.prototype.toString = function () {
var str = this.path.canonicalString();
if (this.collectionGroup !== null) {
str += ' collectionGroup=' + this.collectionGroup;
}
if (this.filters.length > 0) {
str += ", filters: [" + this.filters.join(', ') + "]";
}
if (!isNullOrUndefined(this.limit)) {
str += ', limit: ' + this.limit;
}
if (this.orderBy.length > 0) {
str += ", orderBy: [" + this.orderBy.join(', ') + "]";
}
if (this.startAt) {
str += ', startAt: ' + this.startAt.canonicalId();
}
if (this.endAt) {
str += ', endAt: ' + this.endAt.canonicalId();
}
return "Target(" + str + ")";
};
Target.prototype.isEqual = function (other) {
if (this.limit !== other.limit) {
return false;
}
if (this.orderBy.length !== other.orderBy.length) {
return false;
}
for (var i = 0; i < this.orderBy.length; i++) {
if (!this.orderBy[i].isEqual(other.orderBy[i])) {
return false;
}
}
if (this.filters.length !== other.filters.length) {
return false;
}
for (var i = 0; i < this.filters.length; i++) {
if (!this.filters[i].isEqual(other.filters[i])) {
return false;
}
}
if (this.collectionGroup !== other.collectionGroup) {
return false;
}
if (!this.path.isEqual(other.path)) {
return false;
}
if (this.startAt !== null
? !this.startAt.isEqual(other.startAt)
: other.startAt !== null) {
return false;
}
return this.endAt !== null
? this.endAt.isEqual(other.endAt)
: other.endAt === null;
};
Target.prototype.isDocumentQuery = function () {
return (DocumentKey.isDocumentKey(this.path) &&
this.collectionGroup === null &&
this.filters.length === 0);
};
return Target;
}());
/**
* @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.
*/
/**
* Query encapsulates all the query attributes we support in the SDK. It can
* be run against the LocalStore, as well as be converted to a `Target` to
* query the RemoteStore results.
*/
var Query = /** @class */ (function () {
/**
* Initializes a Query with a path and optional additional query constraints.
* Path must currently be empty if this is a collection group query.
*/
function Query(path, collectionGroup, explicitOrderBy, filters, limit, limitType /* First */, startAt, endAt) {
if (collectionGroup === void 0) { collectionGroup = null; }
if (explicitOrderBy === void 0) { explicitOrderBy = []; }
if (filters === void 0) { filters = []; }
if (limit === void 0) { limit = null; }
if (limitType === void 0) { limitType = "F"; }
if (startAt === void 0) { startAt = null; }
if (endAt === void 0) { endAt = null; }
this.path = path;
this.collectionGroup = collectionGroup;
this.explicitOrderBy = explicitOrderBy;
this.filters = filters;
this.limit = limit;
this.limitType = limitType;
this.startAt = startAt;
this.endAt = endAt;
this.memoizedOrderBy = null;
// The corresponding `Target` of this `Query` instance.
this.memoizedTarget = null;
if (this.startAt) {
this.assertValidBound(this.startAt);
}
if (this.endAt) {
this.assertValidBound(this.endAt);
}
}
Query.atPath = function (path) {
return new Query(path);
};
Object.defineProperty(Query.prototype, "orderBy", {
get: function () {
if (this.memoizedOrderBy === null) {
this.memoizedOrderBy = [];
var inequalityField = this.getInequalityFilterField();
var firstOrderByField = this.getFirstOrderByField();
if (inequalityField !== null && firstOrderByField === null) {
// In order to implicitly add key ordering, we must also add the
// inequality filter field for it to be a valid query.
// Note that the default inequality field and key ordering is ascending.
if (!inequalityField.isKeyField()) {
this.memoizedOrderBy.push(new OrderBy(inequalityField));
}
this.memoizedOrderBy.push(new OrderBy(FieldPath.keyField(), "asc" /* ASCENDING */));
}
else {
debugAssert(inequalityField === null ||
(firstOrderByField !== null &&
inequalityField.isEqual(firstOrderByField)), 'First orderBy should match inequality field.');
var foundKeyOrdering = false;
for (var _i = 0, _e = this.explicitOrderBy; _i < _e.length; _i++) {
var orderBy = _e[_i];
this.memoizedOrderBy.push(orderBy);
if (orderBy.field.isKeyField()) {
foundKeyOrdering = true;
}
}
if (!foundKeyOrdering) {
// The order of the implicit key ordering always matches the last
// explicit order by
var lastDirection = this.explicitOrderBy.length > 0
? this.explicitOrderBy[this.explicitOrderBy.length - 1].dir
: "asc" /* ASCENDING */;
this.memoizedOrderBy.push(new OrderBy(FieldPath.keyField(), lastDirection));
}
}
}
return this.memoizedOrderBy;
},
enumerable: true,
configurable: true
});
Query.prototype.addFilter = function (filter) {
debugAssert(this.getInequalityFilterField() == null ||
!(filter instanceof FieldFilter) ||
!filter.isInequality() ||
filter.field.isEqual(this.getInequalityFilterField()), 'Query must only have one inequality field.');
debugAssert(!this.isDocumentQuery(), 'No filtering allowed for document query');
var newFilters = this.filters.concat([filter]);
return new Query(this.path, this.collectionGroup, this.explicitOrderBy.slice(), newFilters, this.limit, this.limitType, this.startAt, this.endAt);
};
Query.prototype.addOrderBy = function (orderBy) {
debugAssert(!this.startAt && !this.endAt, 'Bounds must be set after orderBy');
// TODO(dimond): validate that orderBy does not list the same key twice.
var newOrderBy = this.explicitOrderBy.concat([orderBy]);
return new Query(this.path, this.collectionGroup, newOrderBy, this.filters.slice(), this.limit, this.limitType, this.startAt, this.endAt);
};
Query.prototype.withLimitToFirst = function (limit) {
return new Query(this.path, this.collectionGroup, this.explicitOrderBy.slice(), this.filters.slice(), limit, "F" /* First */, this.startAt, this.endAt);
};
Query.prototype.withLimitToLast = function (limit) {
return new Query(this.path, this.collectionGroup, this.explicitOrderBy.slice(), this.filters.slice(), limit, "L" /* Last */, this.startAt, this.endAt);
};
Query.prototype.withStartAt = function (bound) {
return new Query(this.path, this.collectionGroup, this.explicitOrderBy.slice(), this.filters.slice(), this.limit, this.limitType, bound, this.endAt);
};
Query.prototype.withEndAt = function (bound) {
return new Query(this.path, this.collectionGroup, this.explicitOrderBy.slice(), this.filters.slice(), this.limit, this.limitType, this.startAt, bound);
};
/**
* Helper to convert a collection group query into a collection query at a
* specific path. This is used when executing collection group queries, since
* we have to split the query into a set of collection queries at multiple
* paths.
*/
Query.prototype.asCollectionQueryAtPath = function (path) {
return new Query(path,
/*collectionGroup=*/ null, this.explicitOrderBy.slice(), this.filters.slice(), this.limit, this.limitType, this.startAt, this.endAt);
};
/**
* Returns true if this query does not specify any query constraints that
* could remove results.
*/
Query.prototype.matchesAllDocuments = function () {
return (this.filters.length === 0 &&
this.limit === null &&
this.startAt == null &&
this.endAt == null &&
(this.explicitOrderBy.length === 0 ||
(this.explicitOrderBy.length === 1 &&
this.explicitOrderBy[0].field.isKeyField())));
};
// TODO(b/29183165): This is used to get a unique string from a query to, for
// example, use as a dictionary key, but the implementation is subject to
// collisions. Make it collision-free.
Query.prototype.canonicalId = function () {
return this.toTarget().canonicalId() + "|lt:" + this.limitType;
};
Query.prototype.toString = function () {
return "Query(target=" + this.toTarget().toString() + "; limitType=" + this.limitType + ")";
};
Query.prototype.isEqual = function (other) {
return (this.toTarget().isEqual(other.toTarget()) &&
this.limitType === other.limitType);
};
Query.prototype.docComparator = function (d1, d2) {
var comparedOnKeyField = false;
for (var _i = 0, _e = this.orderBy; _i < _e.length; _i++) {
var orderBy = _e[_i];
var comp = orderBy.compare(d1, d2);
if (comp !== 0) {
return comp;
}
comparedOnKeyField = comparedOnKeyField || orderBy.field.isKeyField();
}
// Assert that we actually compared by key
debugAssert(comparedOnKeyField, "orderBy used that doesn't compare on key field");
return 0;
};
Query.prototype.matches = function (doc) {
return (this.matchesPathAndCollectionGroup(doc) &&
this.matchesOrderBy(doc) &&
this.matchesFilters(doc) &&
this.matchesBounds(doc));
};
Query.prototype.hasLimitToFirst = function () {
return !isNullOrUndefined(this.limit) && this.limitType === "F" /* First */;
};
Query.prototype.hasLimitToLast = function () {
return !isNullOrUndefined(this.limit) && this.limitType === "L" /* Last */;
};
Query.prototype.getFirstOrderByField = function () {
return this.explicitOrderBy.length > 0
? this.explicitOrderBy[0].field
: null;
};
Query.prototype.getInequalityFilterField = function () {
for (var _i = 0, _e = this.filters; _i < _e.length; _i++) {
var filter = _e[_i];
if (filter instanceof FieldFilter && filter.isInequality()) {
return filter.field;
}
}
return null;
};
// Checks if any of the provided Operators are included in the query and
// returns the first one that is, or null if none are.
Query.prototype.findFilterOperator = function (operators) {
for (var _i = 0, _e = this.filters; _i < _e.length; _i++) {
var filter = _e[_i];
if (filter instanceof FieldFilter) {
if (operators.indexOf(filter.op) >= 0) {
return filter.op;
}
}
}
return null;
};
Query.prototype.isDocumentQuery = function () {
return this.toTarget().isDocumentQuery();
};
Query.prototype.isCollectionGroupQuery = function () {
return this.collectionGroup !== null;
};
/**
* Converts this `Query` instance to it's corresponding `Target`
* representation.
*/
Query.prototype.toTarget = function () {
if (!this.memoizedTarget) {
if (this.limitType === "F" /* First */) {
this.memoizedTarget = new Target(this.path, this.collectionGroup, this.orderBy, this.filters, this.limit, this.startAt, this.endAt);
}
else {
// Flip the orderBy directions since we want the last results
var orderBys = [];
for (var _i = 0, _e = this.orderBy; _i < _e.length; _i++) {
var orderBy = _e[_i];
var dir = orderBy.dir === "desc" /* DESCENDING */
? "asc" /* ASCENDING */
: "desc" /* DESCENDING */;
orderBys.push(new OrderBy(orderBy.field, dir));
}
// We need to swap the cursors to match the now-flipped query ordering.
var startAt = this.endAt
? new Bound(this.endAt.position, !this.endAt.before)
: null;
var endAt = this.startAt
? new Bound(this.startAt.position, !this.startAt.before)
: null;
// Now return as a LimitType.First query.
this.memoizedTarget = new Target(this.path, this.collectionGroup, orderBys, this.filters, this.limit, startAt, endAt);
}
}
return this.memoizedTarget;
};
Query.prototype.matchesPathAndCollectionGroup = function (doc) {
var docPath = doc.key.path;
if (this.collectionGroup !== null) {
// NOTE: this.path is currently always empty since we don't expose Collection
// Group queries rooted at a document path yet.
return (doc.key.hasCollectionId(this.collectionGroup) &&
this.path.isPrefixOf(docPath));
}
else if (DocumentKey.isDocumentKey(this.path)) {
// exact match for document queries
return this.path.isEqual(docPath);
}
else {
// shallow ancestor queries by default
return this.path.isImmediateParentOf(docPath);
}
};
/**
* A document must have a value for every ordering clause in order to show up
* in the results.
*/
Query.prototype.matchesOrderBy = function (doc) {
for (var _i = 0, _e = this.explicitOrderBy; _i < _e.length; _i++) {
var orderBy = _e[_i];
// order by key always matches
if (!orderBy.field.isKeyField() && doc.field(orderBy.field) === null) {
return false;
}
}
return true;
};
Query.prototype.matchesFilters = function (doc) {
for (var _i = 0, _e = this.filters; _i < _e.length; _i++) {
var filter = _e[_i];
if (!filter.matches(doc)) {
return false;
}
}
return true;
};
/**
* Makes sure a document is within the bounds, if provided.
*/
Query.prototype.matchesBounds = function (doc) {
if (this.startAt && !this.startAt.sortsBeforeDocument(this.orderBy, doc)) {
return false;
}
if (this.endAt && this.endAt.sortsBeforeDocument(this.orderBy, doc)) {
return false;
}
return true;
};
Query.prototype.assertValidBound = function (bound) {
debugAssert(bound.position.length <= this.orderBy.length, 'Bound is longer than orderBy');
};
return Query;
}());
var Filter = /** @class */ (function () {
function Filter() {
}
return Filter;
}());
var FieldFilter = /** @class */ (function (_super) {
tslib.__extends(FieldFilter, _super);
function FieldFilter(field, op, value) {
var _this = _super.call(this) || this;
_this.field = field;
_this.op = op;
_this.value = value;
return _this;
}
/**
* Creates a filter based on the provided arguments.
*/
FieldFilter.create = function (field, op, value) {
if (field.isKeyField()) {
if (op === "in" /* IN */) {
debugAssert(isArray(value), 'Comparing on key with IN, but filter value not an ArrayValue');
debugAssert((value.arrayValue.values || []).every(function (elem) { return isReferenceValue(elem); }), 'Comparing on key with IN, but an array value was not a RefValue');
return new KeyFieldInFilter(field, value);
}
else {
debugAssert(isReferenceValue(value), 'Comparing on key, but filter value not a RefValue');
debugAssert(op !== "array-contains" /* ARRAY_CONTAINS */ && op !== "array-contains-any" /* ARRAY_CONTAINS_ANY */, "'" + op.toString() + "' queries don't make sense on document keys.");
return new KeyFieldFilter(field, op, value);
}
}
else if (isNullValue(value)) {
if (op !== "==" /* EQUAL */) {
throw new FirestoreError(Code.INVALID_ARGUMENT, 'Invalid query. Null supports only equality comparisons.');
}
return new FieldFilter(field, op, value);
}
else if (isNanValue(value)) {
if (op !== "==" /* EQUAL */) {
throw new FirestoreError(Code.INVALID_ARGUMENT, 'Invalid query. NaN supports only equality comparisons.');
}
return new FieldFilter(field, op, value);
}
else if (op === "array-contains" /* ARRAY_CONTAINS */) {
return new ArrayContainsFilter(field, value);
}
else if (op === "in" /* IN */) {
debugAssert(isArray(value), 'IN filter has invalid value: ' + value.toString());
return new InFilter(field, value);
}
else if (op === "array-contains-any" /* ARRAY_CONTAINS_ANY */) {
debugAssert(isArray(value), 'ARRAY_CONTAINS_ANY filter has invalid value: ' + value.toString());
return new ArrayContainsAnyFilter(field, value);
}
else {
return new FieldFilter(field, op, value);
}
};
FieldFilter.prototype.matches = function (doc) {
var other = doc.field(this.field);
// Only compare types with matching backend order (such as double and int).
return (other !== null &&
typeOrder(this.value) === typeOrder(other) &&
this.matchesComparison(valueCompare(other, this.value)));
};
FieldFilter.prototype.matchesComparison = function (comparison) {
switch (this.op) {
case "<" /* LESS_THAN */:
return comparison < 0;
case "<=" /* LESS_THAN_OR_EQUAL */:
return comparison <= 0;
case "==" /* EQUAL */:
return comparison === 0;
case ">" /* GREATER_THAN */:
return comparison > 0;
case ">=" /* GREATER_THAN_OR_EQUAL */:
return comparison >= 0;
default:
return fail('Unknown FieldFilter operator: ' + this.op);
}
};
FieldFilter.prototype.isInequality = function () {
return ([
"<" /* LESS_THAN */,
"<=" /* LESS_THAN_OR_EQUAL */,
">" /* GREATER_THAN */,
">=" /* GREATER_THAN_OR_EQUAL */
].indexOf(this.op) >= 0);
};
FieldFilter.prototype.canonicalId = function () {
// TODO(b/29183165): Technically, this won't be unique if two values have
// the same description, such as the int 3 and the string "3". So we should
// add the types in here somehow, too.
return (this.field.canonicalString() +
this.op.toString() +
canonicalId(this.value));
};
FieldFilter.prototype.isEqual = function (other) {
if (other instanceof FieldFilter) {
return (this.op === other.op &&
this.field.isEqual(other.field) &&
valueEquals(this.value, other.value));
}
else {
return false;
}
};
FieldFilter.prototype.toString = function () {
return this.field.canonicalString() + " " + this.op + " " + canonicalId(this.value);
};
return FieldFilter;
}(Filter));
/** Filter that matches on key fields (i.e. '__name__'). */
var KeyFieldFilter = /** @class */ (function (_super) {
tslib.__extends(KeyFieldFilter, _super);
function KeyFieldFilter(field, op, value) {
var _this = _super.call(this, field, op, value) || this;
debugAssert(isReferenceValue(value), 'KeyFieldFilter expects a ReferenceValue');
_this.key = DocumentKey.fromName(value.referenceValue);
return _this;
}
KeyFieldFilter.prototype.matches = function (doc) {
var comparison = DocumentKey.comparator(doc.key, this.key);
return this.matchesComparison(comparison);
};
return KeyFieldFilter;
}(FieldFilter));
/** Filter that matches on key fields within an array. */
var KeyFieldInFilter = /** @class */ (function (_super) {
tslib.__extends(KeyFieldInFilter, _super);
function KeyFieldInFilter(field, value) {
var _this = _super.call(this, field, "in" /* IN */, value) || this;
debugAssert(isArray(value), 'KeyFieldInFilter expects an ArrayValue');
_this.keys = (value.arrayValue.values || []).map(function (v) {
debugAssert(isReferenceValue(v), 'Comparing on key with IN, but an array value was not a ReferenceValue');
return DocumentKey.fromName(v.referenceValue);
});
return _this;
}
KeyFieldInFilter.prototype.matches = function (doc) {
return this.keys.some(function (key) { return key.isEqual(doc.key); });
};
return KeyFieldInFilter;
}(FieldFilter));
/** A Filter that implements the array-contains operator. */
var ArrayContainsFilter = /** @class */ (function (_super) {
tslib.__extends(ArrayContainsFilter, _super);
function ArrayContainsFilter(field, value) {
return _super.call(this, field, "array-contains" /* ARRAY_CONTAINS */, value) || this;
}
ArrayContainsFilter.prototype.matches = function (doc) {
var other = doc.field(this.field);
return isArray(other) && arrayValueContains(other.arrayValue, this.value);
};
return ArrayContainsFilter;
}(FieldFilter));
/** A Filter that implements the IN operator. */
var InFilter = /** @class */ (function (_super) {
tslib.__extends(InFilter, _super);
function InFilter(field, value) {
var _this = _super.call(this, field, "in" /* IN */, value) || this;
debugAssert(isArray(value), 'InFilter expects an ArrayValue');
return _this;
}
InFilter.prototype.matches = function (doc) {
var other = doc.field(this.field);
return other !== null && arrayValueContains(this.value.arrayValue, other);
};
return InFilter;
}(FieldFilter));
/** A Filter that implements the array-contains-any operator. */
var ArrayContainsAnyFilter = /** @class */ (function (_super) {
tslib.__extends(ArrayContainsAnyFilter, _super);
function ArrayContainsAnyFilter(field, value) {
var _this = _super.call(this, field, "array-contains-any" /* ARRAY_CONTAINS_ANY */, value) || this;
debugAssert(isArray(value), 'ArrayContainsAnyFilter expects an ArrayValue');
return _this;
}
ArrayContainsAnyFilter.prototype.matches = function (doc) {
var _this = this;
var other = doc.field(this.field);
if (!isArray(other) || !other.arrayValue.values) {
return false;
}
return other.arrayValue.values.some(function (val) { return arrayValueContains(_this.value.arrayValue, val); });
};
return ArrayContainsAnyFilter;
}(FieldFilter));
/**
* Represents a bound of a query.
*
* The bound is specified with the given components representing a position and
* whether it's just before or just after the position (relative to whatever the
* query order is).
*
* The position represents a logical index position for a query. It's a prefix
* of values for the (potentially implicit) order by clauses of a query.
*
* Bound provides a function to determine whether a document comes before or
* after a bound. This is influenced by whether the position is just before or
* just after the provided values.
*/
var Bound = /** @class */ (function () {
function Bound(position, before) {
this.position = position;
this.before = before;
}
Bound.prototype.canonicalId = function () {
// TODO(b/29183165): Make this collision robust.
return (this.before ? 'b' : 'a') + ":" + this.position
.map(function (p) { return canonicalId(p); })
.join(',');
};
/**
* Returns true if a document sorts before a bound using the provided sort
* order.
*/
Bound.prototype.sortsBeforeDocument = function (orderBy, doc) {
debugAssert(this.position.length <= orderBy.length, "Bound has more components than query's orderBy");
var comparison = 0;
for (var i = 0; i < this.position.length; i++) {
var orderByComponent = orderBy[i];
var component = this.position[i];
if (orderByComponent.field.isKeyField()) {
debugAssert(isReferenceValue(component), 'Bound has a non-key value where the key path is being used.');
comparison = DocumentKey.comparator(DocumentKey.fromName(component.referenceValue), doc.key);
}
else {
var docValue = doc.field(orderByComponent.field);
debugAssert(docValue !== null, 'Field should exist since document matched the orderBy already.');
comparison = valueCompare(component, docValue);
}
if (orderByComponent.dir === "desc" /* DESCENDING */) {
comparison = comparison * -1;
}
if (comparison !== 0) {
break;
}
}
return this.before ? comparison <= 0 : comparison < 0;
};
Bound.prototype.isEqual = function (other) {
if (other === null) {
return false;
}
if (this.before !== other.before ||
this.position.length !== other.position.length) {
return false;
}
for (var i = 0; i < this.position.length; i++) {
var thisPosition = this.position[i];
var otherPosition = other.position[i];
if (!valueEquals(thisPosition, otherPosition)) {
return false;
}
}
return true;
};
return Bound;
}());
/**
* An ordering on a field, in some Direction. Direction defaults to ASCENDING.
*/
var OrderBy = /** @class */ (function () {
function OrderBy(field, dir) {
this.field = field;
if (dir === undefined) {
dir = "asc" /* ASCENDING */;
}
this.dir = dir;
this.isKeyOrderBy = field.isKeyField();
}
OrderBy.prototype.compare = function (d1, d2) {
var comparison = this.isKeyOrderBy
? DocumentKey.comparator(d1.key, d2.key)
: compareDocumentsByField(this.field, d1, d2);
switch (this.dir) {
case "asc" /* ASCENDING */:
return comparison;
case "desc" /* DESCENDING */:
return -1 * comparison;
default:
return fail('Unknown direction: ' + this.dir);
}
};
OrderBy.prototype.canonicalId = function () {
// TODO(b/29183165): Make this collision robust.
return this.field.canonicalString() + this.dir.toString();
};
OrderBy.prototype.toString = function () {
return this.field.canonicalString() + " (" + this.dir + ")";
};
OrderBy.prototype.isEqual = function (other) {
return this.dir === other.dir && this.field.isEqual(other.field);
};
return OrderBy;
}());
/**
* @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.
*/
/**
* DocumentSet is an immutable (copy-on-write) collection that holds documents
* in order specified by the provided comparator. We always add a document key
* comparator on top of what is provided to guarantee document equality based on
* the key.
*/
var DocumentSet = /** @class */ (function () {
/** The default ordering is by key if the comparator is omitted */
function DocumentSet(comp) {
// We are adding document key comparator to the end as it's the only
// guaranteed unique property of a document.
if (comp) {
this.comparator = function (d1, d2) { return comp(d1, d2) || DocumentKey.comparator(d1.key, d2.key); };
}
else {
this.comparator = function (d1, d2) { return DocumentKey.comparator(d1.key, d2.key); };
}
this.keyedMap = documentMap();
this.sortedSet = new SortedMap(this.comparator);
}
/**
* Returns an empty copy of the existing DocumentSet, using the same
* comparator.
*/
DocumentSet.emptySet = function (oldSet) {
return new DocumentSet(oldSet.comparator);
};
DocumentSet.prototype.has = function (key) {
return this.keyedMap.get(key) != null;
};
DocumentSet.prototype.get = function (key) {
return this.keyedMap.get(key);
};
DocumentSet.prototype.first = function () {
return this.sortedSet.minKey();
};
DocumentSet.prototype.last = function () {
return this.sortedSet.maxKey();
};
DocumentSet.prototype.isEmpty = function () {
return this.sortedSet.isEmpty();
};
/**
* Returns the index of the provided key in the document set, or -1 if the
* document key is not present in the set;
*/
DocumentSet.prototype.indexOf = function (key) {
var doc = this.keyedMap.get(key);
return doc ? this.sortedSet.indexOf(doc) : -1;
};
Object.defineProperty(DocumentSet.prototype, "size", {
get: function () {
return this.sortedSet.size;
},
enumerable: true,
configurable: true
});
/** Iterates documents in order defined by "comparator" */
DocumentSet.prototype.forEach = function (cb) {
this.sortedSet.inorderTraversal(function (k, v) {
cb(k);
return false;
});
};
/** Inserts or updates a document with the same key */
DocumentSet.prototype.add = function (doc) {
// First remove the element if we have it.
var set = this.delete(doc.key);
return set.copy(set.keyedMap.insert(doc.key, doc), set.sortedSet.insert(doc, null));
};
/** Deletes a document with a given key */
DocumentSet.prototype.delete = function (key) {
var doc = this.get(key);
if (!doc) {
return this;
}
return this.copy(this.keyedMap.remove(key), this.sortedSet.remove(doc));
};
DocumentSet.prototype.isEqual = function (other) {
if (!(other instanceof DocumentSet)) {
return false;
}
if (this.size !== other.size) {
return false;
}
var thisIt = this.sortedSet.getIterator();
var otherIt = other.sortedSet.getIterator();
while (thisIt.hasNext()) {
var thisDoc = thisIt.getNext().key;
var otherDoc = otherIt.getNext().key;
if (!thisDoc.isEqual(otherDoc)) {
return false;
}
}
return true;
};
DocumentSet.prototype.toString = function () {
var docStrings = [];
this.forEach(function (doc) {
docStrings.push(doc.toString());
});
if (docStrings.length === 0) {
return 'DocumentSet ()';
}
else {
return 'DocumentSet (\n ' + docStrings.join(' \n') + '\n)';
}
};
DocumentSet.prototype.copy = function (keyedMap, sortedSet) {
var newSet = new DocumentSet();
newSet.comparator = this.comparator;
newSet.keyedMap = keyedMap;
newSet.sortedSet = sortedSet;
return newSet;
};
return DocumentSet;
}());
/**
* @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.
*/
/**
* DocumentChangeSet keeps track of a set of changes to docs in a query, merging
* duplicate events for the same doc.
*/
var DocumentChangeSet = /** @class */ (function () {
function DocumentChangeSet() {
this.changeMap = new SortedMap(DocumentKey.comparator);
}
DocumentChangeSet.prototype.track = function (change) {
var key = change.doc.key;
var oldChange = this.changeMap.get(key);
if (!oldChange) {
this.changeMap = this.changeMap.insert(key, change);
return;
}
// Merge the new change with the existing change.
if (change.type !== 0 /* Added */ &&
oldChange.type === 3 /* Metadata */) {
this.changeMap = this.changeMap.insert(key, change);
}
else if (change.type === 3 /* Metadata */ &&
oldChange.type !== 1 /* Removed */) {
this.changeMap = this.changeMap.insert(key, {
type: oldChange.type,
doc: change.doc
});
}
else if (change.type === 2 /* Modified */ &&
oldChange.type === 2 /* Modified */) {
this.changeMap = this.changeMap.insert(key, {
type: 2 /* Modified */,
doc: change.doc
});
}
else if (change.type === 2 /* Modified */ &&
oldChange.type === 0 /* Added */) {
this.changeMap = this.changeMap.insert(key, {
type: 0 /* Added */,
doc: change.doc
});
}
else if (change.type === 1 /* Removed */ &&
oldChange.type === 0 /* Added */) {
this.changeMap = this.changeMap.remove(key);
}
else if (change.type === 1 /* Removed */ &&
oldChange.type === 2 /* Modified */) {
this.changeMap = this.changeMap.insert(key, {
type: 1 /* Removed */,
doc: oldChange.doc
});
}
else if (change.type === 0 /* Added */ &&
oldChange.type === 1 /* Removed */) {
this.changeMap = this.changeMap.insert(key, {
type: 2 /* Modified */,
doc: change.doc
});
}
else {
// This includes these cases, which don't make sense:
// Added->Added
// Removed->Removed
// Modified->Added
// Removed->Modified
// Metadata->Added
// Removed->Metadata
fail('unsupported combination of changes: ' +
JSON.stringify(change) +
' after ' +
JSON.stringify(oldChange));
}
};
DocumentChangeSet.prototype.getChanges = function () {
var changes = [];
this.changeMap.inorderTraversal(function (key, change) {
changes.push(change);
});
return changes;
};
return DocumentChangeSet;
}());
var ViewSnapshot = /** @class */ (function () {
function ViewSnapshot(query, docs, oldDocs, docChanges, mutatedKeys, fromCache, syncStateChanged, excludesMetadataChanges) {
this.query = query;
this.docs = docs;
this.oldDocs = oldDocs;
this.docChanges = docChanges;
this.mutatedKeys = mutatedKeys;
this.fromCache = fromCache;
this.syncStateChanged = syncStateChanged;
this.excludesMetadataChanges = excludesMetadataChanges;
}
/** Returns a view snapshot as if all documents in the snapshot were added. */
ViewSnapshot.fromInitialDocuments = function (query, documents, mutatedKeys, fromCache) {
var changes = [];
documents.forEach(function (doc) {
changes.push({ type: 0 /* Added */, doc: doc });
});
return new ViewSnapshot(query, documents, DocumentSet.emptySet(documents), changes, mutatedKeys, fromCache,
/* syncStateChanged= */ true,
/* excludesMetadataChanges= */ false);
};
Object.defineProperty(ViewSnapshot.prototype, "hasPendingWrites", {
get: function () {
return !this.mutatedKeys.isEmpty();
},
enumerable: true,
configurable: true
});
ViewSnapshot.prototype.isEqual = function (other) {
if (this.fromCache !== other.fromCache ||
this.syncStateChanged !== other.syncStateChanged ||
!this.mutatedKeys.isEqual(other.mutatedKeys) ||
!this.query.isEqual(other.query) ||
!this.docs.isEqual(other.docs) ||
!this.oldDocs.isEqual(other.oldDocs)) {
return false;
}
var changes = this.docChanges;
var otherChanges = other.docChanges;
if (changes.length !== otherChanges.length) {
return false;
}
for (var i = 0; i < changes.length; i++) {
if (changes[i].type !== otherChanges[i].type ||
!changes[i].doc.isEqual(otherChanges[i].doc)) {
return false;
}
}
return true;
};
return ViewSnapshot;
}());
/**
* @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.
*/
var AddedLimboDocument = /** @class */ (function () {
function AddedLimboDocument(key) {
this.key = key;
}
return AddedLimboDocument;
}());
var RemovedLimboDocument = /** @class */ (function () {
function RemovedLimboDocument(key) {
this.key = key;
}
return RemovedLimboDocument;
}());
/**
* View is responsible for computing the final merged truth of what docs are in
* a query. It gets notified of local and remote changes to docs, and applies
* the query filters and limits to determine the most correct possible results.
*/
var View = /** @class */ (function () {
function View(query,
/** Documents included in the remote target */
_syncedDocuments) {
this.query = query;
this._syncedDocuments = _syncedDocuments;
this.syncState = null;
/**
* A flag whether the view is current with the backend. A view is considered
* current after it has seen the current flag from the backend and did not
* lose consistency within the watch stream (e.g. because of an existence
* filter mismatch).
*/
this.current = false;
/** Documents in the view but not in the remote target */
this.limboDocuments = documentKeySet();
/** Document Keys that have local changes */
this.mutatedKeys = documentKeySet();
this.documentSet = new DocumentSet(query.docComparator.bind(query));
}
Object.defineProperty(View.prototype, "syncedDocuments", {
/**
* The set of remote documents that the server has told us belongs to the target associated with
* this view.
*/
get: function () {
return this._syncedDocuments;
},
enumerable: true,
configurable: true
});
/**
* Iterates over a set of doc changes, applies the query limit, and computes
* what the new results should be, what the changes were, and whether we may
* need to go back to the local cache for more results. Does not make any
* changes to the view.
* @param docChanges The doc changes to apply to this view.
* @param previousChanges If this is being called with a refill, then start
* with this set of docs and changes instead of the current view.
* @return a new set of docs, changes, and refill flag.
*/
View.prototype.computeDocChanges = function (docChanges, previousChanges) {
var _this = this;
var changeSet = previousChanges
? previousChanges.changeSet
: new DocumentChangeSet();
var oldDocumentSet = previousChanges
? previousChanges.documentSet
: this.documentSet;
var newMutatedKeys = previousChanges
? previousChanges.mutatedKeys
: this.mutatedKeys;
var newDocumentSet = oldDocumentSet;
var needsRefill = false;
// Track the last doc in a (full) limit. This is necessary, because some
// update (a delete, or an update moving a doc past the old limit) might
// mean there is some other document in the local cache that either should
// come (1) between the old last limit doc and the new last document, in the
// case of updates, or (2) after the new last document, in the case of
// deletes. So we keep this doc at the old limit to compare the updates to.
//
// Note that this should never get used in a refill (when previousChanges is
// set), because there will only be adds -- no deletes or updates.
var lastDocInLimit = this.query.hasLimitToFirst() && oldDocumentSet.size === this.query.limit
? oldDocumentSet.last()
: null;
var firstDocInLimit = this.query.hasLimitToLast() && oldDocumentSet.size === this.query.limit
? oldDocumentSet.first()
: null;
docChanges.inorderTraversal(function (key, newMaybeDoc) {
var oldDoc = oldDocumentSet.get(key);
var newDoc = newMaybeDoc instanceof Document ? newMaybeDoc : null;
if (newDoc) {
debugAssert(key.isEqual(newDoc.key), 'Mismatching keys found in document changes: ' +
key +
' != ' +
newDoc.key);
newDoc = _this.query.matches(newDoc) ? newDoc : null;
}
var oldDocHadPendingMutations = oldDoc
? _this.mutatedKeys.has(oldDoc.key)
: false;
var newDocHasPendingMutations = newDoc
? newDoc.hasLocalMutations ||
// We only consider committed mutations for documents that were
// mutated during the lifetime of the view.
(_this.mutatedKeys.has(newDoc.key) && newDoc.hasCommittedMutations)
: false;
var changeApplied = false;
// Calculate change
if (oldDoc && newDoc) {
var docsEqual = oldDoc.data().isEqual(newDoc.data());
if (!docsEqual) {
if (!_this.shouldWaitForSyncedDocument(oldDoc, newDoc)) {
changeSet.track({
type: 2 /* Modified */,
doc: newDoc
});
changeApplied = true;
if ((lastDocInLimit &&
_this.query.docComparator(newDoc, lastDocInLimit) > 0) ||
(firstDocInLimit &&
_this.query.docComparator(newDoc, firstDocInLimit) < 0)) {
// This doc moved from inside the limit to outside the limit.
// That means there may be some other doc in the local cache
// that should be included instead.
needsRefill = true;
}
}
}
else if (oldDocHadPendingMutations !== newDocHasPendingMutations) {
changeSet.track({ type: 3 /* Metadata */, doc: newDoc });
changeApplied = true;
}
}
else if (!oldDoc && newDoc) {
changeSet.track({ type: 0 /* Added */, doc: newDoc });
changeApplied = true;
}
else if (oldDoc && !newDoc) {
changeSet.track({ type: 1 /* Removed */, doc: oldDoc });
changeApplied = true;
if (lastDocInLimit || firstDocInLimit) {
// A doc was removed from a full limit query. We'll need to
// requery from the local cache to see if we know about some other
// doc that should be in the results.
needsRefill = true;
}
}
if (changeApplied) {
if (newDoc) {
newDocumentSet = newDocumentSet.add(newDoc);
if (newDocHasPendingMutations) {
newMutatedKeys = newMutatedKeys.add(key);
}
else {
newMutatedKeys = newMutatedKeys.delete(key);
}
}
else {
newDocumentSet = newDocumentSet.delete(key);
newMutatedKeys = newMutatedKeys.delete(key);
}
}
});
// Drop documents out to meet limit/limitToLast requirement.
if (this.query.hasLimitToFirst() || this.query.hasLimitToLast()) {
while (newDocumentSet.size > this.query.limit) {
var oldDoc = this.query.hasLimitToFirst()
? newDocumentSet.last()
: newDocumentSet.first();
newDocumentSet = newDocumentSet.delete(oldDoc.key);
newMutatedKeys = newMutatedKeys.delete(oldDoc.key);
changeSet.track({ type: 1 /* Removed */, doc: oldDoc });
}
}
debugAssert(!needsRefill || !previousChanges, 'View was refilled using docs that themselves needed refilling.');
return {
documentSet: newDocumentSet,
changeSet: changeSet,
needsRefill: needsRefill,
mutatedKeys: newMutatedKeys
};
};
View.prototype.shouldWaitForSyncedDocument = function (oldDoc, newDoc) {
// We suppress the initial change event for documents that were modified as
// part of a write acknowledgment (e.g. when the value of a server transform
// is applied) as Watch will send us the same document again.
// By suppressing the event, we only raise two user visible events (one with
// `hasPendingWrites` and the final state of the document) instead of three
// (one with `hasPendingWrites`, the modified document with
// `hasPendingWrites` and the final state of the document).
return (oldDoc.hasLocalMutations &&
newDoc.hasCommittedMutations &&
!newDoc.hasLocalMutations);
};
/**
* Updates the view with the given ViewDocumentChanges and optionally updates
* limbo docs and sync state from the provided target change.
* @param docChanges The set of changes to make to the view's docs.
* @param updateLimboDocuments Whether to update limbo documents based on this
* change.
* @param targetChange A target change to apply for computing limbo docs and
* sync state.
* @return A new ViewChange with the given docs, changes, and sync state.
*/
// PORTING NOTE: The iOS/Android clients always compute limbo document changes.
View.prototype.applyChanges = function (docChanges, updateLimboDocuments, targetChange) {
var _this = this;
debugAssert(!docChanges.needsRefill, 'Cannot apply changes that need a refill');
var oldDocs = this.documentSet;
this.documentSet = docChanges.documentSet;
this.mutatedKeys = docChanges.mutatedKeys;
// Sort changes based on type and query comparator
var changes = docChanges.changeSet.getChanges();
changes.sort(function (c1, c2) {
return (compareChangeType(c1.type, c2.type) ||
_this.query.docComparator(c1.doc, c2.doc));
});
this.applyTargetChange(targetChange);
var limboChanges = updateLimboDocuments
? this.updateLimboDocuments()
: [];
var synced = this.limboDocuments.size === 0 && this.current;
var newSyncState = synced ? 1 /* Synced */ : 0 /* Local */;
var syncStateChanged = newSyncState !== this.syncState;
this.syncState = newSyncState;
if (changes.length === 0 && !syncStateChanged) {
// no changes
return { limboChanges: limboChanges };
}
else {
var snap = new ViewSnapshot(this.query, docChanges.documentSet, oldDocs, changes, docChanges.mutatedKeys, newSyncState === 0 /* Local */, syncStateChanged,
/* excludesMetadataChanges= */ false);
return {
snapshot: snap,
limboChanges: limboChanges
};
}
};
/**
* Applies an OnlineState change to the view, potentially generating a
* ViewChange if the view's syncState changes as a result.
*/
View.prototype.applyOnlineStateChange = function (onlineState) {
if (this.current && onlineState === "Offline" /* Offline */) {
// If we're offline, set `current` to false and then call applyChanges()
// to refresh our syncState and generate a ViewChange as appropriate. We
// are guaranteed to get a new TargetChange that sets `current` back to
// true once the client is back online.
this.current = false;
return this.applyChanges({
documentSet: this.documentSet,
changeSet: new DocumentChangeSet(),
mutatedKeys: this.mutatedKeys,
needsRefill: false
},
/* updateLimboDocuments= */ false);
}
else {
// No effect, just return a no-op ViewChange.
return { limboChanges: [] };
}
};
/**
* Returns whether the doc for the given key should be in limbo.
*/
View.prototype.shouldBeInLimbo = function (key) {
// If the remote end says it's part of this query, it's not in limbo.
if (this._syncedDocuments.has(key)) {
return false;
}
// The local store doesn't think it's a result, so it shouldn't be in limbo.
if (!this.documentSet.has(key)) {
return false;
}
// If there are local changes to the doc, they might explain why the server
// doesn't know that it's part of the query. So don't put it in limbo.
// TODO(klimt): Ideally, we would only consider changes that might actually
// affect this specific query.
if (this.documentSet.get(key).hasLocalMutations) {
return false;
}
// Everything else is in limbo.
return true;
};
/**
* Updates syncedDocuments, current, and limbo docs based on the given change.
* Returns the list of changes to which docs are in limbo.
*/
View.prototype.applyTargetChange = function (targetChange) {
var _this = this;
if (targetChange) {
targetChange.addedDocuments.forEach(function (key) { return (_this._syncedDocuments = _this._syncedDocuments.add(key)); });
targetChange.modifiedDocuments.forEach(function (key) {
debugAssert(_this._syncedDocuments.has(key), "Modified document " + key + " not found in view.");
});
targetChange.removedDocuments.forEach(function (key) { return (_this._syncedDocuments = _this._syncedDocuments.delete(key)); });
this.current = targetChange.current;
}
};
View.prototype.updateLimboDocuments = function () {
var _this = this;
// We can only determine limbo documents when we're in-sync with the server.
if (!this.current) {
return [];
}
// TODO(klimt): Do this incrementally so that it's not quadratic when
// updating many documents.
var oldLimboDocuments = this.limboDocuments;
this.limboDocuments = documentKeySet();
this.documentSet.forEach(function (doc) {
if (_this.shouldBeInLimbo(doc.key)) {
_this.limboDocuments = _this.limboDocuments.add(doc.key);
}
});
// Diff the new limbo docs with the old limbo docs.
var changes = [];
oldLimboDocuments.forEach(function (key) {
if (!_this.limboDocuments.has(key)) {
changes.push(new RemovedLimboDocument(key));
}
});
this.limboDocuments.forEach(function (key) {
if (!oldLimboDocuments.has(key)) {
changes.push(new AddedLimboDocument(key));
}
});
return changes;
};
/**
* Update the in-memory state of the current view with the state read from
* persistence.
*
* We update the query view whenever a client's primary status changes:
* - When a client transitions from primary to secondary, it can miss
* LocalStorage updates and its query views may temporarily not be
* synchronized with the state on disk.
* - For secondary to primary transitions, the client needs to update the list
* of `syncedDocuments` since secondary clients update their query views
* based purely on synthesized RemoteEvents.
*
* @param queryResult.documents - The documents that match the query according
* to the LocalStore.
* @param queryResult.remoteKeys - The keys of the documents that match the
* query according to the backend.
*
* @return The ViewChange that resulted from this synchronization.
*/
// PORTING NOTE: Multi-tab only.
View.prototype.synchronizeWithPersistedState = function (queryResult) {
this._syncedDocuments = queryResult.remoteKeys;
this.limboDocuments = documentKeySet();
var docChanges = this.computeDocChanges(queryResult.documents);
return this.applyChanges(docChanges, /*updateLimboDocuments=*/ true);
};
/**
* Returns a view snapshot as if this query was just listened to. Contains
* a document add for every existing document and the `fromCache` and
* `hasPendingWrites` status of the already established view.
*/
// PORTING NOTE: Multi-tab only.
View.prototype.computeInitialSnapshot = function () {
return ViewSnapshot.fromInitialDocuments(this.query, this.documentSet, this.mutatedKeys, this.syncState === 0 /* Local */);
};
return View;
}());
function compareChangeType(c1, c2) {
var order = function (change) {
switch (change) {
case 0 /* Added */:
return 1;
case 2 /* Modified */:
return 2;
case 3 /* Metadata */:
// A metadata change is converted to a modified change at the public
// api layer. Since we sort by document key and then change type,
// metadata and modified changes must be sorted equivalently.
return 2;
case 1 /* Removed */:
return 0;
default:
return fail('Unknown ChangeType: ' + change);
}
};
return order(c1) - order(c2);
}
/**
* @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.
*/
var LOG_TAG$5 = 'ExponentialBackoff';
/**
* Initial backoff time in milliseconds after an error.
* Set to 1s according to https://cloud.google.com/apis/design/errors.
*/
var DEFAULT_BACKOFF_INITIAL_DELAY_MS = 1000;
var DEFAULT_BACKOFF_FACTOR = 1.5;
/** Maximum backoff time in milliseconds */
var DEFAULT_BACKOFF_MAX_DELAY_MS = 60 * 1000;
/**
* A helper for running delayed tasks following an exponential backoff curve
* between attempts.
*
* Each delay is made up of a "base" delay which follows the exponential
* backoff curve, and a +/- 50% "jitter" that is calculated and added to the
* base delay. This prevents clients from accidentally synchronizing their
* delays causing spikes of load to the backend.
*/
var ExponentialBackoff = /** @class */ (function () {
function ExponentialBackoff(
/**
* The AsyncQueue to run backoff operations on.
*/
queue,
/**
* The ID to use when scheduling backoff operations on the AsyncQueue.
*/
timerId,
/**
* The initial delay (used as the base delay on the first retry attempt).
* Note that jitter will still be applied, so the actual delay could be as
* little as 0.5*initialDelayMs.
*/
initialDelayMs,
/**
* The multiplier to use to determine the extended base delay after each
* attempt.
*/
backoffFactor,
/**
* The maximum base delay after which no further backoff is performed.
* Note that jitter will still be applied, so the actual delay could be as
* much as 1.5*maxDelayMs.
*/
maxDelayMs) {
if (initialDelayMs === void 0) { initialDelayMs = DEFAULT_BACKOFF_INITIAL_DELAY_MS; }
if (backoffFactor === void 0) { backoffFactor = DEFAULT_BACKOFF_FACTOR; }
if (maxDelayMs === void 0) { maxDelayMs = DEFAULT_BACKOFF_MAX_DELAY_MS; }
this.queue = queue;
this.timerId = timerId;
this.initialDelayMs = initialDelayMs;
this.backoffFactor = backoffFactor;
this.maxDelayMs = maxDelayMs;
this.currentBaseMs = 0;
this.timerPromise = null;
/** The last backoff attempt, as epoch milliseconds. */
this.lastAttemptTime = Date.now();
this.reset();
}
/**
* Resets the backoff delay.
*
* The very next backoffAndWait() will have no delay. If it is called again
* (i.e. due to an error), initialDelayMs (plus jitter) will be used, and
* subsequent ones will increase according to the backoffFactor.
*/
ExponentialBackoff.prototype.reset = function () {
this.currentBaseMs = 0;
};
/**
* Resets the backoff delay to the maximum delay (e.g. for use after a
* RESOURCE_EXHAUSTED error).
*/
ExponentialBackoff.prototype.resetToMax = function () {
this.currentBaseMs = this.maxDelayMs;
};
/**
* Returns a promise that resolves after currentDelayMs, and increases the
* delay for any subsequent attempts. If there was a pending backoff operation
* already, it will be canceled.
*/
ExponentialBackoff.prototype.backoffAndRun = function (op) {
var _this = this;
// Cancel any pending backoff operation.
this.cancel();
// First schedule using the current base (which may be 0 and should be
// honored as such).
var desiredDelayWithJitterMs = Math.floor(this.currentBaseMs + this.jitterDelayMs());
// Guard against lastAttemptTime being in the future due to a clock change.
var delaySoFarMs = Math.max(0, Date.now() - this.lastAttemptTime);
// Guard against the backoff delay already being past.
var remainingDelayMs = Math.max(0, desiredDelayWithJitterMs - delaySoFarMs);
if (remainingDelayMs > 0) {
logDebug(LOG_TAG$5, "Backing off for " + remainingDelayMs + " ms " +
("(base delay: " + this.currentBaseMs + " ms, ") +
("delay with jitter: " + desiredDelayWithJitterMs + " ms, ") +
("last attempt: " + delaySoFarMs + " ms ago)"));
}
this.timerPromise = this.queue.enqueueAfterDelay(this.timerId, remainingDelayMs, function () {
_this.lastAttemptTime = Date.now();
return op();
});
// Apply backoff factor to determine next delay and ensure it is within
// bounds.
this.currentBaseMs *= this.backoffFactor;
if (this.currentBaseMs < this.initialDelayMs) {
this.currentBaseMs = this.initialDelayMs;
}
if (this.currentBaseMs > this.maxDelayMs) {
this.currentBaseMs = this.maxDelayMs;
}
};
ExponentialBackoff.prototype.skipBackoff = function () {
if (this.timerPromise !== null) {
this.timerPromise.skipDelay();
this.timerPromise = null;
}
};
ExponentialBackoff.prototype.cancel = function () {
if (this.timerPromise !== null) {
this.timerPromise.cancel();
this.timerPromise = null;
}
};
/** Returns a random value in the range [-currentBaseMs/2, currentBaseMs/2] */
ExponentialBackoff.prototype.jitterDelayMs = function () {
return (Math.random() - 0.5) * this.currentBaseMs;
};
return ExponentialBackoff;
}());
/**
* @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.
*/
var LOG_TAG$6 = 'AsyncQueue';
/**
* Represents an operation scheduled to be run in the future on an AsyncQueue.
*
* It is created via DelayedOperation.createAndSchedule().
*
* Supports cancellation (via cancel()) and early execution (via skipDelay()).
*
* Note: We implement `PromiseLike` instead of `Promise`, as the `Promise` type
* in newer versions of TypeScript defines `finally`, which is not available in
* IE.
*/
var DelayedOperation = /** @class */ (function () {
function DelayedOperation(asyncQueue, timerId, targetTimeMs, op, removalCallback) {
this.asyncQueue = asyncQueue;
this.timerId = timerId;
this.targetTimeMs = targetTimeMs;
this.op = op;
this.removalCallback = removalCallback;
this.deferred = new Deferred();
this.then = this.deferred.promise.then.bind(this.deferred.promise);
// It's normal for the deferred promise to be canceled (due to cancellation)
// and so we attach a dummy catch callback to avoid
// 'UnhandledPromiseRejectionWarning' log spam.
this.deferred.promise.catch(function (err) { });
}
/**
* Creates and returns a DelayedOperation that has been scheduled to be
* executed on the provided asyncQueue after the provided delayMs.
*
* @param asyncQueue The queue to schedule the operation on.
* @param id A Timer ID identifying the type of operation this is.
* @param delayMs The delay (ms) before the operation should be scheduled.
* @param op The operation to run.
* @param removalCallback A callback to be called synchronously once the
* operation is executed or canceled, notifying the AsyncQueue to remove it
* from its delayedOperations list.
* PORTING NOTE: This exists to prevent making removeDelayedOperation() and
* the DelayedOperation class public.
*/
DelayedOperation.createAndSchedule = function (asyncQueue, timerId, delayMs, op, removalCallback) {
var targetTime = Date.now() + delayMs;
var delayedOp = new DelayedOperation(asyncQueue, timerId, targetTime, op, removalCallback);
delayedOp.start(delayMs);
return delayedOp;
};
/**
* Starts the timer. This is called immediately after construction by
* createAndSchedule().
*/
DelayedOperation.prototype.start = function (delayMs) {
var _this = this;
this.timerHandle = setTimeout(function () { return _this.handleDelayElapsed(); }, delayMs);
};
/**
* Queues the operation to run immediately (if it hasn't already been run or
* canceled).
*/
DelayedOperation.prototype.skipDelay = function () {
return this.handleDelayElapsed();
};
/**
* Cancels the operation if it hasn't already been executed or canceled. The
* promise will be rejected.
*
* As long as the operation has not yet been run, calling cancel() provides a
* guarantee that the operation will not be run.
*/
DelayedOperation.prototype.cancel = function (reason) {
if (this.timerHandle !== null) {
this.clearTimeout();
this.deferred.reject(new FirestoreError(Code.CANCELLED, 'Operation cancelled' + (reason ? ': ' + reason : '')));
}
};
DelayedOperation.prototype.handleDelayElapsed = function () {
var _this = this;
this.asyncQueue.enqueueAndForget(function () {
if (_this.timerHandle !== null) {
_this.clearTimeout();
return _this.op().then(function (result) {
return _this.deferred.resolve(result);
});
}
else {
return Promise.resolve();
}
});
};
DelayedOperation.prototype.clearTimeout = function () {
if (this.timerHandle !== null) {
this.removalCallback(this);
clearTimeout(this.timerHandle);
this.timerHandle = null;
}
};
return DelayedOperation;
}());
var AsyncQueue = /** @class */ (function () {
function AsyncQueue() {
var _this = this;
// The last promise in the queue.
this.tail = Promise.resolve();
// The last retryable operation. Retryable operation are run in order and
// retried with backoff.
this.retryableTail = Promise.resolve();
// Is this AsyncQueue being shut down? Once it is set to true, it will not
// be changed again.
this._isShuttingDown = false;
// Operations scheduled to be queued in the future. Operations are
// automatically removed after they are run or canceled.
this.delayedOperations = [];
// visible for testing
this.failure = null;
// Flag set while there's an outstanding AsyncQueue operation, used for
// assertion sanity-checks.
this.operationInProgress = false;
// List of TimerIds to fast-forward delays for.
this.timerIdsToSkip = [];
// Backoff timer used to schedule retries for retryable operations
this.backoff = new ExponentialBackoff(this, "async_queue_retry" /* AsyncQueueRetry */);
// Visibility handler that triggers an immediate retry of all retryable
// operations. Meant to speed up recovery when we regain file system access
// after page comes into foreground.
this.visibilityHandler = function () { return _this.backoff.skipBackoff(); };
var window = PlatformSupport.getPlatform().window;
if (window && typeof window.addEventListener === 'function') {
window.addEventListener('visibilitychange', this.visibilityHandler);
}
}
Object.defineProperty(AsyncQueue.prototype, "isShuttingDown", {
// Is this AsyncQueue being shut down? If true, this instance will not enqueue
// any new operations, Promises from enqueue requests will not resolve.
get: function () {
return this._isShuttingDown;
},
enumerable: true,
configurable: true
});
/**
* Adds a new operation to the queue without waiting for it to complete (i.e.
* we ignore the Promise result).
*/
AsyncQueue.prototype.enqueueAndForget = function (op) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.enqueue(op);
};
/**
* Regardless if the queue has initialized shutdown, adds a new operation to the
* queue without waiting for it to complete (i.e. we ignore the Promise result).
*/
AsyncQueue.prototype.enqueueAndForgetEvenAfterShutdown = function (op) {
this.verifyNotFailed();
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.enqueueInternal(op);
};
/**
* Regardless if the queue has initialized shutdown, adds a new operation to the
* queue.
*/
AsyncQueue.prototype.enqueueEvenAfterShutdown = function (op) {
this.verifyNotFailed();
return this.enqueueInternal(op);
};
/**
* Adds a new operation to the queue and initialize the shut down of this queue.
* Returns a promise that will be resolved when the promise returned by the new
* operation is (with its value).
* Once this method is called, the only possible way to request running an operation
* is through `enqueueAndForgetEvenAfterShutdown`.
*/
AsyncQueue.prototype.enqueueAndInitiateShutdown = function (op) {
return tslib.__awaiter(this, void 0, void 0, function () {
var window_1;
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
this.verifyNotFailed();
if (!!this._isShuttingDown) return [3 /*break*/, 2];
this._isShuttingDown = true;
window_1 = PlatformSupport.getPlatform().window;
if (window_1) {
window_1.removeEventListener('visibilitychange', this.visibilityHandler);
}
return [4 /*yield*/, this.enqueueEvenAfterShutdown(op)];
case 1:
_e.sent();
_e.label = 2;
case 2: return [2 /*return*/];
}
});
});
};
/**
* Adds a new operation to the queue. Returns a promise that will be resolved
* when the promise returned by the new operation is (with its value).
*/
AsyncQueue.prototype.enqueue = function (op) {
this.verifyNotFailed();
if (this._isShuttingDown) {
// Return a Promise which never resolves.
return new Promise(function (resolve) { });
}
return this.enqueueInternal(op);
};
/**
* Enqueue a retryable operation.
*
* A retryable operation is rescheduled with backoff if it fails with a
* IndexedDbTransactionError (the error type used by SimpleDb). All
* retryable operations are executed in order and only run if all prior
* operations were retried successfully.
*/
AsyncQueue.prototype.enqueueRetryable = function (op) {
var _this = this;
this.verifyNotFailed();
if (this._isShuttingDown) {
return;
}
this.retryableTail = this.retryableTail.then(function () {
var deferred = new Deferred();
var retryingOp = function () { return tslib.__awaiter(_this, void 0, void 0, function () {
var e_3;
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
_e.trys.push([0, 2, , 3]);
return [4 /*yield*/, op()];
case 1:
_e.sent();
deferred.resolve();
this.backoff.reset();
return [3 /*break*/, 3];
case 2:
e_3 = _e.sent();
if (isIndexedDbTransactionError(e_3)) {
logDebug(LOG_TAG$6, 'Operation failed with retryable error: ' + e_3);
this.backoff.backoffAndRun(retryingOp);
}
else {
deferred.resolve();
throw e_3; // Failure will be handled by AsyncQueue
}
return [3 /*break*/, 3];
case 3: return [2 /*return*/];
}
});
}); };
_this.enqueueAndForget(retryingOp);
return deferred.promise;
});
};
AsyncQueue.prototype.enqueueInternal = function (op) {
var _this = this;
var newTail = this.tail.then(function () {
_this.operationInProgress = true;
return op()
.catch(function (error) {
_this.failure = error;
_this.operationInProgress = false;
var message = error.stack || error.message || '';
logError('INTERNAL UNHANDLED ERROR: ', message);
// Re-throw the error so that this.tail becomes a rejected Promise and
// all further attempts to chain (via .then) will just short-circuit
// and return the rejected Promise.
throw error;
})
.then(function (result) {
_this.operationInProgress = false;
return result;
});
});
this.tail = newTail;
return newTail;
};
/**
* Schedules an operation to be queued on the AsyncQueue once the specified
* `delayMs` has elapsed. The returned DelayedOperation can be used to cancel
* or fast-forward the operation prior to its running.
*/
AsyncQueue.prototype.enqueueAfterDelay = function (timerId, delayMs, op) {
var _this = this;
this.verifyNotFailed();
debugAssert(delayMs >= 0, "Attempted to schedule an operation with a negative delay of " + delayMs);
// Fast-forward delays for timerIds that have been overriden.
if (this.timerIdsToSkip.indexOf(timerId) > -1) {
delayMs = 0;
}
var delayedOp = DelayedOperation.createAndSchedule(this, timerId, delayMs, op, function (removedOp) { return _this.removeDelayedOperation(removedOp); });
this.delayedOperations.push(delayedOp);
return delayedOp;
};
AsyncQueue.prototype.verifyNotFailed = function () {
if (this.failure) {
fail('AsyncQueue is already failed: ' +
(this.failure.stack || this.failure.message));
}
};
/**
* Verifies there's an operation currently in-progress on the AsyncQueue.
* Unfortunately we can't verify that the running code is in the promise chain
* of that operation, so this isn't a foolproof check, but it should be enough
* to catch some bugs.
*/
AsyncQueue.prototype.verifyOperationInProgress = function () {
debugAssert(this.operationInProgress, 'verifyOpInProgress() called when no op in progress on this queue.');
};
/**
* Waits until all currently queued tasks are finished executing. Delayed
* operations are not run.
*/
AsyncQueue.prototype.drain = function () {
return tslib.__awaiter(this, void 0, void 0, function () {
var currentTail;
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
currentTail = this.tail;
return [4 /*yield*/, currentTail];
case 1:
_e.sent();
_e.label = 2;
case 2:
if (currentTail !== this.tail) return [3 /*break*/, 0];
_e.label = 3;
case 3: return [2 /*return*/];
}
});
});
};
/**
* For Tests: Determine if a delayed operation with a particular TimerId
* exists.
*/
AsyncQueue.prototype.containsDelayedOperation = function (timerId) {
for (var _i = 0, _e = this.delayedOperations; _i < _e.length; _i++) {
var op = _e[_i];
if (op.timerId === timerId) {
return true;
}
}
return false;
};
/**
* For Tests: Runs some or all delayed operations early.
*
* @param lastTimerId Delayed operations up to and including this TimerId will
* be drained. Pass TimerId.All to run all delayed operations.
* @returns a Promise that resolves once all operations have been run.
*/
AsyncQueue.prototype.runAllDelayedOperationsUntil = function (lastTimerId) {
var _this = this;
// Note that draining may generate more delayed ops, so we do that first.
return this.drain().then(function () {
// Run ops in the same order they'd run if they ran naturally.
_this.delayedOperations.sort(function (a, b) { return a.targetTimeMs - b.targetTimeMs; });
for (var _i = 0, _e = _this.delayedOperations; _i < _e.length; _i++) {
var op = _e[_i];
op.skipDelay();
if (lastTimerId !== "all" /* All */ && op.timerId === lastTimerId) {
break;
}
}
return _this.drain();
});
};
/**
* For Tests: Skip all subsequent delays for a timer id.
*/
AsyncQueue.prototype.skipDelaysForTimerId = function (timerId) {
this.timerIdsToSkip.push(timerId);
};
/** Called once a DelayedOperation is run or canceled. */
AsyncQueue.prototype.removeDelayedOperation = function (op) {
// NOTE: indexOf / slice are O(n), but delayedOperations is expected to be small.
var index = this.delayedOperations.indexOf(op);
debugAssert(index >= 0, 'Delayed operation not found.');
this.delayedOperations.splice(index, 1);
};
return AsyncQueue;
}());
/**
* Returns a FirestoreError that can be surfaced to the user if the provided
* error is an IndexedDbTransactionError. Re-throws the error otherwise.
*/
function wrapInUserErrorIfRecoverable(e, msg) {
logError(LOG_TAG$6, msg + ": " + e);
if (isIndexedDbTransactionError(e)) {
return new FirestoreError(Code.UNAVAILABLE, msg + ": " + e);
}
else {
throw e;
}
}
/**
* @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.
*/
/**
* Error Codes describing the different ways GRPC can fail. These are copied
* directly from GRPC's sources here:
*
* https://github.com/grpc/grpc/blob/bceec94ea4fc5f0085d81235d8e1c06798dc341a/include/grpc%2B%2B/impl/codegen/status_code_enum.h
*
* Important! The names of these identifiers matter because the string forms
* are used for reverse lookups from the webchannel stream. Do NOT change the
* names of these identifiers or change this into a const enum.
*/
var RpcCode;
(function (RpcCode) {
RpcCode[RpcCode["OK"] = 0] = "OK";
RpcCode[RpcCode["CANCELLED"] = 1] = "CANCELLED";
RpcCode[RpcCode["UNKNOWN"] = 2] = "UNKNOWN";
RpcCode[RpcCode["INVALID_ARGUMENT"] = 3] = "INVALID_ARGUMENT";
RpcCode[RpcCode["DEADLINE_EXCEEDED"] = 4] = "DEADLINE_EXCEEDED";
RpcCode[RpcCode["NOT_FOUND"] = 5] = "NOT_FOUND";
RpcCode[RpcCode["ALREADY_EXISTS"] = 6] = "ALREADY_EXISTS";
RpcCode[RpcCode["PERMISSION_DENIED"] = 7] = "PERMISSION_DENIED";
RpcCode[RpcCode["UNAUTHENTICATED"] = 16] = "UNAUTHENTICATED";
RpcCode[RpcCode["RESOURCE_EXHAUSTED"] = 8] = "RESOURCE_EXHAUSTED";
RpcCode[RpcCode["FAILED_PRECONDITION"] = 9] = "FAILED_PRECONDITION";
RpcCode[RpcCode["ABORTED"] = 10] = "ABORTED";
RpcCode[RpcCode["OUT_OF_RANGE"] = 11] = "OUT_OF_RANGE";
RpcCode[RpcCode["UNIMPLEMENTED"] = 12] = "UNIMPLEMENTED";
RpcCode[RpcCode["INTERNAL"] = 13] = "INTERNAL";
RpcCode[RpcCode["UNAVAILABLE"] = 14] = "UNAVAILABLE";
RpcCode[RpcCode["DATA_LOSS"] = 15] = "DATA_LOSS";
})(RpcCode || (RpcCode = {}));
/**
* Determines whether an error code represents a permanent error when received
* in response to a non-write operation.
*
* See isPermanentWriteError for classifying write errors.
*/
function isPermanentError(code) {
switch (code) {
case Code.OK:
return fail('Treated status OK as error');
case Code.CANCELLED:
case Code.UNKNOWN:
case Code.DEADLINE_EXCEEDED:
case Code.RESOURCE_EXHAUSTED:
case Code.INTERNAL:
case Code.UNAVAILABLE:
// Unauthenticated means something went wrong with our token and we need
// to retry with new credentials which will happen automatically.
case Code.UNAUTHENTICATED:
return false;
case Code.INVALID_ARGUMENT:
case Code.NOT_FOUND:
case Code.ALREADY_EXISTS:
case Code.PERMISSION_DENIED:
case Code.FAILED_PRECONDITION:
// Aborted might be retried in some scenarios, but that is dependant on
// the context and should handled individually by the calling code.
// See https://cloud.google.com/apis/design/errors.
case Code.ABORTED:
case Code.OUT_OF_RANGE:
case Code.UNIMPLEMENTED:
case Code.DATA_LOSS:
return true;
default:
return fail('Unknown status code: ' + code);
}
}
/**
* Determines whether an error code represents a permanent error when received
* in response to a write operation.
*
* Write operations must be handled specially because as of b/119437764, ABORTED
* errors on the write stream should be retried too (even though ABORTED errors
* are not generally retryable).
*
* Note that during the initial handshake on the write stream an ABORTED error
* signals that we should discard our stream token (i.e. it is permanent). This
* means a handshake error should be classified with isPermanentError, above.
*/
function isPermanentWriteError(code) {
return isPermanentError(code) && code !== Code.ABORTED;
}
/**
* Maps an error Code from GRPC status code number, like 0, 1, or 14. These
* are not the same as HTTP status codes.
*
* @returns The Code equivalent to the given GRPC status code. Fails if there
* is no match.
*/
function mapCodeFromRpcCode(code) {
if (code === undefined) {
// This shouldn't normally happen, but in certain error cases (like trying
// to send invalid proto messages) we may get an error with no GRPC code.
logError('GRPC error has no .code');
return Code.UNKNOWN;
}
switch (code) {
case RpcCode.OK:
return Code.OK;
case RpcCode.CANCELLED:
return Code.CANCELLED;
case RpcCode.UNKNOWN:
return Code.UNKNOWN;
case RpcCode.DEADLINE_EXCEEDED:
return Code.DEADLINE_EXCEEDED;
case RpcCode.RESOURCE_EXHAUSTED:
return Code.RESOURCE_EXHAUSTED;
case RpcCode.INTERNAL:
return Code.INTERNAL;
case RpcCode.UNAVAILABLE:
return Code.UNAVAILABLE;
case RpcCode.UNAUTHENTICATED:
return Code.UNAUTHENTICATED;
case RpcCode.INVALID_ARGUMENT:
return Code.INVALID_ARGUMENT;
case RpcCode.NOT_FOUND:
return Code.NOT_FOUND;
case RpcCode.ALREADY_EXISTS:
return Code.ALREADY_EXISTS;
case RpcCode.PERMISSION_DENIED:
return Code.PERMISSION_DENIED;
case RpcCode.FAILED_PRECONDITION:
return Code.FAILED_PRECONDITION;
case RpcCode.ABORTED:
return Code.ABORTED;
case RpcCode.OUT_OF_RANGE:
return Code.OUT_OF_RANGE;
case RpcCode.UNIMPLEMENTED:
return Code.UNIMPLEMENTED;
case RpcCode.DATA_LOSS:
return Code.DATA_LOSS;
default:
return fail('Unknown status code: ' + code);
}
}
/**
* @license
* Copyright 2019 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.
*/
var RETRY_COUNT = 5;
/**
* TransactionRunner encapsulates the logic needed to run and retry transactions
* with backoff.
*/
var TransactionRunner = /** @class */ (function () {
function TransactionRunner(asyncQueue, remoteStore, updateFunction, deferred) {
this.asyncQueue = asyncQueue;
this.remoteStore = remoteStore;
this.updateFunction = updateFunction;
this.deferred = deferred;
this.retries = RETRY_COUNT;
this.backoff = new ExponentialBackoff(this.asyncQueue, "transaction_retry" /* TransactionRetry */);
}
/** Runs the transaction and sets the result on deferred. */
TransactionRunner.prototype.run = function () {
this.runWithBackOff();
};
TransactionRunner.prototype.runWithBackOff = function () {
var _this = this;
this.backoff.backoffAndRun(function () { return tslib.__awaiter(_this, void 0, void 0, function () {
var transaction, userPromise;
var _this = this;
return tslib.__generator(this, function (_e) {
transaction = this.remoteStore.createTransaction();
userPromise = this.tryRunUpdateFunction(transaction);
if (userPromise) {
userPromise
.then(function (result) {
_this.asyncQueue.enqueueAndForget(function () {
return transaction
.commit()
.then(function () {
_this.deferred.resolve(result);
})
.catch(function (commitError) {
_this.handleTransactionError(commitError);
});
});
})
.catch(function (userPromiseError) {
_this.handleTransactionError(userPromiseError);
});
}
return [2 /*return*/];
});
}); });
};
TransactionRunner.prototype.tryRunUpdateFunction = function (transaction) {
try {
var userPromise = this.updateFunction(transaction);
if (isNullOrUndefined(userPromise) ||
!userPromise.catch ||
!userPromise.then) {
this.deferred.reject(Error('Transaction callback must return a Promise'));
return null;
}
return userPromise;
}
catch (error) {
// Do not retry errors thrown by user provided updateFunction.
this.deferred.reject(error);
return null;
}
};
TransactionRunner.prototype.handleTransactionError = function (error) {
var _this = this;
if (this.retries > 0 && this.isRetryableTransactionError(error)) {
this.retries -= 1;
this.asyncQueue.enqueueAndForget(function () {
_this.runWithBackOff();
return Promise.resolve();
});
}
else {
this.deferred.reject(error);
}
};
TransactionRunner.prototype.isRetryableTransactionError = function (error) {
if (error.name === 'FirebaseError') {
// In transactions, the backend will fail outdated reads with FAILED_PRECONDITION and
// non-matching document versions with ABORTED. These errors should be retried.
var code = error.code;
return (code === 'aborted' ||
code === 'failed-precondition' ||
!isPermanentError(code));
}
return false;
};
return TransactionRunner;
}());
/**
* @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.
*/
var LOG_TAG$7 = 'SyncEngine';
/**
* QueryView contains all of the data that SyncEngine needs to keep track of for
* a particular query.
*/
var QueryView = /** @class */ (function () {
function QueryView(
/**
* The query itself.
*/
query,
/**
* The target number created by the client that is used in the watch
* stream to identify this query.
*/
targetId,
/**
* The view is responsible for computing the final merged truth of what
* docs are in the query. It gets notified of local and remote changes,
* and applies the query filters and limits to determine the most correct
* possible results.
*/
view) {
this.query = query;
this.targetId = targetId;
this.view = view;
}
return QueryView;
}());
/** Tracks a limbo resolution. */
var LimboResolution = /** @class */ (function () {
function LimboResolution(key) {
this.key = key;
/**
* Set to true once we've received a document. This is used in
* getRemoteKeysForTarget() and ultimately used by WatchChangeAggregator to
* decide whether it needs to manufacture a delete event for the target once
* the target is CURRENT.
*/
this.receivedDocument = false;
}
return LimboResolution;
}());
/**
* SyncEngine is the central controller in the client SDK architecture. It is
* the glue code between the EventManager, LocalStore, and RemoteStore. Some of
* SyncEngine's responsibilities include:
* 1. Coordinating client requests and remote events between the EventManager
* and the local and remote data stores.
* 2. Managing a View object for each query, providing the unified view between
* the local and remote data stores.
* 3. Notifying the RemoteStore when the LocalStore has new mutations in its
* queue that need sending to the backend.
*
* The SyncEngines methods should only ever be called by methods running in the
* global async queue.
*/
var SyncEngine = /** @class */ (function () {
function SyncEngine(localStore, remoteStore,
// PORTING NOTE: Manages state synchronization in multi-tab environments.
sharedClientState, currentUser, maxConcurrentLimboResolutions) {
this.localStore = localStore;
this.remoteStore = remoteStore;
this.sharedClientState = sharedClientState;
this.currentUser = currentUser;
this.maxConcurrentLimboResolutions = maxConcurrentLimboResolutions;
this.syncEngineListener = null;
this.queryViewsByQuery = new ObjectMap(function (q) { return q.canonicalId(); });
this.queriesByTarget = new Map();
/**
* The keys of documents that are in limbo for which we haven't yet started a
* limbo resolution query.
*/
this.enqueuedLimboResolutions = [];
/**
* Keeps track of the target ID for each document that is in limbo with an
* active target.
*/
this.activeLimboTargetsByKey = new SortedMap(DocumentKey.comparator);
/**
* Keeps track of the information about an active limbo resolution for each
* active target ID that was started for the purpose of limbo resolution.
*/
this.activeLimboResolutionsByTarget = new Map();
this.limboDocumentRefs = new ReferenceSet();
/** Stores user completion handlers, indexed by User and BatchId. */
this.mutationUserCallbacks = {};
/** Stores user callbacks waiting for all pending writes to be acknowledged. */
this.pendingWritesCallbacks = new Map();
this.limboTargetIdGenerator = TargetIdGenerator.forSyncEngine();
this.onlineState = "Unknown" /* Unknown */;
}
Object.defineProperty(SyncEngine.prototype, "isPrimaryClient", {
get: function () {
return true;
},
enumerable: true,
configurable: true
});
/** Subscribes to SyncEngine notifications. Has to be called exactly once. */
SyncEngine.prototype.subscribe = function (syncEngineListener) {
debugAssert(syncEngineListener !== null, 'SyncEngine listener cannot be null');
debugAssert(this.syncEngineListener === null, 'SyncEngine already has a subscriber.');
this.syncEngineListener = syncEngineListener;
};
/**
* Initiates the new listen, resolves promise when listen enqueued to the
* server. All the subsequent view snapshots or errors are sent to the
* subscribed handlers. Returns the initial snapshot.
*/
SyncEngine.prototype.listen = function (query) {
return tslib.__awaiter(this, void 0, void 0, function () {
var targetId, viewSnapshot, queryView, targetData, status_1;
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
this.assertSubscribed('listen()');
queryView = this.queryViewsByQuery.get(query);
if (!queryView) return [3 /*break*/, 1];
// PORTING NOTE: With Multi-Tab Web, it is possible that a query view
// already exists when EventManager calls us for the first time. This
// happens when the primary tab is already listening to this query on
// behalf of another tab and the user of the primary also starts listening
// to the query. EventManager will not have an assigned target ID in this
// case and calls `listen` to obtain this ID.
targetId = queryView.targetId;
this.sharedClientState.addLocalQueryTarget(targetId);
viewSnapshot = queryView.view.computeInitialSnapshot();
return [3 /*break*/, 4];
case 1: return [4 /*yield*/, this.localStore.allocateTarget(query.toTarget())];
case 2:
targetData = _e.sent();
status_1 = this.sharedClientState.addLocalQueryTarget(targetData.targetId);
targetId = targetData.targetId;
return [4 /*yield*/, this.initializeViewAndComputeSnapshot(query, targetId, status_1 === 'current')];
case 3:
viewSnapshot = _e.sent();
if (this.isPrimaryClient) {
this.remoteStore.listen(targetData);
}
_e.label = 4;
case 4: return [2 /*return*/, viewSnapshot];
}
});
});
};
/**
* Registers a view for a previously unknown query and computes its initial
* snapshot.
*/
SyncEngine.prototype.initializeViewAndComputeSnapshot = function (query, targetId, current) {
return tslib.__awaiter(this, void 0, void 0, function () {
var queryResult, view, viewDocChanges, synthesizedTargetChange, viewChange, data;
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0: return [4 /*yield*/, this.localStore.executeQuery(query,
/* usePreviousResults= */ true)];
case 1:
queryResult = _e.sent();
view = new View(query, queryResult.remoteKeys);
viewDocChanges = view.computeDocChanges(queryResult.documents);
synthesizedTargetChange = TargetChange.createSynthesizedTargetChangeForCurrentChange(targetId, current && this.onlineState !== "Offline" /* Offline */);
viewChange = view.applyChanges(viewDocChanges,
/* updateLimboDocuments= */ this.isPrimaryClient, synthesizedTargetChange);
this.updateTrackedLimbos(targetId, viewChange.limboChanges);
debugAssert(!!viewChange.snapshot, 'applyChanges for new view should always return a snapshot');
data = new QueryView(query, targetId, view);
this.queryViewsByQuery.set(query, data);
if (this.queriesByTarget.has(targetId)) {
this.queriesByTarget.get(targetId).push(query);
}
else {
this.queriesByTarget.set(targetId, [query]);
}
return [2 /*return*/, viewChange.snapshot];
}
});
});
};
/** Stops listening to the query. */
SyncEngine.prototype.unlisten = function (query) {
return tslib.__awaiter(this, void 0, void 0, function () {
var queryView, queries, targetRemainsActive;
var _this = this;
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
this.assertSubscribed('unlisten()');
queryView = this.queryViewsByQuery.get(query);
debugAssert(!!queryView, 'Trying to unlisten on query not found:' + query);
queries = this.queriesByTarget.get(queryView.targetId);
if (queries.length > 1) {
this.queriesByTarget.set(queryView.targetId, queries.filter(function (q) { return !q.isEqual(query); }));
this.queryViewsByQuery.delete(query);
return [2 /*return*/];
}
if (!this.isPrimaryClient) return [3 /*break*/, 3];
// We need to remove the local query target first to allow us to verify
// whether any other client is still interested in this target.
this.sharedClientState.removeLocalQueryTarget(queryView.targetId);
targetRemainsActive = this.sharedClientState.isActiveQueryTarget(queryView.targetId);
if (!!targetRemainsActive) return [3 /*break*/, 2];
return [4 /*yield*/, this.localStore
.releaseTarget(queryView.targetId, /*keepPersistedTargetData=*/ false)
.then(function () {
_this.sharedClientState.clearQueryState(queryView.targetId);
_this.remoteStore.unlisten(queryView.targetId);
_this.removeAndCleanupTarget(queryView.targetId);
})
.catch(ignoreIfPrimaryLeaseLoss)];
case 1:
_e.sent();
_e.label = 2;
case 2: return [3 /*break*/, 5];
case 3:
this.removeAndCleanupTarget(queryView.targetId);
return [4 /*yield*/, this.localStore.releaseTarget(queryView.targetId,
/*keepPersistedTargetData=*/ true)];
case 4:
_e.sent();
_e.label = 5;
case 5: return [2 /*return*/];
}
});
});
};
/**
* Initiates the write of local mutation batch which involves adding the
* writes to the mutation queue, notifying the remote store about new
* mutations and raising events for any changes this write caused.
*
* The promise returned by this call is resolved when the above steps
* have completed, *not* when the write was acked by the backend. The
* userCallback is resolved once the write was acked/rejected by the
* backend (or failed locally for any other reason).
*/
SyncEngine.prototype.write = function (batch, userCallback) {
return tslib.__awaiter(this, void 0, void 0, function () {
var result, e_4, error;
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
this.assertSubscribed('write()');
_e.label = 1;
case 1:
_e.trys.push([1, 5, , 6]);
return [4 /*yield*/, this.localStore.localWrite(batch)];
case 2:
result = _e.sent();
this.sharedClientState.addPendingMutation(result.batchId);
this.addMutationCallback(result.batchId, userCallback);
return [4 /*yield*/, this.emitNewSnapsAndNotifyLocalStore(result.changes)];
case 3:
_e.sent();
return [4 /*yield*/, this.remoteStore.fillWritePipeline()];
case 4:
_e.sent();
return [3 /*break*/, 6];
case 5:
e_4 = _e.sent();
error = wrapInUserErrorIfRecoverable(e_4, "Failed to persist write");
userCallback.reject(error);
return [3 /*break*/, 6];
case 6: return [2 /*return*/];
}
});
});
};
/**
* Takes an updateFunction in which a set of reads and writes can be performed
* atomically. In the updateFunction, the client can read and write values
* using the supplied transaction object. After the updateFunction, all
* changes will be committed. If a retryable error occurs (ex: some other
* client has changed any of the data referenced), then the updateFunction
* will be called again after a backoff. If the updateFunction still fails
* after all retries, then the transaction will be rejected.
*
* The transaction object passed to the updateFunction contains methods for
* accessing documents and collections. Unlike other datastore access, data
* accessed with the transaction will not reflect local changes that have not
* been committed. For this reason, it is required that all reads are
* performed before any writes. Transactions must be performed while online.
*
* The Deferred input is resolved when the transaction is fully committed.
*/
SyncEngine.prototype.runTransaction = function (asyncQueue, updateFunction, deferred) {
new TransactionRunner(asyncQueue, this.remoteStore, updateFunction, deferred).run();
};
SyncEngine.prototype.applyRemoteEvent = function (remoteEvent) {
return tslib.__awaiter(this, void 0, void 0, function () {
var changes, error_2;
var _this = this;
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
this.assertSubscribed('applyRemoteEvent()');
_e.label = 1;
case 1:
_e.trys.push([1, 4, , 6]);
return [4 /*yield*/, this.localStore.applyRemoteEvent(remoteEvent)];
case 2:
changes = _e.sent();
// Update `receivedDocument` as appropriate for any limbo targets.
remoteEvent.targetChanges.forEach(function (targetChange, targetId) {
var limboResolution = _this.activeLimboResolutionsByTarget.get(targetId);
if (limboResolution) {
// Since this is a limbo resolution lookup, it's for a single document
// and it could be added, modified, or removed, but not a combination.
hardAssert(targetChange.addedDocuments.size +
targetChange.modifiedDocuments.size +
targetChange.removedDocuments.size <=
1, 'Limbo resolution for single document contains multiple changes.');
if (targetChange.addedDocuments.size > 0) {
limboResolution.receivedDocument = true;
}
else if (targetChange.modifiedDocuments.size > 0) {
hardAssert(limboResolution.receivedDocument, 'Received change for limbo target document without add.');
}
else if (targetChange.removedDocuments.size > 0) {
hardAssert(limboResolution.receivedDocument, 'Received remove for limbo target document without add.');
limboResolution.receivedDocument = false;
}
}
});
return [4 /*yield*/, this.emitNewSnapsAndNotifyLocalStore(changes, remoteEvent)];
case 3:
_e.sent();
return [3 /*break*/, 6];
case 4:
error_2 = _e.sent();
return [4 /*yield*/, ignoreIfPrimaryLeaseLoss(error_2)];
case 5:
_e.sent();
return [3 /*break*/, 6];
case 6: return [2 /*return*/];
}
});
});
};
/**
* Applies an OnlineState change to the sync engine and notifies any views of
* the change.
*/
SyncEngine.prototype.applyOnlineStateChange = function (onlineState, source) {
this.assertSubscribed('applyOnlineStateChange()');
var newViewSnapshots = [];
this.queryViewsByQuery.forEach(function (query, queryView) {
var viewChange = queryView.view.applyOnlineStateChange(onlineState);
debugAssert(viewChange.limboChanges.length === 0, 'OnlineState should not affect limbo documents.');
if (viewChange.snapshot) {
newViewSnapshots.push(viewChange.snapshot);
}
});
this.syncEngineListener.onOnlineStateChange(onlineState);
this.syncEngineListener.onWatchChange(newViewSnapshots);
this.onlineState = onlineState;
};
SyncEngine.prototype.rejectListen = function (targetId, err) {
return tslib.__awaiter(this, void 0, void 0, function () {
var limboResolution, limboKey, documentUpdates, resolvedLimboDocuments, event_2;
var _this = this;
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
this.assertSubscribed('rejectListens()');
// PORTING NOTE: Multi-tab only.
this.sharedClientState.updateQueryState(targetId, 'rejected', err);
limboResolution = this.activeLimboResolutionsByTarget.get(targetId);
limboKey = limboResolution && limboResolution.key;
if (!limboKey) return [3 /*break*/, 2];
documentUpdates = new SortedMap(DocumentKey.comparator);
documentUpdates = documentUpdates.insert(limboKey, new NoDocument(limboKey, SnapshotVersion.min()));
resolvedLimboDocuments = documentKeySet().add(limboKey);
event_2 = new RemoteEvent(SnapshotVersion.min(),
/* targetChanges= */ new Map(),
/* targetMismatches= */ new SortedSet(primitiveComparator), documentUpdates, resolvedLimboDocuments);
return [4 /*yield*/, this.applyRemoteEvent(event_2)];
case 1:
_e.sent();
// Since this query failed, we won't want to manually unlisten to it.
// We only remove it from bookkeeping after we successfully applied the
// RemoteEvent. If `applyRemoteEvent()` throws, we want to re-listen to
// this query when the RemoteStore restarts the Watch stream, which should
// re-trigger the target failure.
this.activeLimboTargetsByKey = this.activeLimboTargetsByKey.remove(limboKey);
this.activeLimboResolutionsByTarget.delete(targetId);
this.pumpEnqueuedLimboResolutions();
return [3 /*break*/, 4];
case 2: return [4 /*yield*/, this.localStore
.releaseTarget(targetId, /* keepPersistedTargetData */ false)
.then(function () { return _this.removeAndCleanupTarget(targetId, err); })
.catch(ignoreIfPrimaryLeaseLoss)];
case 3:
_e.sent();
_e.label = 4;
case 4: return [2 /*return*/];
}
});
});
};
SyncEngine.prototype.applySuccessfulWrite = function (mutationBatchResult) {
return tslib.__awaiter(this, void 0, void 0, function () {
var batchId, changes, error_3;
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
this.assertSubscribed('applySuccessfulWrite()');
batchId = mutationBatchResult.batch.batchId;
// The local store may or may not be able to apply the write result and
// raise events immediately (depending on whether the watcher is caught
// up), so we raise user callbacks first so that they consistently happen
// before listen events.
this.processUserCallback(batchId, /*error=*/ null);
this.triggerPendingWritesCallbacks(batchId);
_e.label = 1;
case 1:
_e.trys.push([1, 4, , 6]);
return [4 /*yield*/, this.localStore.acknowledgeBatch(mutationBatchResult)];
case 2:
changes = _e.sent();
this.sharedClientState.updateMutationState(batchId, 'acknowledged');
return [4 /*yield*/, this.emitNewSnapsAndNotifyLocalStore(changes)];
case 3:
_e.sent();
return [3 /*break*/, 6];
case 4:
error_3 = _e.sent();
return [4 /*yield*/, ignoreIfPrimaryLeaseLoss(error_3)];
case 5:
_e.sent();
return [3 /*break*/, 6];
case 6: return [2 /*return*/];
}
});
});
};
SyncEngine.prototype.rejectFailedWrite = function (batchId, error) {
return tslib.__awaiter(this, void 0, void 0, function () {
var changes, error_4;
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
this.assertSubscribed('rejectFailedWrite()');
// The local store may or may not be able to apply the write result and
// raise events immediately (depending on whether the watcher is caught up),
// so we raise user callbacks first so that they consistently happen before
// listen events.
this.processUserCallback(batchId, error);
this.triggerPendingWritesCallbacks(batchId);
_e.label = 1;
case 1:
_e.trys.push([1, 4, , 6]);
return [4 /*yield*/, this.localStore.rejectBatch(batchId)];
case 2:
changes = _e.sent();
this.sharedClientState.updateMutationState(batchId, 'rejected', error);
return [4 /*yield*/, this.emitNewSnapsAndNotifyLocalStore(changes)];
case 3:
_e.sent();
return [3 /*break*/, 6];
case 4:
error_4 = _e.sent();
return [4 /*yield*/, ignoreIfPrimaryLeaseLoss(error_4)];
case 5:
_e.sent();
return [3 /*break*/, 6];
case 6: return [2 /*return*/];
}
});
});
};
/**
* Registers a user callback that resolves when all pending mutations at the moment of calling
* are acknowledged .
*/
SyncEngine.prototype.registerPendingWritesCallback = function (callback) {
return tslib.__awaiter(this, void 0, void 0, function () {
var highestBatchId, callbacks, e_5, firestoreError;
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
if (!this.remoteStore.canUseNetwork()) {
logDebug(LOG_TAG$7, 'The network is disabled. The task returned by ' +
"'awaitPendingWrites()' will not complete until the network is enabled.");
}
_e.label = 1;
case 1:
_e.trys.push([1, 3, , 4]);
return [4 /*yield*/, this.localStore.getHighestUnacknowledgedBatchId()];
case 2:
highestBatchId = _e.sent();
if (highestBatchId === BATCHID_UNKNOWN) {
// Trigger the callback right away if there is no pending writes at the moment.
callback.resolve();
return [2 /*return*/];
}
callbacks = this.pendingWritesCallbacks.get(highestBatchId) || [];
callbacks.push(callback);
this.pendingWritesCallbacks.set(highestBatchId, callbacks);
return [3 /*break*/, 4];
case 3:
e_5 = _e.sent();
firestoreError = wrapInUserErrorIfRecoverable(e_5, 'Initialization of waitForPendingWrites() operation failed');
callback.reject(firestoreError);
return [3 /*break*/, 4];
case 4: return [2 /*return*/];
}
});
});
};
/**
* Triggers the callbacks that are waiting for this batch id to get acknowledged by server,
* if there are any.
*/
SyncEngine.prototype.triggerPendingWritesCallbacks = function (batchId) {
(this.pendingWritesCallbacks.get(batchId) || []).forEach(function (callback) {
callback.resolve();
});
this.pendingWritesCallbacks.delete(batchId);
};
/** Reject all outstanding callbacks waiting for pending writes to complete. */
SyncEngine.prototype.rejectOutstandingPendingWritesCallbacks = function (errorMessage) {
this.pendingWritesCallbacks.forEach(function (callbacks) {
callbacks.forEach(function (callback) {
callback.reject(new FirestoreError(Code.CANCELLED, errorMessage));
});
});
this.pendingWritesCallbacks.clear();
};
SyncEngine.prototype.addMutationCallback = function (batchId, callback) {
var newCallbacks = this.mutationUserCallbacks[this.currentUser.toKey()];
if (!newCallbacks) {
newCallbacks = new SortedMap(primitiveComparator);
}
newCallbacks = newCallbacks.insert(batchId, callback);
this.mutationUserCallbacks[this.currentUser.toKey()] = newCallbacks;
};
/**
* Resolves or rejects the user callback for the given batch and then discards
* it.
*/
SyncEngine.prototype.processUserCallback = function (batchId, error) {
var newCallbacks = this.mutationUserCallbacks[this.currentUser.toKey()];
// NOTE: Mutations restored from persistence won't have callbacks, so it's
// okay for there to be no callback for this ID.
if (newCallbacks) {
var callback = newCallbacks.get(batchId);
if (callback) {
debugAssert(batchId === newCallbacks.minKey(), 'Mutation callbacks processed out-of-order?');
if (error) {
callback.reject(error);
}
else {
callback.resolve();
}
newCallbacks = newCallbacks.remove(batchId);
}
this.mutationUserCallbacks[this.currentUser.toKey()] = newCallbacks;
}
};
SyncEngine.prototype.removeAndCleanupTarget = function (targetId, error) {
var _this = this;
if (error === void 0) { error = null; }
this.sharedClientState.removeLocalQueryTarget(targetId);
debugAssert(this.queriesByTarget.has(targetId) &&
this.queriesByTarget.get(targetId).length !== 0, "There are no queries mapped to target id " + targetId);
for (var _i = 0, _e = this.queriesByTarget.get(targetId); _i < _e.length; _i++) {
var query = _e[_i];
this.queryViewsByQuery.delete(query);
if (error) {
this.syncEngineListener.onWatchError(query, error);
}
}
this.queriesByTarget.delete(targetId);
if (this.isPrimaryClient) {
var limboKeys = this.limboDocumentRefs.removeReferencesForId(targetId);
limboKeys.forEach(function (limboKey) {
var isReferenced = _this.limboDocumentRefs.containsKey(limboKey);
if (!isReferenced) {
// We removed the last reference for this key
_this.removeLimboTarget(limboKey);
}
});
}
};
SyncEngine.prototype.removeLimboTarget = function (key) {
// It's possible that the target already got removed because the query failed. In that case,
// the key won't exist in `limboTargetsByKey`. Only do the cleanup if we still have the target.
var limboTargetId = this.activeLimboTargetsByKey.get(key);
if (limboTargetId === null) {
// This target already got removed, because the query failed.
return;
}
this.remoteStore.unlisten(limboTargetId);
this.activeLimboTargetsByKey = this.activeLimboTargetsByKey.remove(key);
this.activeLimboResolutionsByTarget.delete(limboTargetId);
this.pumpEnqueuedLimboResolutions();
};
SyncEngine.prototype.updateTrackedLimbos = function (targetId, limboChanges) {
for (var _i = 0, limboChanges_1 = limboChanges; _i < limboChanges_1.length; _i++) {
var limboChange = limboChanges_1[_i];
if (limboChange instanceof AddedLimboDocument) {
this.limboDocumentRefs.addReference(limboChange.key, targetId);
this.trackLimboChange(limboChange);
}
else if (limboChange instanceof RemovedLimboDocument) {
logDebug(LOG_TAG$7, 'Document no longer in limbo: ' + limboChange.key);
this.limboDocumentRefs.removeReference(limboChange.key, targetId);
var isReferenced = this.limboDocumentRefs.containsKey(limboChange.key);
if (!isReferenced) {
// We removed the last reference for this key
this.removeLimboTarget(limboChange.key);
}
}
else {
fail('Unknown limbo change: ' + JSON.stringify(limboChange));
}
}
};
SyncEngine.prototype.trackLimboChange = function (limboChange) {
var key = limboChange.key;
if (!this.activeLimboTargetsByKey.get(key)) {
logDebug(LOG_TAG$7, 'New document in limbo: ' + key);
this.enqueuedLimboResolutions.push(key);
this.pumpEnqueuedLimboResolutions();
}
};
/**
* Starts listens for documents in limbo that are enqueued for resolution,
* subject to a maximum number of concurrent resolutions.
*
* Without bounding the number of concurrent resolutions, the server can fail
* with "resource exhausted" errors which can lead to pathological client
* behavior as seen in https://github.com/firebase/firebase-js-sdk/issues/2683.
*/
SyncEngine.prototype.pumpEnqueuedLimboResolutions = function () {
while (this.enqueuedLimboResolutions.length > 0 &&
this.activeLimboTargetsByKey.size < this.maxConcurrentLimboResolutions) {
var key = this.enqueuedLimboResolutions.shift();
var limboTargetId = this.limboTargetIdGenerator.next();
this.activeLimboResolutionsByTarget.set(limboTargetId, new LimboResolution(key));
this.activeLimboTargetsByKey = this.activeLimboTargetsByKey.insert(key, limboTargetId);
this.remoteStore.listen(new TargetData(Query.atPath(key.path).toTarget(), limboTargetId, 2 /* LimboResolution */, ListenSequence.INVALID));
}
};
// Visible for testing
SyncEngine.prototype.activeLimboDocumentResolutions = function () {
return this.activeLimboTargetsByKey;
};
// Visible for testing
SyncEngine.prototype.enqueuedLimboDocumentResolutions = function () {
return this.enqueuedLimboResolutions;
};
SyncEngine.prototype.emitNewSnapsAndNotifyLocalStore = function (changes, remoteEvent) {
return tslib.__awaiter(this, void 0, void 0, function () {
var newSnaps, docChangesInAllViews, queriesProcessed;
var _this = this;
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
newSnaps = [];
docChangesInAllViews = [];
queriesProcessed = [];
this.queryViewsByQuery.forEach(function (_, queryView) {
queriesProcessed.push(Promise.resolve()
.then(function () {
var viewDocChanges = queryView.view.computeDocChanges(changes);
if (!viewDocChanges.needsRefill) {
return viewDocChanges;
}
// The query has a limit and some docs were removed, so we need
// to re-run the query against the local store to make sure we
// didn't lose any good docs that had been past the limit.
return _this.localStore
.executeQuery(queryView.query, /* usePreviousResults= */ false)
.then(function (_e) {
var documents = _e.documents;
return queryView.view.computeDocChanges(documents, viewDocChanges);
});
})
.then(function (viewDocChanges) {
var targetChange = remoteEvent && remoteEvent.targetChanges.get(queryView.targetId);
var viewChange = queryView.view.applyChanges(viewDocChanges,
/* updateLimboDocuments= */ _this.isPrimaryClient, targetChange);
_this.updateTrackedLimbos(queryView.targetId, viewChange.limboChanges);
if (viewChange.snapshot) {
if (_this.isPrimaryClient) {
_this.sharedClientState.updateQueryState(queryView.targetId, viewChange.snapshot.fromCache ? 'not-current' : 'current');
}
newSnaps.push(viewChange.snapshot);
var docChanges = LocalViewChanges.fromSnapshot(queryView.targetId, viewChange.snapshot);
docChangesInAllViews.push(docChanges);
}
}));
});
return [4 /*yield*/, Promise.all(queriesProcessed)];
case 1:
_e.sent();
this.syncEngineListener.onWatchChange(newSnaps);
return [4 /*yield*/, this.localStore.notifyLocalViewChanges(docChangesInAllViews)];
case 2:
_e.sent();
return [2 /*return*/];
}
});
});
};
SyncEngine.prototype.assertSubscribed = function (fnName) {
debugAssert(this.syncEngineListener !== null, 'Trying to call ' + fnName + ' before calling subscribe().');
};
SyncEngine.prototype.handleCredentialChange = function (user) {
return tslib.__awaiter(this, void 0, void 0, function () {
var userChanged, result;
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
userChanged = !this.currentUser.isEqual(user);
if (!userChanged) return [3 /*break*/, 3];
return [4 /*yield*/, this.localStore.handleUserChange(user)];
case 1:
result = _e.sent();
this.currentUser = user;
// Fails tasks waiting for pending writes requested by previous user.
this.rejectOutstandingPendingWritesCallbacks("'waitForPendingWrites' promise is rejected due to a user change.");
// TODO(b/114226417): Consider calling this only in the primary tab.
this.sharedClientState.handleUserChange(user, result.removedBatchIds, result.addedBatchIds);
return [4 /*yield*/, this.emitNewSnapsAndNotifyLocalStore(result.affectedDocuments)];
case 2:
_e.sent();
_e.label = 3;
case 3: return [4 /*yield*/, this.remoteStore.handleCredentialChange()];
case 4:
_e.sent();
return [2 /*return*/];
}
});
});
};
SyncEngine.prototype.enableNetwork = function () {
return this.remoteStore.enableNetwork();
};
SyncEngine.prototype.disableNetwork = function () {
return this.remoteStore.disableNetwork();
};
SyncEngine.prototype.getRemoteKeysForTarget = function (targetId) {
var limboResolution = this.activeLimboResolutionsByTarget.get(targetId);
if (limboResolution && limboResolution.receivedDocument) {
return documentKeySet().add(limboResolution.key);
}
else {
var keySet = documentKeySet();
var queries = this.queriesByTarget.get(targetId);
if (!queries) {
return keySet;
}
for (var _i = 0, queries_1 = queries; _i < queries_1.length; _i++) {
var query = queries_1[_i];
var queryView = this.queryViewsByQuery.get(query);
debugAssert(!!queryView, "No query view found for " + query);
keySet = keySet.unionWith(queryView.view.syncedDocuments);
}
return keySet;
}
};
return SyncEngine;
}());
/**
* An impplementation of SyncEngine that implement SharedClientStateSyncer for
* Multi-Tab synchronization.
*/
// PORTING NOTE: Web only
var MultiTabSyncEngine = /** @class */ (function (_super) {
tslib.__extends(MultiTabSyncEngine, _super);
function MultiTabSyncEngine(localStore, remoteStore, sharedClientState, currentUser, maxConcurrentLimboResolutions) {
var _this = _super.call(this, localStore, remoteStore, sharedClientState, currentUser, maxConcurrentLimboResolutions) || this;
_this.localStore = localStore;
// The primary state is set to `true` or `false` immediately after Firestore
// startup. In the interim, a client should only be considered primary if
// `isPrimary` is true.
_this._isPrimaryClient = undefined;
return _this;
}
Object.defineProperty(MultiTabSyncEngine.prototype, "isPrimaryClient", {
get: function () {
return this._isPrimaryClient === true;
},
enumerable: true,
configurable: true
});
MultiTabSyncEngine.prototype.enableNetwork = function () {
this.localStore.setNetworkEnabled(true);
return _super.prototype.enableNetwork.call(this);
};
MultiTabSyncEngine.prototype.disableNetwork = function () {
this.localStore.setNetworkEnabled(false);
return _super.prototype.disableNetwork.call(this);
};
/**
* Reconcile the list of synced documents in an existing view with those
* from persistence.
*/
MultiTabSyncEngine.prototype.synchronizeViewAndComputeSnapshot = function (queryView) {
return tslib.__awaiter(this, void 0, void 0, function () {
var queryResult, viewSnapshot;
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0: return [4 /*yield*/, this.localStore.executeQuery(queryView.query,
/* usePreviousResults= */ true)];
case 1:
queryResult = _e.sent();
viewSnapshot = queryView.view.synchronizeWithPersistedState(queryResult);
if (this._isPrimaryClient) {
this.updateTrackedLimbos(queryView.targetId, viewSnapshot.limboChanges);
}
return [2 /*return*/, viewSnapshot];
}
});
});
};
MultiTabSyncEngine.prototype.applyOnlineStateChange = function (onlineState, source) {
// If we are the primary client, the online state of all clients only
// depends on the online state of the local RemoteStore.
if (this.isPrimaryClient && source === 0 /* RemoteStore */) {
_super.prototype.applyOnlineStateChange.call(this, onlineState, source);
this.sharedClientState.setOnlineState(onlineState);
}
// If we are the secondary client, we explicitly ignore the remote store's
// online state (the local client may go offline, even though the primary
// tab remains online) and only apply the primary tab's online state from
// SharedClientState.
if (!this.isPrimaryClient &&
source === 1 /* SharedClientState */) {
_super.prototype.applyOnlineStateChange.call(this, onlineState, source);
}
};
MultiTabSyncEngine.prototype.applyBatchState = function (batchId, batchState, error) {
return tslib.__awaiter(this, void 0, void 0, function () {
var documents;
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
this.assertSubscribed('applyBatchState()');
return [4 /*yield*/, this.localStore.lookupMutationDocuments(batchId)];
case 1:
documents = _e.sent();
if (documents === null) {
// A throttled tab may not have seen the mutation before it was completed
// and removed from the mutation queue, in which case we won't have cached
// the affected documents. In this case we can safely ignore the update
// since that means we didn't apply the mutation locally at all (if we
// had, we would have cached the affected documents), and so we will just
// see any resulting document changes via normal remote document updates
// as applicable.
logDebug(LOG_TAG$7, 'Cannot apply mutation batch with id: ' + batchId);
return [2 /*return*/];
}
if (!(batchState === 'pending')) return [3 /*break*/, 3];
// If we are the primary client, we need to send this write to the
// backend. Secondary clients will ignore these writes since their remote
// connection is disabled.
return [4 /*yield*/, this.remoteStore.fillWritePipeline()];
case 2:
// If we are the primary client, we need to send this write to the
// backend. Secondary clients will ignore these writes since their remote
// connection is disabled.
_e.sent();
return [3 /*break*/, 4];
case 3:
if (batchState === 'acknowledged' || batchState === 'rejected') {
// NOTE: Both these methods are no-ops for batches that originated from
// other clients.
this.processUserCallback(batchId, error ? error : null);
this.localStore.removeCachedMutationBatchMetadata(batchId);
}
else {
fail("Unknown batchState: " + batchState);
}
_e.label = 4;
case 4: return [4 /*yield*/, this.emitNewSnapsAndNotifyLocalStore(documents)];
case 5:
_e.sent();
return [2 /*return*/];
}
});
});
};
MultiTabSyncEngine.prototype.applyPrimaryState = function (isPrimary) {
return tslib.__awaiter(this, void 0, void 0, function () {
var activeTargets, activeQueries, _i, activeQueries_1, targetData, activeTargets_1, p_1;
var _this = this;
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
if (!(isPrimary === true && this._isPrimaryClient !== true)) return [3 /*break*/, 3];
activeTargets = this.sharedClientState.getAllActiveQueryTargets();
return [4 /*yield*/, this.synchronizeQueryViewsAndRaiseSnapshots(activeTargets.toArray(),
/*transitionToPrimary=*/ true)];
case 1:
activeQueries = _e.sent();
this._isPrimaryClient = true;
return [4 /*yield*/, this.remoteStore.applyPrimaryState(true)];
case 2:
_e.sent();
for (_i = 0, activeQueries_1 = activeQueries; _i < activeQueries_1.length; _i++) {
targetData = activeQueries_1[_i];
this.remoteStore.listen(targetData);
}
return [3 /*break*/, 7];
case 3:
if (!(isPrimary === false && this._isPrimaryClient !== false)) return [3 /*break*/, 7];
activeTargets_1 = [];
p_1 = Promise.resolve();
this.queriesByTarget.forEach(function (_, targetId) {
if (_this.sharedClientState.isLocalQueryTarget(targetId)) {
activeTargets_1.push(targetId);
}
else {
p_1 = p_1.then(function () {
_this.removeAndCleanupTarget(targetId);
return _this.localStore.releaseTarget(targetId,
/*keepPersistedTargetData=*/ true);
});
}
_this.remoteStore.unlisten(targetId);
});
return [4 /*yield*/, p_1];
case 4:
_e.sent();
return [4 /*yield*/, this.synchronizeQueryViewsAndRaiseSnapshots(activeTargets_1,
/*transitionToPrimary=*/ false)];
case 5:
_e.sent();
this.resetLimboDocuments();
this._isPrimaryClient = false;
return [4 /*yield*/, this.remoteStore.applyPrimaryState(false)];
case 6:
_e.sent();
_e.label = 7;
case 7: return [2 /*return*/];
}
});
});
};
MultiTabSyncEngine.prototype.resetLimboDocuments = function () {
var _this = this;
this.activeLimboResolutionsByTarget.forEach(function (_, targetId) {
_this.remoteStore.unlisten(targetId);
});
this.limboDocumentRefs.removeAllReferences();
this.activeLimboResolutionsByTarget = new Map();
this.activeLimboTargetsByKey = new SortedMap(DocumentKey.comparator);
};
/**
* Reconcile the query views of the provided query targets with the state from
* persistence. Raises snapshots for any changes that affect the local
* client and returns the updated state of all target's query data.
*
* @param targets the list of targets with views that need to be recomputed
* @param transitionToPrimary `true` iff the tab transitions from a secondary
* tab to a primary tab
*/
MultiTabSyncEngine.prototype.synchronizeQueryViewsAndRaiseSnapshots = function (targets, transitionToPrimary) {
return tslib.__awaiter(this, void 0, void 0, function () {
var activeQueries, newViewSnapshots, _i, targets_1, targetId, targetData, queries, _e, queries_2, query, queryView, viewChange, target;
return tslib.__generator(this, function (_f) {
switch (_f.label) {
case 0:
activeQueries = [];
newViewSnapshots = [];
_i = 0, targets_1 = targets;
_f.label = 1;
case 1:
if (!(_i < targets_1.length)) return [3 /*break*/, 14];
targetId = targets_1[_i];
targetData = void 0;
queries = this.queriesByTarget.get(targetId);
if (!(queries && queries.length !== 0)) return [3 /*break*/, 8];
// For queries that have a local View, we need to update their state
// in LocalStore (as the resume token and the snapshot version
// might have changed) and reconcile their views with the persisted
// state (the list of syncedDocuments may have gotten out of sync).
return [4 /*yield*/, this.localStore.releaseTarget(targetId,
/*keepPersistedTargetData=*/ true)];
case 2:
// For queries that have a local View, we need to update their state
// in LocalStore (as the resume token and the snapshot version
// might have changed) and reconcile their views with the persisted
// state (the list of syncedDocuments may have gotten out of sync).
_f.sent();
return [4 /*yield*/, this.localStore.allocateTarget(queries[0].toTarget())];
case 3:
targetData = _f.sent();
_e = 0, queries_2 = queries;
_f.label = 4;
case 4:
if (!(_e < queries_2.length)) return [3 /*break*/, 7];
query = queries_2[_e];
queryView = this.queryViewsByQuery.get(query);
debugAssert(!!queryView, "No query view found for " + query);
return [4 /*yield*/, this.synchronizeViewAndComputeSnapshot(queryView)];
case 5:
viewChange = _f.sent();
if (viewChange.snapshot) {
newViewSnapshots.push(viewChange.snapshot);
}
_f.label = 6;
case 6:
_e++;
return [3 /*break*/, 4];
case 7: return [3 /*break*/, 12];
case 8:
debugAssert(transitionToPrimary, 'A secondary tab should never have an active target without an active query.');
return [4 /*yield*/, this.localStore.getTarget(targetId)];
case 9:
target = _f.sent();
debugAssert(!!target, "Target for id " + targetId + " not found");
return [4 /*yield*/, this.localStore.allocateTarget(target)];
case 10:
targetData = _f.sent();
return [4 /*yield*/, this.initializeViewAndComputeSnapshot(this.synthesizeTargetToQuery(target), targetId,
/*current=*/ false)];
case 11:
_f.sent();
_f.label = 12;
case 12:
activeQueries.push(targetData);
_f.label = 13;
case 13:
_i++;
return [3 /*break*/, 1];
case 14:
this.syncEngineListener.onWatchChange(newViewSnapshots);
return [2 /*return*/, activeQueries];
}
});
});
};
/**
* Creates a `Query` object from the specified `Target`. There is no way to
* obtain the original `Query`, so we synthesize a `Query` from the `Target`
* object.
*
* The synthesized result might be different from the original `Query`, but
* since the synthesized `Query` should return the same results as the
* original one (only the presentation of results might differ), the potential
* difference will not cause issues.
*/
MultiTabSyncEngine.prototype.synthesizeTargetToQuery = function (target) {
return new Query(target.path, target.collectionGroup, target.orderBy, target.filters, target.limit, "F" /* First */, target.startAt, target.endAt);
};
MultiTabSyncEngine.prototype.getActiveClients = function () {
return this.localStore.getActiveClients();
};
MultiTabSyncEngine.prototype.applyTargetState = function (targetId, state, error) {
return tslib.__awaiter(this, void 0, void 0, function () {
var _e, changes, synthesizedRemoteEvent;
return tslib.__generator(this, function (_f) {
switch (_f.label) {
case 0:
if (this._isPrimaryClient) {
// If we receive a target state notification via WebStorage, we are
// either already secondary or another tab has taken the primary lease.
logDebug(LOG_TAG$7, 'Ignoring unexpected query state notification.');
return [2 /*return*/];
}
if (!this.queriesByTarget.has(targetId)) return [3 /*break*/, 7];
_e = state;
switch (_e) {
case 'current': return [3 /*break*/, 1];
case 'not-current': return [3 /*break*/, 1];
case 'rejected': return [3 /*break*/, 4];
}
return [3 /*break*/, 6];
case 1: return [4 /*yield*/, this.localStore.getNewDocumentChanges()];
case 2:
changes = _f.sent();
synthesizedRemoteEvent = RemoteEvent.createSynthesizedRemoteEventForCurrentChange(targetId, state === 'current');
return [4 /*yield*/, this.emitNewSnapsAndNotifyLocalStore(changes, synthesizedRemoteEvent)];
case 3:
_f.sent();
return [3 /*break*/, 7];
case 4: return [4 /*yield*/, this.localStore.releaseTarget(targetId,
/* keepPersistedTargetData */ true)];
case 5:
_f.sent();
this.removeAndCleanupTarget(targetId, error);
return [3 /*break*/, 7];
case 6:
fail('Unexpected target state: ' + state);
_f.label = 7;
case 7: return [2 /*return*/];
}
});
});
};
MultiTabSyncEngine.prototype.applyActiveTargetsChange = function (added, removed) {
return tslib.__awaiter(this, void 0, void 0, function () {
var _i, added_1, targetId, target, targetData, _loop_3, this_2, _e, removed_1, targetId;
var _this = this;
return tslib.__generator(this, function (_f) {
switch (_f.label) {
case 0:
if (!this._isPrimaryClient) {
return [2 /*return*/];
}
_i = 0, added_1 = added;
_f.label = 1;
case 1:
if (!(_i < added_1.length)) return [3 /*break*/, 6];
targetId = added_1[_i];
if (this.queriesByTarget.has(targetId)) {
// A target might have been added in a previous attempt
logDebug(LOG_TAG$7, 'Adding an already active target ' + targetId);
return [3 /*break*/, 5];
}
return [4 /*yield*/, this.localStore.getTarget(targetId)];
case 2:
target = _f.sent();
debugAssert(!!target, "Query data for active target " + targetId + " not found");
return [4 /*yield*/, this.localStore.allocateTarget(target)];
case 3:
targetData = _f.sent();
return [4 /*yield*/, this.initializeViewAndComputeSnapshot(this.synthesizeTargetToQuery(target), targetData.targetId,
/*current=*/ false)];
case 4:
_f.sent();
this.remoteStore.listen(targetData);
_f.label = 5;
case 5:
_i++;
return [3 /*break*/, 1];
case 6:
_loop_3 = function (targetId) {
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
// Check that the target is still active since the target might have been
// removed if it has been rejected by the backend.
if (!this_2.queriesByTarget.has(targetId)) {
return [2 /*return*/, "continue"];
}
// Release queries that are still active.
return [4 /*yield*/, this_2.localStore
.releaseTarget(targetId, /* keepPersistedTargetData */ false)
.then(function () {
_this.remoteStore.unlisten(targetId);
_this.removeAndCleanupTarget(targetId);
})
.catch(ignoreIfPrimaryLeaseLoss)];
case 1:
// Release queries that are still active.
_e.sent();
return [2 /*return*/];
}
});
};
this_2 = this;
_e = 0, removed_1 = removed;
_f.label = 7;
case 7:
if (!(_e < removed_1.length)) return [3 /*break*/, 10];
targetId = removed_1[_e];
return [5 /*yield**/, _loop_3(targetId)];
case 8:
_f.sent();
_f.label = 9;
case 9:
_e++;
return [3 /*break*/, 7];
case 10: return [2 /*return*/];
}
});
});
};
return MultiTabSyncEngine;
}(SyncEngine));
/**
* @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.
*/
var LOG_TAG$8 = 'PersistentStream';
/** The time a stream stays open after it is marked idle. */
var IDLE_TIMEOUT_MS = 60 * 1000;
/**
* A PersistentStream is an abstract base class that represents a streaming RPC
* to the Firestore backend. It's built on top of the connections own support
* for streaming RPCs, and adds several critical features for our clients:
*
* - Exponential backoff on failure
* - Authentication via CredentialsProvider
* - Dispatching all callbacks into the shared worker queue
* - Closing idle streams after 60 seconds of inactivity
*
* Subclasses of PersistentStream implement serialization of models to and
* from the JSON representation of the protocol buffers for a specific
* streaming RPC.
*
* ## Starting and Stopping
*
* Streaming RPCs are stateful and need to be start()ed before messages can
* be sent and received. The PersistentStream will call the onOpen() function
* of the listener once the stream is ready to accept requests.
*
* Should a start() fail, PersistentStream will call the registered onClose()
* listener with a FirestoreError indicating what went wrong.
*
* A PersistentStream can be started and stopped repeatedly.
*
* Generic types:
* SendType: The type of the outgoing message of the underlying
* connection stream
* ReceiveType: The type of the incoming message of the underlying
* connection stream
* ListenerType: The type of the listener that will be used for callbacks
*/
var PersistentStream = /** @class */ (function () {
function PersistentStream(queue, connectionTimerId, idleTimerId, connection, credentialsProvider, listener) {
this.queue = queue;
this.idleTimerId = idleTimerId;
this.connection = connection;
this.credentialsProvider = credentialsProvider;
this.listener = listener;
this.state = 0 /* Initial */;
/**
* A close count that's incremented every time the stream is closed; used by
* getCloseGuardedDispatcher() to invalidate callbacks that happen after
* close.
*/
this.closeCount = 0;
this.idleTimer = null;
this.stream = null;
this.backoff = new ExponentialBackoff(queue, connectionTimerId);
}
/**
* Returns true if start() has been called and no error has occurred. True
* indicates the stream is open or in the process of opening (which
* encompasses respecting backoff, getting auth tokens, and starting the
* actual RPC). Use isOpen() to determine if the stream is open and ready for
* outbound requests.
*/
PersistentStream.prototype.isStarted = function () {
return (this.state === 1 /* Starting */ ||
this.state === 2 /* Open */ ||
this.state === 4 /* Backoff */);
};
/**
* Returns true if the underlying RPC is open (the onOpen() listener has been
* called) and the stream is ready for outbound requests.
*/
PersistentStream.prototype.isOpen = function () {
return this.state === 2 /* Open */;
};
/**
* Starts the RPC. Only allowed if isStarted() returns false. The stream is
* not immediately ready for use: onOpen() will be invoked when the RPC is
* ready for outbound requests, at which point isOpen() will return true.
*
* When start returns, isStarted() will return true.
*/
PersistentStream.prototype.start = function () {
if (this.state === 3 /* Error */) {
this.performBackoff();
return;
}
debugAssert(this.state === 0 /* Initial */, 'Already started');
this.auth();
};
/**
* Stops the RPC. This call is idempotent and allowed regardless of the
* current isStarted() state.
*
* When stop returns, isStarted() and isOpen() will both return false.
*/
PersistentStream.prototype.stop = function () {
return tslib.__awaiter(this, void 0, void 0, function () {
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
if (!this.isStarted()) return [3 /*break*/, 2];
return [4 /*yield*/, this.close(0 /* Initial */)];
case 1:
_e.sent();
_e.label = 2;
case 2: return [2 /*return*/];
}
});
});
};
/**
* After an error the stream will usually back off on the next attempt to
* start it. If the error warrants an immediate restart of the stream, the
* sender can use this to indicate that the receiver should not back off.
*
* Each error will call the onClose() listener. That function can decide to
* inhibit backoff if required.
*/
PersistentStream.prototype.inhibitBackoff = function () {
debugAssert(!this.isStarted(), 'Can only inhibit backoff in a stopped state');
this.state = 0 /* Initial */;
this.backoff.reset();
};
/**
* Marks this stream as idle. If no further actions are performed on the
* stream for one minute, the stream will automatically close itself and
* notify the stream's onClose() handler with Status.OK. The stream will then
* be in a !isStarted() state, requiring the caller to start the stream again
* before further use.
*
* Only streams that are in state 'Open' can be marked idle, as all other
* states imply pending network operations.
*/
PersistentStream.prototype.markIdle = function () {
var _this = this;
// Starts the idle time if we are in state 'Open' and are not yet already
// running a timer (in which case the previous idle timeout still applies).
if (this.isOpen() && this.idleTimer === null) {
this.idleTimer = this.queue.enqueueAfterDelay(this.idleTimerId, IDLE_TIMEOUT_MS, function () { return _this.handleIdleCloseTimer(); });
}
};
/** Sends a message to the underlying stream. */
PersistentStream.prototype.sendRequest = function (msg) {
this.cancelIdleCheck();
this.stream.send(msg);
};
/** Called by the idle timer when the stream should close due to inactivity. */
PersistentStream.prototype.handleIdleCloseTimer = function () {
return tslib.__awaiter(this, void 0, void 0, function () {
return tslib.__generator(this, function (_e) {
if (this.isOpen()) {
// When timing out an idle stream there's no reason to force the stream into backoff when
// it restarts so set the stream state to Initial instead of Error.
return [2 /*return*/, this.close(0 /* Initial */)];
}
return [2 /*return*/];
});
});
};
/** Marks the stream as active again. */
PersistentStream.prototype.cancelIdleCheck = function () {
if (this.idleTimer) {
this.idleTimer.cancel();
this.idleTimer = null;
}
};
/**
* Closes the stream and cleans up as necessary:
*
* * closes the underlying GRPC stream;
* * calls the onClose handler with the given 'error';
* * sets internal stream state to 'finalState';
* * adjusts the backoff timer based on the error
*
* A new stream can be opened by calling start().
*
* @param finalState the intended state of the stream after closing.
* @param error the error the connection was closed with.
*/
PersistentStream.prototype.close = function (finalState, error) {
return tslib.__awaiter(this, void 0, void 0, function () {
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
debugAssert(this.isStarted(), 'Only started streams should be closed.');
debugAssert(finalState === 3 /* Error */ || isNullOrUndefined(error), "Can't provide an error when not in an error state.");
// Cancel any outstanding timers (they're guaranteed not to execute).
this.cancelIdleCheck();
this.backoff.cancel();
// Invalidates any stream-related callbacks (e.g. from auth or the
// underlying stream), guaranteeing they won't execute.
this.closeCount++;
if (finalState !== 3 /* Error */) {
// If this is an intentional close ensure we don't delay our next connection attempt.
this.backoff.reset();
}
else if (error && error.code === Code.RESOURCE_EXHAUSTED) {
// Log the error. (Probably either 'quota exceeded' or 'max queue length reached'.)
logError(error.toString());
logError('Using maximum backoff delay to prevent overloading the backend.');
this.backoff.resetToMax();
}
else if (error && error.code === Code.UNAUTHENTICATED) {
// "unauthenticated" error means the token was rejected. Try force refreshing it in case it
// just expired.
this.credentialsProvider.invalidateToken();
}
// Clean up the underlying stream because we are no longer interested in events.
if (this.stream !== null) {
this.tearDown();
this.stream.close();
this.stream = null;
}
// This state must be assigned before calling onClose() to allow the callback to
// inhibit backoff or otherwise manipulate the state in its non-started state.
this.state = finalState;
// Notify the listener that the stream closed.
return [4 /*yield*/, this.listener.onClose(error)];
case 1:
// Notify the listener that the stream closed.
_e.sent();
return [2 /*return*/];
}
});
});
};
/**
* Can be overridden to perform additional cleanup before the stream is closed.
* Calling super.tearDown() is not required.
*/
PersistentStream.prototype.tearDown = function () { };
PersistentStream.prototype.auth = function () {
var _this = this;
debugAssert(this.state === 0 /* Initial */, 'Must be in initial state to auth');
this.state = 1 /* Starting */;
var dispatchIfNotClosed = this.getCloseGuardedDispatcher(this.closeCount);
// TODO(mikelehen): Just use dispatchIfNotClosed, but see TODO below.
var closeCount = this.closeCount;
this.credentialsProvider.getToken().then(function (token) {
// Stream can be stopped while waiting for authentication.
// TODO(mikelehen): We really should just use dispatchIfNotClosed
// and let this dispatch onto the queue, but that opened a spec test can
// of worms that I don't want to deal with in this PR.
if (_this.closeCount === closeCount) {
// Normally we'd have to schedule the callback on the AsyncQueue.
// However, the following calls are safe to be called outside the
// AsyncQueue since they don't chain asynchronous calls
_this.startStream(token);
}
}, function (error) {
dispatchIfNotClosed(function () {
var rpcError = new FirestoreError(Code.UNKNOWN, 'Fetching auth token failed: ' + error.message);
return _this.handleStreamClose(rpcError);
});
});
};
PersistentStream.prototype.startStream = function (token) {
var _this = this;
debugAssert(this.state === 1 /* Starting */, 'Trying to start stream in a non-starting state');
var dispatchIfNotClosed = this.getCloseGuardedDispatcher(this.closeCount);
this.stream = this.startRpc(token);
this.stream.onOpen(function () {
dispatchIfNotClosed(function () {
debugAssert(_this.state === 1 /* Starting */, 'Expected stream to be in state Starting, but was ' + _this.state);
_this.state = 2 /* Open */;
return _this.listener.onOpen();
});
});
this.stream.onClose(function (error) {
dispatchIfNotClosed(function () {
return _this.handleStreamClose(error);
});
});
this.stream.onMessage(function (msg) {
dispatchIfNotClosed(function () {
return _this.onMessage(msg);
});
});
};
PersistentStream.prototype.performBackoff = function () {
var _this = this;
debugAssert(this.state === 3 /* Error */, 'Should only perform backoff when in Error state');
this.state = 4 /* Backoff */;
this.backoff.backoffAndRun(function () { return tslib.__awaiter(_this, void 0, void 0, function () {
return tslib.__generator(this, function (_e) {
debugAssert(this.state === 4 /* Backoff */, 'Backoff elapsed but state is now: ' + this.state);
this.state = 0 /* Initial */;
this.start();
debugAssert(this.isStarted(), 'PersistentStream should have started');
return [2 /*return*/];
});
}); });
};
// Visible for tests
PersistentStream.prototype.handleStreamClose = function (error) {
debugAssert(this.isStarted(), "Can't handle server close on non-started stream");
logDebug(LOG_TAG$8, "close with error: " + error);
this.stream = null;
// In theory the stream could close cleanly, however, in our current model
// we never expect this to happen because if we stop a stream ourselves,
// this callback will never be called. To prevent cases where we retry
// without a backoff accidentally, we set the stream to error in all cases.
return this.close(3 /* Error */, error);
};
/**
* Returns a "dispatcher" function that dispatches operations onto the
* AsyncQueue but only runs them if closeCount remains unchanged. This allows
* us to turn auth / stream callbacks into no-ops if the stream is closed /
* re-opened, etc.
*/
PersistentStream.prototype.getCloseGuardedDispatcher = function (startCloseCount) {
var _this = this;
return function (fn) {
_this.queue.enqueueAndForget(function () {
if (_this.closeCount === startCloseCount) {
return fn();
}
else {
logDebug(LOG_TAG$8, 'stream callback skipped by getCloseGuardedDispatcher.');
return Promise.resolve();
}
});
};
};
return PersistentStream;
}());
/**
* A PersistentStream that implements the Listen RPC.
*
* Once the Listen stream has called the onOpen() listener, any number of
* listen() and unlisten() calls can be made to control what changes will be
* sent from the server for ListenResponses.
*/
var PersistentListenStream = /** @class */ (function (_super) {
tslib.__extends(PersistentListenStream, _super);
function PersistentListenStream(queue, connection, credentials, serializer, listener) {
var _this = _super.call(this, queue, "listen_stream_connection_backoff" /* ListenStreamConnectionBackoff */, "listen_stream_idle" /* ListenStreamIdle */, connection, credentials, listener) || this;
_this.serializer = serializer;
return _this;
}
PersistentListenStream.prototype.startRpc = function (token) {
return this.connection.openStream('Listen', token);
};
PersistentListenStream.prototype.onMessage = function (watchChangeProto) {
// A successful response means the stream is healthy
this.backoff.reset();
var watchChange = this.serializer.fromWatchChange(watchChangeProto);
var snapshot = this.serializer.versionFromListenResponse(watchChangeProto);
return this.listener.onWatchChange(watchChange, snapshot);
};
/**
* Registers interest in the results of the given target. If the target
* includes a resumeToken it will be included in the request. Results that
* affect the target will be streamed back as WatchChange messages that
* reference the targetId.
*/
PersistentListenStream.prototype.watch = function (targetData) {
var request = {};
request.database = this.serializer.encodedDatabaseId;
request.addTarget = this.serializer.toTarget(targetData);
var labels = this.serializer.toListenRequestLabels(targetData);
if (labels) {
request.labels = labels;
}
this.sendRequest(request);
};
/**
* Unregisters interest in the results of the target associated with the
* given targetId.
*/
PersistentListenStream.prototype.unwatch = function (targetId) {
var request = {};
request.database = this.serializer.encodedDatabaseId;
request.removeTarget = targetId;
this.sendRequest(request);
};
return PersistentListenStream;
}(PersistentStream));
/**
* A Stream that implements the Write RPC.
*
* The Write RPC requires the caller to maintain special streamToken
* state in between calls, to help the server understand which responses the
* client has processed by the time the next request is made. Every response
* will contain a streamToken; this value must be passed to the next
* request.
*
* After calling start() on this stream, the next request must be a handshake,
* containing whatever streamToken is on hand. Once a response to this
* request is received, all pending mutations may be submitted. When
* submitting multiple batches of mutations at the same time, it's
* okay to use the same streamToken for the calls to writeMutations.
*
* TODO(b/33271235): Use proto types
*/
var PersistentWriteStream = /** @class */ (function (_super) {
tslib.__extends(PersistentWriteStream, _super);
function PersistentWriteStream(queue, connection, credentials, serializer, listener) {
var _this = _super.call(this, queue, "write_stream_connection_backoff" /* WriteStreamConnectionBackoff */, "write_stream_idle" /* WriteStreamIdle */, connection, credentials, listener) || this;
_this.serializer = serializer;
_this.handshakeComplete_ = false;
/**
* The last received stream token from the server, used to acknowledge which
* responses the client has processed. Stream tokens are opaque checkpoint
* markers whose only real value is their inclusion in the next request.
*
* PersistentWriteStream manages propagating this value from responses to the
* next request.
*/
_this.lastStreamToken = ByteString.EMPTY_BYTE_STRING;
return _this;
}
Object.defineProperty(PersistentWriteStream.prototype, "handshakeComplete", {
/**
* Tracks whether or not a handshake has been successfully exchanged and
* the stream is ready to accept mutations.
*/
get: function () {
return this.handshakeComplete_;
},
enumerable: true,
configurable: true
});
// Override of PersistentStream.start
PersistentWriteStream.prototype.start = function () {
this.handshakeComplete_ = false;
_super.prototype.start.call(this);
};
PersistentWriteStream.prototype.tearDown = function () {
if (this.handshakeComplete_) {
this.writeMutations([]);
}
};
PersistentWriteStream.prototype.startRpc = function (token) {
return this.connection.openStream('Write', token);
};
PersistentWriteStream.prototype.onMessage = function (responseProto) {
// Always capture the last stream token.
hardAssert(!!responseProto.streamToken, 'Got a write response without a stream token');
this.lastStreamToken = this.serializer.fromBytes(responseProto.streamToken);
if (!this.handshakeComplete_) {
// The first response is always the handshake response
hardAssert(!responseProto.writeResults || responseProto.writeResults.length === 0, 'Got mutation results for handshake');
this.handshakeComplete_ = true;
return this.listener.onHandshakeComplete();
}
else {
// A successful first write response means the stream is healthy,
// Note, that we could consider a successful handshake healthy, however,
// the write itself might be causing an error we want to back off from.
this.backoff.reset();
var results = this.serializer.fromWriteResults(responseProto.writeResults, responseProto.commitTime);
var commitVersion = this.serializer.fromVersion(responseProto.commitTime);
return this.listener.onMutationResult(commitVersion, results);
}
};
/**
* Sends an initial streamToken to the server, performing the handshake
* required to make the StreamingWrite RPC work. Subsequent
* calls should wait until onHandshakeComplete was called.
*/
PersistentWriteStream.prototype.writeHandshake = function () {
debugAssert(this.isOpen(), 'Writing handshake requires an opened stream');
debugAssert(!this.handshakeComplete_, 'Handshake already completed');
// TODO(dimond): Support stream resumption. We intentionally do not set the
// stream token on the handshake, ignoring any stream token we might have.
var request = {};
request.database = this.serializer.encodedDatabaseId;
this.sendRequest(request);
};
/** Sends a group of mutations to the Firestore backend to apply. */
PersistentWriteStream.prototype.writeMutations = function (mutations) {
var _this = this;
debugAssert(this.isOpen(), 'Writing mutations requires an opened stream');
debugAssert(this.handshakeComplete_, 'Handshake must be complete before writing mutations');
debugAssert(this.lastStreamToken.approximateByteSize() > 0, 'Trying to write mutation without a token');
var request = {
streamToken: this.serializer.toBytes(this.lastStreamToken),
writes: mutations.map(function (mutation) { return _this.serializer.toMutation(mutation); })
};
this.sendRequest(request);
};
return PersistentWriteStream;
}(PersistentStream));
/**
* @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.
*/
/**
* Datastore and its related methods are a wrapper around the external Google
* Cloud Datastore grpc API, which provides an interface that is more convenient
* for the rest of the client SDK architecture to consume.
*/
var Datastore = /** @class */ (function () {
function Datastore() {
// Make sure that the structural type of `Datastore` is unique.
// See https://github.com/microsoft/TypeScript/issues/5451
this._ = undefined;
}
return Datastore;
}());
/**
* An implementation of Datastore that exposes additional state for internal
* consumption.
*/
var DatastoreImpl = /** @class */ (function (_super) {
tslib.__extends(DatastoreImpl, _super);
function DatastoreImpl(connection, credentials, serializer) {
var _this = _super.call(this) || this;
_this.connection = connection;
_this.credentials = credentials;
_this.serializer = serializer;
return _this;
}
/** Gets an auth token and invokes the provided RPC. */
DatastoreImpl.prototype.invokeRPC = function (rpcName, request) {
var _this = this;
return this.credentials
.getToken()
.then(function (token) {
return _this.connection.invokeRPC(rpcName, request, token);
})
.catch(function (error) {
if (error.code === Code.UNAUTHENTICATED) {
_this.credentials.invalidateToken();
}
throw error;
});
};
/** Gets an auth token and invokes the provided RPC with streamed results. */
DatastoreImpl.prototype.invokeStreamingRPC = function (rpcName, request) {
var _this = this;
return this.credentials
.getToken()
.then(function (token) {
return _this.connection.invokeStreamingRPC(rpcName, request, token);
})
.catch(function (error) {
if (error.code === Code.UNAUTHENTICATED) {
_this.credentials.invalidateToken();
}
throw error;
});
};
return DatastoreImpl;
}(Datastore));
function newDatastore(connection, credentials, serializer) {
return new DatastoreImpl(connection, credentials, serializer);
}
function invokeCommitRpc(datastore, mutations) {
return tslib.__awaiter(this, void 0, void 0, function () {
var datastoreImpl, params, response;
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
datastoreImpl = debugCast(datastore, DatastoreImpl);
params = {
database: datastoreImpl.serializer.encodedDatabaseId,
writes: mutations.map(function (m) { return datastoreImpl.serializer.toMutation(m); })
};
return [4 /*yield*/, datastoreImpl.invokeRPC('Commit', params)];
case 1:
response = _e.sent();
return [2 /*return*/, datastoreImpl.serializer.fromWriteResults(response.writeResults, response.commitTime)];
}
});
});
}
function invokeBatchGetDocumentsRpc(datastore, keys) {
return tslib.__awaiter(this, void 0, void 0, function () {
var datastoreImpl, params, response, docs, result;
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
datastoreImpl = debugCast(datastore, DatastoreImpl);
params = {
database: datastoreImpl.serializer.encodedDatabaseId,
documents: keys.map(function (k) { return datastoreImpl.serializer.toName(k); })
};
return [4 /*yield*/, datastoreImpl.invokeStreamingRPC('BatchGetDocuments', params)];
case 1:
response = _e.sent();
docs = new Map();
response.forEach(function (proto) {
var doc = datastoreImpl.serializer.fromMaybeDocument(proto);
docs.set(doc.key.toString(), doc);
});
result = [];
keys.forEach(function (key) {
var doc = docs.get(key.toString());
hardAssert(!!doc, 'Missing entity in write response for ' + key);
result.push(doc);
});
return [2 /*return*/, result];
}
});
});
}
function newPersistentWriteStream(datastore, queue, listener) {
var datastoreImpl = debugCast(datastore, DatastoreImpl);
return new PersistentWriteStream(queue, datastoreImpl.connection, datastoreImpl.credentials, datastoreImpl.serializer, listener);
}
function newPersistentWatchStream(datastore, queue, listener) {
var datastoreImpl = debugCast(datastore, DatastoreImpl);
return new PersistentListenStream(queue, datastoreImpl.connection, datastoreImpl.credentials, datastoreImpl.serializer, listener);
}
/**
* @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.
*/
/**
* Internal transaction object responsible for accumulating the mutations to
* perform and the base versions for any documents read.
*/
var Transaction = /** @class */ (function () {
function Transaction(datastore) {
this.datastore = datastore;
// The version of each document that was read during this transaction.
this.readVersions = documentVersionMap();
this.mutations = [];
this.committed = false;
/**
* A deferred usage error that occurred previously in this transaction that
* will cause the transaction to fail once it actually commits.
*/
this.lastWriteError = null;
/**
* Set of documents that have been written in the transaction.
*
* When there's more than one write to the same key in a transaction, any
* writes after the first are handled differently.
*/
this.writtenDocs = new Set();
}
Transaction.prototype.lookup = function (keys) {
return tslib.__awaiter(this, void 0, void 0, function () {
var docs;
var _this = this;
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
this.ensureCommitNotCalled();
if (this.mutations.length > 0) {
throw new FirestoreError(Code.INVALID_ARGUMENT, 'Firestore transactions require all reads to be executed before all writes.');
}
return [4 /*yield*/, invokeBatchGetDocumentsRpc(this.datastore, keys)];
case 1:
docs = _e.sent();
docs.forEach(function (doc) {
if (doc instanceof NoDocument || doc instanceof Document) {
_this.recordVersion(doc);
}
else {
fail('Document in a transaction was a ' + doc.constructor.name);
}
});
return [2 /*return*/, docs];
}
});
});
};
Transaction.prototype.set = function (key, data) {
this.write(data.toMutations(key, this.precondition(key)));
this.writtenDocs.add(key);
};
Transaction.prototype.update = function (key, data) {
try {
this.write(data.toMutations(key, this.preconditionForUpdate(key)));
}
catch (e) {
this.lastWriteError = e;
}
this.writtenDocs.add(key);
};
Transaction.prototype.delete = function (key) {
this.write([new DeleteMutation(key, this.precondition(key))]);
this.writtenDocs.add(key);
};
Transaction.prototype.commit = function () {
return tslib.__awaiter(this, void 0, void 0, function () {
var unwritten;
var _this = this;
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
this.ensureCommitNotCalled();
if (this.lastWriteError) {
throw this.lastWriteError;
}
unwritten = this.readVersions;
// For each mutation, note that the doc was written.
this.mutations.forEach(function (mutation) {
unwritten = unwritten.remove(mutation.key);
});
// For each document that was read but not written to, we want to perform
// a `verify` operation.
unwritten.forEach(function (key, _version) {
_this.mutations.push(new VerifyMutation(key, _this.precondition(key)));
});
return [4 /*yield*/, invokeCommitRpc(this.datastore, this.mutations)];
case 1:
_e.sent();
this.committed = true;
return [2 /*return*/];
}
});
});
};
Transaction.prototype.recordVersion = function (doc) {
var docVersion;
if (doc instanceof Document) {
docVersion = doc.version;
}
else if (doc instanceof NoDocument) {
// For deleted docs, we must use baseVersion 0 when we overwrite them.
docVersion = SnapshotVersion.min();
}
else {
throw fail('Document in a transaction was a ' + doc.constructor.name);
}
var existingVersion = this.readVersions.get(doc.key);
if (existingVersion !== null) {
if (!docVersion.isEqual(existingVersion)) {
// This transaction will fail no matter what.
throw new FirestoreError(Code.ABORTED, 'Document version changed between two reads.');
}
}
else {
this.readVersions = this.readVersions.insert(doc.key, docVersion);
}
};
/**
* Returns the version of this document when it was read in this transaction,
* as a precondition, or no precondition if it was not read.
*/
Transaction.prototype.precondition = function (key) {
var version = this.readVersions.get(key);
if (!this.writtenDocs.has(key) && version) {
return Precondition.updateTime(version);
}
else {
return Precondition.none();
}
};
/**
* Returns the precondition for a document if the operation is an update.
*/
Transaction.prototype.preconditionForUpdate = function (key) {
var version = this.readVersions.get(key);
// The first time a document is written, we want to take into account the
// read time and existence
if (!this.writtenDocs.has(key) && version) {
if (version.isEqual(SnapshotVersion.min())) {
// The document doesn't exist, so fail the transaction.
// This has to be validated locally because you can't send a
// precondition that a document does not exist without changing the
// semantics of the backend write to be an insert. This is the reverse
// of what we want, since we want to assert that the document doesn't
// exist but then send the update and have it fail. Since we can't
// express that to the backend, we have to validate locally.
// Note: this can change once we can send separate verify writes in the
// transaction.
throw new FirestoreError(Code.INVALID_ARGUMENT, "Can't update a document that doesn't exist.");
}
// Document exists, base precondition on document update time.
return Precondition.updateTime(version);
}
else {
// Document was not read, so we just use the preconditions for a blind
// update.
return Precondition.exists(true);
}
};
Transaction.prototype.write = function (mutations) {
this.ensureCommitNotCalled();
this.mutations = this.mutations.concat(mutations);
};
Transaction.prototype.ensureCommitNotCalled = function () {
debugAssert(!this.committed, 'A transaction object cannot be used after its update callback has been invoked.');
};
return Transaction;
}());
/**
* @license
* Copyright 2018 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.
*/
var LOG_TAG$9 = 'OnlineStateTracker';
// To deal with transient failures, we allow multiple stream attempts before
// giving up and transitioning from OnlineState.Unknown to Offline.
// TODO(mikelehen): This used to be set to 2 as a mitigation for b/66228394.
// @jdimond thinks that bug is sufficiently fixed so that we can set this back
// to 1. If that works okay, we could potentially remove this logic entirely.
var MAX_WATCH_STREAM_FAILURES = 1;
// To deal with stream attempts that don't succeed or fail in a timely manner,
// we have a timeout for OnlineState to reach Online or Offline.
// If the timeout is reached, we transition to Offline rather than waiting
// indefinitely.
var ONLINE_STATE_TIMEOUT_MS = 10 * 1000;
/**
* A component used by the RemoteStore to track the OnlineState (that is,
* whether or not the client as a whole should be considered to be online or
* offline), implementing the appropriate heuristics.
*
* In particular, when the client is trying to connect to the backend, we
* allow up to MAX_WATCH_STREAM_FAILURES within ONLINE_STATE_TIMEOUT_MS for
* a connection to succeed. If we have too many failures or the timeout elapses,
* then we set the OnlineState to Offline, and the client will behave as if
* it is offline (get()s will return cached data, etc.).
*/
var OnlineStateTracker = /** @class */ (function () {
function OnlineStateTracker(asyncQueue, onlineStateHandler) {
this.asyncQueue = asyncQueue;
this.onlineStateHandler = onlineStateHandler;
/** The current OnlineState. */
this.state = "Unknown" /* Unknown */;
/**
* A count of consecutive failures to open the stream. If it reaches the
* maximum defined by MAX_WATCH_STREAM_FAILURES, we'll set the OnlineState to
* Offline.
*/
this.watchStreamFailures = 0;
/**
* A timer that elapses after ONLINE_STATE_TIMEOUT_MS, at which point we
* transition from OnlineState.Unknown to OnlineState.Offline without waiting
* for the stream to actually fail (MAX_WATCH_STREAM_FAILURES times).
*/
this.onlineStateTimer = null;
/**
* Whether the client should log a warning message if it fails to connect to
* the backend (initially true, cleared after a successful stream, or if we've
* logged the message already).
*/
this.shouldWarnClientIsOffline = true;
}
/**
* Called by RemoteStore when a watch stream is started (including on each
* backoff attempt).
*
* If this is the first attempt, it sets the OnlineState to Unknown and starts
* the onlineStateTimer.
*/
OnlineStateTracker.prototype.handleWatchStreamStart = function () {
var _this = this;
if (this.watchStreamFailures === 0) {
this.setAndBroadcast("Unknown" /* Unknown */);
debugAssert(this.onlineStateTimer === null, "onlineStateTimer shouldn't be started yet");
this.onlineStateTimer = this.asyncQueue.enqueueAfterDelay("online_state_timeout" /* OnlineStateTimeout */, ONLINE_STATE_TIMEOUT_MS, function () {
_this.onlineStateTimer = null;
debugAssert(_this.state === "Unknown" /* Unknown */, 'Timer should be canceled if we transitioned to a different state.');
_this.logClientOfflineWarningIfNecessary("Backend didn't respond within " + ONLINE_STATE_TIMEOUT_MS / 1000 + " " +
"seconds.");
_this.setAndBroadcast("Offline" /* Offline */);
// NOTE: handleWatchStreamFailure() will continue to increment
// watchStreamFailures even though we are already marked Offline,
// but this is non-harmful.
return Promise.resolve();
});
}
};
/**
* Updates our OnlineState as appropriate after the watch stream reports a
* failure. The first failure moves us to the 'Unknown' state. We then may
* allow multiple failures (based on MAX_WATCH_STREAM_FAILURES) before we
* actually transition to the 'Offline' state.
*/
OnlineStateTracker.prototype.handleWatchStreamFailure = function (error) {
if (this.state === "Online" /* Online */) {
this.setAndBroadcast("Unknown" /* Unknown */);
// To get to OnlineState.Online, set() must have been called which would
// have reset our heuristics.
debugAssert(this.watchStreamFailures === 0, 'watchStreamFailures must be 0');
debugAssert(this.onlineStateTimer === null, 'onlineStateTimer must be null');
}
else {
this.watchStreamFailures++;
if (this.watchStreamFailures >= MAX_WATCH_STREAM_FAILURES) {
this.clearOnlineStateTimer();
this.logClientOfflineWarningIfNecessary("Connection failed " + MAX_WATCH_STREAM_FAILURES + " " +
("times. Most recent error: " + error.toString()));
this.setAndBroadcast("Offline" /* Offline */);
}
}
};
/**
* Explicitly sets the OnlineState to the specified state.
*
* Note that this resets our timers / failure counters, etc. used by our
* Offline heuristics, so must not be used in place of
* handleWatchStreamStart() and handleWatchStreamFailure().
*/
OnlineStateTracker.prototype.set = function (newState) {
this.clearOnlineStateTimer();
this.watchStreamFailures = 0;
if (newState === "Online" /* Online */) {
// We've connected to watch at least once. Don't warn the developer
// about being offline going forward.
this.shouldWarnClientIsOffline = false;
}
this.setAndBroadcast(newState);
};
OnlineStateTracker.prototype.setAndBroadcast = function (newState) {
if (newState !== this.state) {
this.state = newState;
this.onlineStateHandler(newState);
}
};
OnlineStateTracker.prototype.logClientOfflineWarningIfNecessary = function (details) {
var message = "Could not reach Cloud Firestore backend. " + details + "\n" +
"This typically indicates that your device does not have a healthy " +
"Internet connection at the moment. The client will operate in offline " +
"mode until it is able to successfully connect to the backend.";
if (this.shouldWarnClientIsOffline) {
logError(message);
this.shouldWarnClientIsOffline = false;
}
else {
logDebug(LOG_TAG$9, message);
}
};
OnlineStateTracker.prototype.clearOnlineStateTimer = function () {
if (this.onlineStateTimer !== null) {
this.onlineStateTimer.cancel();
this.onlineStateTimer = null;
}
};
return OnlineStateTracker;
}());
/**
* @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.
*/
/**
* Represents a changed document and a list of target ids to which this change
* applies.
*
* If document has been deleted NoDocument will be provided.
*/
var DocumentWatchChange = /** @class */ (function () {
function DocumentWatchChange(
/** The new document applies to all of these targets. */
updatedTargetIds,
/** The new document is removed from all of these targets. */
removedTargetIds,
/** The key of the document for this change. */
key,
/**
* The new document or NoDocument if it was deleted. Is null if the
* document went out of view without the server sending a new document.
*/
newDoc) {
this.updatedTargetIds = updatedTargetIds;
this.removedTargetIds = removedTargetIds;
this.key = key;
this.newDoc = newDoc;
}
return DocumentWatchChange;
}());
var ExistenceFilterChange = /** @class */ (function () {
function ExistenceFilterChange(targetId, existenceFilter) {
this.targetId = targetId;
this.existenceFilter = existenceFilter;
}
return ExistenceFilterChange;
}());
var WatchTargetChange = /** @class */ (function () {
function WatchTargetChange(
/** What kind of change occurred to the watch target. */
state,
/** The target IDs that were added/removed/set. */
targetIds,
/**
* An opaque, server-assigned token that allows watching a target to be
* resumed after disconnecting without retransmitting all the data that
* matches the target. The resume token essentially identifies a point in
* time from which the server should resume sending results.
*/
resumeToken,
/** An RPC error indicating why the watch failed. */
cause) {
if (resumeToken === void 0) { resumeToken = ByteString.EMPTY_BYTE_STRING; }
if (cause === void 0) { cause = null; }
this.state = state;
this.targetIds = targetIds;
this.resumeToken = resumeToken;
this.cause = cause;
}
return WatchTargetChange;
}());
/** Tracks the internal state of a Watch target. */
var TargetState = /** @class */ (function () {
function TargetState() {
/**
* The number of pending responses (adds or removes) that we are waiting on.
* We only consider targets active that have no pending responses.
*/
this.pendingResponses = 0;
/**
* Keeps track of the document changes since the last raised snapshot.
*
* These changes are continuously updated as we receive document updates and
* always reflect the current set of changes against the last issued snapshot.
*/
this.documentChanges = snapshotChangesMap();
/** See public getters for explanations of these fields. */
this._resumeToken = ByteString.EMPTY_BYTE_STRING;
this._current = false;
/**
* Whether this target state should be included in the next snapshot. We
* initialize to true so that newly-added targets are included in the next
* RemoteEvent.
*/
this._hasPendingChanges = true;
}
Object.defineProperty(TargetState.prototype, "current", {
/**
* Whether this target has been marked 'current'.
*
* 'Current' has special meaning in the RPC protocol: It implies that the
* Watch backend has sent us all changes up to the point at which the target
* was added and that the target is consistent with the rest of the watch
* stream.
*/
get: function () {
return this._current;
},
enumerable: true,
configurable: true
});
Object.defineProperty(TargetState.prototype, "resumeToken", {
/** The last resume token sent to us for this target. */
get: function () {
return this._resumeToken;
},
enumerable: true,
configurable: true
});
Object.defineProperty(TargetState.prototype, "isPending", {
/** Whether this target has pending target adds or target removes. */
get: function () {
return this.pendingResponses !== 0;
},
enumerable: true,
configurable: true
});
Object.defineProperty(TargetState.prototype, "hasPendingChanges", {
/** Whether we have modified any state that should trigger a snapshot. */
get: function () {
return this._hasPendingChanges;
},
enumerable: true,
configurable: true
});
/**
* Applies the resume token to the TargetChange, but only when it has a new
* value. Empty resumeTokens are discarded.
*/
TargetState.prototype.updateResumeToken = function (resumeToken) {
if (resumeToken.approximateByteSize() > 0) {
this._hasPendingChanges = true;
this._resumeToken = resumeToken;
}
};
/**
* Creates a target change from the current set of changes.
*
* To reset the document changes after raising this snapshot, call
* `clearPendingChanges()`.
*/
TargetState.prototype.toTargetChange = function () {
var addedDocuments = documentKeySet();
var modifiedDocuments = documentKeySet();
var removedDocuments = documentKeySet();
this.documentChanges.forEach(function (key, changeType) {
switch (changeType) {
case 0 /* Added */:
addedDocuments = addedDocuments.add(key);
break;
case 2 /* Modified */:
modifiedDocuments = modifiedDocuments.add(key);
break;
case 1 /* Removed */:
removedDocuments = removedDocuments.add(key);
break;
default:
fail('Encountered invalid change type: ' + changeType);
}
});
return new TargetChange(this._resumeToken, this._current, addedDocuments, modifiedDocuments, removedDocuments);
};
/**
* Resets the document changes and sets `hasPendingChanges` to false.
*/
TargetState.prototype.clearPendingChanges = function () {
this._hasPendingChanges = false;
this.documentChanges = snapshotChangesMap();
};
TargetState.prototype.addDocumentChange = function (key, changeType) {
this._hasPendingChanges = true;
this.documentChanges = this.documentChanges.insert(key, changeType);
};
TargetState.prototype.removeDocumentChange = function (key) {
this._hasPendingChanges = true;
this.documentChanges = this.documentChanges.remove(key);
};
TargetState.prototype.recordPendingTargetRequest = function () {
this.pendingResponses += 1;
};
TargetState.prototype.recordTargetResponse = function () {
this.pendingResponses -= 1;
};
TargetState.prototype.markCurrent = function () {
this._hasPendingChanges = true;
this._current = true;
};
return TargetState;
}());
var LOG_TAG$a = 'WatchChangeAggregator';
/**
* A helper class to accumulate watch changes into a RemoteEvent.
*/
var WatchChangeAggregator = /** @class */ (function () {
function WatchChangeAggregator(metadataProvider) {
this.metadataProvider = metadataProvider;
/** The internal state of all tracked targets. */
this.targetStates = new Map();
/** Keeps track of the documents to update since the last raised snapshot. */
this.pendingDocumentUpdates = maybeDocumentMap();
/** A mapping of document keys to their set of target IDs. */
this.pendingDocumentTargetMapping = documentTargetMap();
/**
* A list of targets with existence filter mismatches. These targets are
* known to be inconsistent and their listens needs to be re-established by
* RemoteStore.
*/
this.pendingTargetResets = new SortedSet(primitiveComparator);
}
/**
* Processes and adds the DocumentWatchChange to the current set of changes.
*/
WatchChangeAggregator.prototype.handleDocumentChange = function (docChange) {
for (var _i = 0, _e = docChange.updatedTargetIds; _i < _e.length; _i++) {
var targetId = _e[_i];
if (docChange.newDoc instanceof Document) {
this.addDocumentToTarget(targetId, docChange.newDoc);
}
else if (docChange.newDoc instanceof NoDocument) {
this.removeDocumentFromTarget(targetId, docChange.key, docChange.newDoc);
}
}
for (var _f = 0, _g = docChange.removedTargetIds; _f < _g.length; _f++) {
var targetId = _g[_f];
this.removeDocumentFromTarget(targetId, docChange.key, docChange.newDoc);
}
};
/** Processes and adds the WatchTargetChange to the current set of changes. */
WatchChangeAggregator.prototype.handleTargetChange = function (targetChange) {
var _this = this;
this.forEachTarget(targetChange, function (targetId) {
var targetState = _this.ensureTargetState(targetId);
switch (targetChange.state) {
case 0 /* NoChange */:
if (_this.isActiveTarget(targetId)) {
targetState.updateResumeToken(targetChange.resumeToken);
}
break;
case 1 /* Added */:
// We need to decrement the number of pending acks needed from watch
// for this targetId.
targetState.recordTargetResponse();
if (!targetState.isPending) {
// We have a freshly added target, so we need to reset any state
// that we had previously. This can happen e.g. when remove and add
// back a target for existence filter mismatches.
targetState.clearPendingChanges();
}
targetState.updateResumeToken(targetChange.resumeToken);
break;
case 2 /* Removed */:
// We need to keep track of removed targets to we can post-filter and
// remove any target changes.
// We need to decrement the number of pending acks needed from watch
// for this targetId.
targetState.recordTargetResponse();
if (!targetState.isPending) {
_this.removeTarget(targetId);
}
debugAssert(!targetChange.cause, 'WatchChangeAggregator does not handle errored targets');
break;
case 3 /* Current */:
if (_this.isActiveTarget(targetId)) {
targetState.markCurrent();
targetState.updateResumeToken(targetChange.resumeToken);
}
break;
case 4 /* Reset */:
if (_this.isActiveTarget(targetId)) {
// Reset the target and synthesizes removes for all existing
// documents. The backend will re-add any documents that still
// match the target before it sends the next global snapshot.
_this.resetTarget(targetId);
targetState.updateResumeToken(targetChange.resumeToken);
}
break;
default:
fail('Unknown target watch change state: ' + targetChange.state);
}
});
};
/**
* Iterates over all targetIds that the watch change applies to: either the
* targetIds explicitly listed in the change or the targetIds of all currently
* active targets.
*/
WatchChangeAggregator.prototype.forEachTarget = function (targetChange, fn) {
var _this = this;
if (targetChange.targetIds.length > 0) {
targetChange.targetIds.forEach(fn);
}
else {
this.targetStates.forEach(function (_, targetId) {
if (_this.isActiveTarget(targetId)) {
fn(targetId);
}
});
}
};
/**
* Handles existence filters and synthesizes deletes for filter mismatches.
* Targets that are invalidated by filter mismatches are added to
* `pendingTargetResets`.
*/
WatchChangeAggregator.prototype.handleExistenceFilter = function (watchChange) {
var targetId = watchChange.targetId;
var expectedCount = watchChange.existenceFilter.count;
var targetData = this.targetDataForActiveTarget(targetId);
if (targetData) {
var target = targetData.target;
if (target.isDocumentQuery()) {
if (expectedCount === 0) {
// The existence filter told us the document does not exist. We deduce
// that this document does not exist and apply a deleted document to
// our updates. Without applying this deleted document there might be
// another query that will raise this document as part of a snapshot
// until it is resolved, essentially exposing inconsistency between
// queries.
var key = new DocumentKey(target.path);
this.removeDocumentFromTarget(targetId, key, new NoDocument(key, SnapshotVersion.min()));
}
else {
hardAssert(expectedCount === 1, 'Single document existence filter with count: ' + expectedCount);
}
}
else {
var currentSize = this.getCurrentDocumentCountForTarget(targetId);
if (currentSize !== expectedCount) {
// Existence filter mismatch: We reset the mapping and raise a new
// snapshot with `isFromCache:true`.
this.resetTarget(targetId);
this.pendingTargetResets = this.pendingTargetResets.add(targetId);
}
}
}
};
/**
* Converts the currently accumulated state into a remote event at the
* provided snapshot version. Resets the accumulated changes before returning.
*/
WatchChangeAggregator.prototype.createRemoteEvent = function (snapshotVersion) {
var _this = this;
var targetChanges = new Map();
this.targetStates.forEach(function (targetState, targetId) {
var targetData = _this.targetDataForActiveTarget(targetId);
if (targetData) {
if (targetState.current && targetData.target.isDocumentQuery()) {
// Document queries for document that don't exist can produce an empty
// result set. To update our local cache, we synthesize a document
// delete if we have not previously received the document. This
// resolves the limbo state of the document, removing it from
// limboDocumentRefs.
//
// TODO(dimond): Ideally we would have an explicit lookup target
// instead resulting in an explicit delete message and we could
// remove this special logic.
var key = new DocumentKey(targetData.target.path);
if (_this.pendingDocumentUpdates.get(key) === null &&
!_this.targetContainsDocument(targetId, key)) {
_this.removeDocumentFromTarget(targetId, key, new NoDocument(key, snapshotVersion));
}
}
if (targetState.hasPendingChanges) {
targetChanges.set(targetId, targetState.toTargetChange());
targetState.clearPendingChanges();
}
}
});
var resolvedLimboDocuments = documentKeySet();
// We extract the set of limbo-only document updates as the GC logic
// special-cases documents that do not appear in the target cache.
//
// TODO(gsoltis): Expand on this comment once GC is available in the JS
// client.
this.pendingDocumentTargetMapping.forEach(function (key, targets) {
var isOnlyLimboTarget = true;
targets.forEachWhile(function (targetId) {
var targetData = _this.targetDataForActiveTarget(targetId);
if (targetData &&
targetData.purpose !== 2 /* LimboResolution */) {
isOnlyLimboTarget = false;
return false;
}
return true;
});
if (isOnlyLimboTarget) {
resolvedLimboDocuments = resolvedLimboDocuments.add(key);
}
});
var remoteEvent = new RemoteEvent(snapshotVersion, targetChanges, this.pendingTargetResets, this.pendingDocumentUpdates, resolvedLimboDocuments);
this.pendingDocumentUpdates = maybeDocumentMap();
this.pendingDocumentTargetMapping = documentTargetMap();
this.pendingTargetResets = new SortedSet(primitiveComparator);
return remoteEvent;
};
/**
* Adds the provided document to the internal list of document updates and
* its document key to the given target's mapping.
*/
// Visible for testing.
WatchChangeAggregator.prototype.addDocumentToTarget = function (targetId, document) {
if (!this.isActiveTarget(targetId)) {
return;
}
var changeType = this.targetContainsDocument(targetId, document.key)
? 2 /* Modified */
: 0 /* Added */;
var targetState = this.ensureTargetState(targetId);
targetState.addDocumentChange(document.key, changeType);
this.pendingDocumentUpdates = this.pendingDocumentUpdates.insert(document.key, document);
this.pendingDocumentTargetMapping = this.pendingDocumentTargetMapping.insert(document.key, this.ensureDocumentTargetMapping(document.key).add(targetId));
};
/**
* Removes the provided document from the target mapping. If the
* document no longer matches the target, but the document's state is still
* known (e.g. we know that the document was deleted or we received the change
* that caused the filter mismatch), the new document can be provided
* to update the remote document cache.
*/
// Visible for testing.
WatchChangeAggregator.prototype.removeDocumentFromTarget = function (targetId, key, updatedDocument) {
if (!this.isActiveTarget(targetId)) {
return;
}
var targetState = this.ensureTargetState(targetId);
if (this.targetContainsDocument(targetId, key)) {
targetState.addDocumentChange(key, 1 /* Removed */);
}
else {
// The document may have entered and left the target before we raised a
// snapshot, so we can just ignore the change.
targetState.removeDocumentChange(key);
}
this.pendingDocumentTargetMapping = this.pendingDocumentTargetMapping.insert(key, this.ensureDocumentTargetMapping(key).delete(targetId));
if (updatedDocument) {
this.pendingDocumentUpdates = this.pendingDocumentUpdates.insert(key, updatedDocument);
}
};
WatchChangeAggregator.prototype.removeTarget = function (targetId) {
this.targetStates.delete(targetId);
};
/**
* Returns the current count of documents in the target. This includes both
* the number of documents that the LocalStore considers to be part of the
* target as well as any accumulated changes.
*/
WatchChangeAggregator.prototype.getCurrentDocumentCountForTarget = function (targetId) {
var targetState = this.ensureTargetState(targetId);
var targetChange = targetState.toTargetChange();
return (this.metadataProvider.getRemoteKeysForTarget(targetId).size +
targetChange.addedDocuments.size -
targetChange.removedDocuments.size);
};
/**
* Increment the number of acks needed from watch before we can consider the
* server to be 'in-sync' with the client's active targets.
*/
WatchChangeAggregator.prototype.recordPendingTargetRequest = function (targetId) {
// For each request we get we need to record we need a response for it.
var targetState = this.ensureTargetState(targetId);
targetState.recordPendingTargetRequest();
};
WatchChangeAggregator.prototype.ensureTargetState = function (targetId) {
var result = this.targetStates.get(targetId);
if (!result) {
result = new TargetState();
this.targetStates.set(targetId, result);
}
return result;
};
WatchChangeAggregator.prototype.ensureDocumentTargetMapping = function (key) {
var targetMapping = this.pendingDocumentTargetMapping.get(key);
if (!targetMapping) {
targetMapping = new SortedSet(primitiveComparator);
this.pendingDocumentTargetMapping = this.pendingDocumentTargetMapping.insert(key, targetMapping);
}
return targetMapping;
};
/**
* Verifies that the user is still interested in this target (by calling
* `getTargetDataForTarget()`) and that we are not waiting for pending ADDs
* from watch.
*/
WatchChangeAggregator.prototype.isActiveTarget = function (targetId) {
var targetActive = this.targetDataForActiveTarget(targetId) !== null;
if (!targetActive) {
logDebug(LOG_TAG$a, 'Detected inactive target', targetId);
}
return targetActive;
};
/**
* Returns the TargetData for an active target (i.e. a target that the user
* is still interested in that has no outstanding target change requests).
*/
WatchChangeAggregator.prototype.targetDataForActiveTarget = function (targetId) {
var targetState = this.targetStates.get(targetId);
return targetState && targetState.isPending
? null
: this.metadataProvider.getTargetDataForTarget(targetId);
};
/**
* Resets the state of a Watch target to its initial state (e.g. sets
* 'current' to false, clears the resume token and removes its target mapping
* from all documents).
*/
WatchChangeAggregator.prototype.resetTarget = function (targetId) {
var _this = this;
debugAssert(!this.targetStates.get(targetId).isPending, 'Should only reset active targets');
this.targetStates.set(targetId, new TargetState());
// Trigger removal for any documents currently mapped to this target.
// These removals will be part of the initial snapshot if Watch does not
// resend these documents.
var existingKeys = this.metadataProvider.getRemoteKeysForTarget(targetId);
existingKeys.forEach(function (key) {
_this.removeDocumentFromTarget(targetId, key, /*updatedDocument=*/ null);
});
};
/**
* Returns whether the LocalStore considers the document to be part of the
* specified target.
*/
WatchChangeAggregator.prototype.targetContainsDocument = function (targetId, key) {
var existingKeys = this.metadataProvider.getRemoteKeysForTarget(targetId);
return existingKeys.has(key);
};
return WatchChangeAggregator;
}());
function documentTargetMap() {
return new SortedMap(DocumentKey.comparator);
}
function snapshotChangesMap() {
return new SortedMap(DocumentKey.comparator);
}
/**
* @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.
*/
var LOG_TAG$b = 'RemoteStore';
// TODO(b/35853402): Negotiate this with the stream.
var MAX_PENDING_WRITES = 10;
/**
* RemoteStore - An interface to remotely stored data, basically providing a
* wrapper around the Datastore that is more reliable for the rest of the
* system.
*
* RemoteStore is responsible for maintaining the connection to the server.
* - maintaining a list of active listens.
* - reconnecting when the connection is dropped.
* - resuming all the active listens on reconnect.
*
* RemoteStore handles all incoming events from the Datastore.
* - listening to the watch stream and repackaging the events as RemoteEvents
* - notifying SyncEngine of any changes to the active listens.
*
* RemoteStore takes writes from other components and handles them reliably.
* - pulling pending mutations from LocalStore and sending them to Datastore.
* - retrying mutations that failed because of network problems.
* - acking mutations to the SyncEngine once they are accepted or rejected.
*/
var RemoteStore = /** @class */ (function () {
function RemoteStore(
/**
* The local store, used to fill the write pipeline with outbound mutations.
*/
localStore,
/** The client-side proxy for interacting with the backend. */
datastore, asyncQueue, onlineStateHandler, connectivityMonitor) {
var _this = this;
this.localStore = localStore;
this.datastore = datastore;
this.asyncQueue = asyncQueue;
/**
* A list of up to MAX_PENDING_WRITES writes that we have fetched from the
* LocalStore via fillWritePipeline() and have or will send to the write
* stream.
*
* Whenever writePipeline.length > 0 the RemoteStore will attempt to start or
* restart the write stream. When the stream is established the writes in the
* pipeline will be sent in order.
*
* Writes remain in writePipeline until they are acknowledged by the backend
* and thus will automatically be re-sent if the stream is interrupted /
* restarted before they're acknowledged.
*
* Write responses from the backend are linked to their originating request
* purely based on order, and so we can just shift() writes from the front of
* the writePipeline as we receive responses.
*/
this.writePipeline = [];
/**
* A mapping of watched targets that the client cares about tracking and the
* user has explicitly called a 'listen' for this target.
*
* These targets may or may not have been sent to or acknowledged by the
* server. On re-establishing the listen stream, these targets should be sent
* to the server. The targets removed with unlistens are removed eagerly
* without waiting for confirmation from the listen stream.
*/
this.listenTargets = new Map();
this.watchChangeAggregator = null;
/**
* Set to true by enableNetwork() and false by disableNetwork() and indicates
* the user-preferred network state.
*/
this.networkEnabled = false;
this.isPrimary = false;
/**
* When set to `true`, the network was taken offline due to an IndexedDB
* failure. The state is flipped to `false` when access becomes available
* again.
*/
this.indexedDbFailed = false;
this.connectivityMonitor = connectivityMonitor;
this.connectivityMonitor.addCallback(function (status) {
asyncQueue.enqueueAndForget(function () { return tslib.__awaiter(_this, void 0, void 0, function () {
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
if (!this.canUseNetwork()) return [3 /*break*/, 2];
logDebug(LOG_TAG$b, 'Restarting streams for network reachability change.');
return [4 /*yield*/, this.restartNetwork()];
case 1:
_e.sent();
_e.label = 2;
case 2: return [2 /*return*/];
}
});
}); });
});
this.onlineStateTracker = new OnlineStateTracker(asyncQueue, onlineStateHandler);
// Create streams (but note they're not started yet).
this.watchStream = newPersistentWatchStream(this.datastore, asyncQueue, {
onOpen: this.onWatchStreamOpen.bind(this),
onClose: this.onWatchStreamClose.bind(this),
onWatchChange: this.onWatchStreamChange.bind(this)
});
this.writeStream = newPersistentWriteStream(this.datastore, asyncQueue, {
onOpen: this.onWriteStreamOpen.bind(this),
onClose: this.onWriteStreamClose.bind(this),
onHandshakeComplete: this.onWriteHandshakeComplete.bind(this),
onMutationResult: this.onMutationResult.bind(this)
});
}
/**
* Starts up the remote store, creating streams, restoring state from
* LocalStore, etc.
*/
RemoteStore.prototype.start = function () {
return this.enableNetwork();
};
/** Re-enables the network. Idempotent. */
RemoteStore.prototype.enableNetwork = function () {
this.networkEnabled = true;
return this.enableNetworkInternal();
};
RemoteStore.prototype.enableNetworkInternal = function () {
return tslib.__awaiter(this, void 0, void 0, function () {
var _e;
return tslib.__generator(this, function (_f) {
switch (_f.label) {
case 0:
if (!this.canUseNetwork()) return [3 /*break*/, 3];
_e = this.writeStream;
return [4 /*yield*/, this.localStore.getLastStreamToken()];
case 1:
_e.lastStreamToken = _f.sent();
if (this.shouldStartWatchStream()) {
this.startWatchStream();
}
else {
this.onlineStateTracker.set("Unknown" /* Unknown */);
}
// This will start the write stream if necessary.
return [4 /*yield*/, this.fillWritePipeline()];
case 2:
// This will start the write stream if necessary.
_f.sent();
_f.label = 3;
case 3: return [2 /*return*/];
}
});
});
};
/**
* Temporarily disables the network. The network can be re-enabled using
* enableNetwork().
*/
RemoteStore.prototype.disableNetwork = function () {
return tslib.__awaiter(this, void 0, void 0, function () {
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
this.networkEnabled = false;
return [4 /*yield*/, this.disableNetworkInternal()];
case 1:
_e.sent();
// Set the OnlineState to Offline so get()s return from cache, etc.
this.onlineStateTracker.set("Offline" /* Offline */);
return [2 /*return*/];
}
});
});
};
RemoteStore.prototype.disableNetworkInternal = function () {
return tslib.__awaiter(this, void 0, void 0, function () {
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0: return [4 /*yield*/, this.writeStream.stop()];
case 1:
_e.sent();
return [4 /*yield*/, this.watchStream.stop()];
case 2:
_e.sent();
if (this.writePipeline.length > 0) {
logDebug(LOG_TAG$b, "Stopping write stream with " + this.writePipeline.length + " pending writes");
this.writePipeline = [];
}
this.cleanUpWatchStreamState();
return [2 /*return*/];
}
});
});
};
RemoteStore.prototype.shutdown = function () {
return tslib.__awaiter(this, void 0, void 0, function () {
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
logDebug(LOG_TAG$b, 'RemoteStore shutting down.');
this.networkEnabled = false;
return [4 /*yield*/, this.disableNetworkInternal()];
case 1:
_e.sent();
this.connectivityMonitor.shutdown();
// Set the OnlineState to Unknown (rather than Offline) to avoid potentially
// triggering spurious listener events with cached data, etc.
this.onlineStateTracker.set("Unknown" /* Unknown */);
return [2 /*return*/];
}
});
});
};
/**
* Starts new listen for the given target. Uses resume token if provided. It
* is a no-op if the target of given `TargetData` is already being listened to.
*/
RemoteStore.prototype.listen = function (targetData) {
if (this.listenTargets.has(targetData.targetId)) {
return;
}
// Mark this as something the client is currently listening for.
this.listenTargets.set(targetData.targetId, targetData);
if (this.shouldStartWatchStream()) {
// The listen will be sent in onWatchStreamOpen
this.startWatchStream();
}
else if (this.watchStream.isOpen()) {
this.sendWatchRequest(targetData);
}
};
/**
* Removes the listen from server. It is a no-op if the given target id is
* not being listened to.
*/
RemoteStore.prototype.unlisten = function (targetId) {
debugAssert(this.listenTargets.has(targetId), "unlisten called on target no currently watched: " + targetId);
this.listenTargets.delete(targetId);
if (this.watchStream.isOpen()) {
this.sendUnwatchRequest(targetId);
}
if (this.listenTargets.size === 0) {
if (this.watchStream.isOpen()) {
this.watchStream.markIdle();
}
else if (this.canUseNetwork()) {
// Revert to OnlineState.Unknown if the watch stream is not open and we
// have no listeners, since without any listens to send we cannot
// confirm if the stream is healthy and upgrade to OnlineState.Online.
this.onlineStateTracker.set("Unknown" /* Unknown */);
}
}
};
/** {@link TargetMetadataProvider.getTargetDataForTarget} */
RemoteStore.prototype.getTargetDataForTarget = function (targetId) {
return this.listenTargets.get(targetId) || null;
};
/** {@link TargetMetadataProvider.getRemoteKeysForTarget} */
RemoteStore.prototype.getRemoteKeysForTarget = function (targetId) {
return this.syncEngine.getRemoteKeysForTarget(targetId);
};
/**
* We need to increment the the expected number of pending responses we're due
* from watch so we wait for the ack to process any messages from this target.
*/
RemoteStore.prototype.sendWatchRequest = function (targetData) {
this.watchChangeAggregator.recordPendingTargetRequest(targetData.targetId);
this.watchStream.watch(targetData);
};
/**
* We need to increment the expected number of pending responses we're due
* from watch so we wait for the removal on the server before we process any
* messages from this target.
*/
RemoteStore.prototype.sendUnwatchRequest = function (targetId) {
this.watchChangeAggregator.recordPendingTargetRequest(targetId);
this.watchStream.unwatch(targetId);
};
RemoteStore.prototype.startWatchStream = function () {
debugAssert(this.shouldStartWatchStream(), 'startWatchStream() called when shouldStartWatchStream() is false.');
this.watchChangeAggregator = new WatchChangeAggregator(this);
this.watchStream.start();
this.onlineStateTracker.handleWatchStreamStart();
};
/**
* Returns whether the watch stream should be started because it's necessary
* and has not yet been started.
*/
RemoteStore.prototype.shouldStartWatchStream = function () {
return (this.canUseNetwork() &&
!this.watchStream.isStarted() &&
this.listenTargets.size > 0);
};
RemoteStore.prototype.canUseNetwork = function () {
return !this.indexedDbFailed && this.isPrimary && this.networkEnabled;
};
RemoteStore.prototype.cleanUpWatchStreamState = function () {
this.watchChangeAggregator = null;
};
RemoteStore.prototype.onWatchStreamOpen = function () {
return tslib.__awaiter(this, void 0, void 0, function () {
var _this = this;
return tslib.__generator(this, function (_e) {
this.listenTargets.forEach(function (targetData, targetId) {
_this.sendWatchRequest(targetData);
});
return [2 /*return*/];
});
});
};
RemoteStore.prototype.onWatchStreamClose = function (error) {
return tslib.__awaiter(this, void 0, void 0, function () {
return tslib.__generator(this, function (_e) {
if (error === undefined) {
// Graceful stop (due to stop() or idle timeout). Make sure that's
// desirable.
debugAssert(!this.shouldStartWatchStream(), 'Watch stream was stopped gracefully while still needed.');
}
this.cleanUpWatchStreamState();
// If we still need the watch stream, retry the connection.
if (this.shouldStartWatchStream()) {
this.onlineStateTracker.handleWatchStreamFailure(error);
this.startWatchStream();
}
else {
// No need to restart watch stream because there are no active targets.
// The online state is set to unknown because there is no active attempt
// at establishing a connection
this.onlineStateTracker.set("Unknown" /* Unknown */);
}
return [2 /*return*/];
});
});
};
RemoteStore.prototype.onWatchStreamChange = function (watchChange, snapshotVersion) {
return tslib.__awaiter(this, void 0, void 0, function () {
var e_6, lastRemoteSnapshotVersion, e_7;
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
// Mark the client as online since we got a message from the server
this.onlineStateTracker.set("Online" /* Online */);
if (!(watchChange instanceof WatchTargetChange &&
watchChange.state === 2 /* Removed */ &&
watchChange.cause)) return [3 /*break*/, 6];
_e.label = 1;
case 1:
_e.trys.push([1, 3, , 5]);
return [4 /*yield*/, this.handleTargetError(watchChange)];
case 2:
_e.sent();
return [3 /*break*/, 5];
case 3:
e_6 = _e.sent();
logDebug(LOG_TAG$b, 'Failed to remove targets %s: %s ', watchChange.targetIds.join(','), e_6);
return [4 /*yield*/, this.disableNetworkUntilRecovery(e_6)];
case 4:
_e.sent();
return [3 /*break*/, 5];
case 5: return [2 /*return*/];
case 6:
if (watchChange instanceof DocumentWatchChange) {
this.watchChangeAggregator.handleDocumentChange(watchChange);
}
else if (watchChange instanceof ExistenceFilterChange) {
this.watchChangeAggregator.handleExistenceFilter(watchChange);
}
else {
debugAssert(watchChange instanceof WatchTargetChange, 'Expected watchChange to be an instance of WatchTargetChange');
this.watchChangeAggregator.handleTargetChange(watchChange);
}
if (!!snapshotVersion.isEqual(SnapshotVersion.min())) return [3 /*break*/, 13];
_e.label = 7;
case 7:
_e.trys.push([7, 11, , 13]);
return [4 /*yield*/, this.localStore.getLastRemoteSnapshotVersion()];
case 8:
lastRemoteSnapshotVersion = _e.sent();
if (!(snapshotVersion.compareTo(lastRemoteSnapshotVersion) >= 0)) return [3 /*break*/, 10];
// We have received a target change with a global snapshot if the snapshot
// version is not equal to SnapshotVersion.min().
return [4 /*yield*/, this.raiseWatchSnapshot(snapshotVersion)];
case 9:
// We have received a target change with a global snapshot if the snapshot
// version is not equal to SnapshotVersion.min().
_e.sent();
_e.label = 10;
case 10: return [3 /*break*/, 13];
case 11:
e_7 = _e.sent();
logDebug(LOG_TAG$b, 'Failed to raise snapshot:', e_7);
return [4 /*yield*/, this.disableNetworkUntilRecovery(e_7)];
case 12:
_e.sent();
return [3 /*break*/, 13];
case 13: return [2 /*return*/];
}
});
});
};
/**
* Recovery logic for IndexedDB errors that takes the network offline until
* IndexedDb probing succeeds. Retries are scheduled with backoff using
* `enqueueRetryable()`.
*/
RemoteStore.prototype.disableNetworkUntilRecovery = function (e) {
return tslib.__awaiter(this, void 0, void 0, function () {
var _this = this;
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
if (!isIndexedDbTransactionError(e)) return [3 /*break*/, 2];
debugAssert(!this.indexedDbFailed, 'Unexpected network event when IndexedDB was marked failed.');
this.indexedDbFailed = true;
// Disable network and raise offline snapshots
return [4 /*yield*/, this.disableNetworkInternal()];
case 1:
// Disable network and raise offline snapshots
_e.sent();
this.onlineStateTracker.set("Offline" /* Offline */);
// Probe IndexedDB periodically and re-enable network
this.asyncQueue.enqueueRetryable(function () { return tslib.__awaiter(_this, void 0, void 0, function () {
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
logDebug(LOG_TAG$b, 'Retrying IndexedDB access');
// Issue a simple read operation to determine if IndexedDB recovered.
// Ideally, we would expose a health check directly on SimpleDb, but
// RemoteStore only has access to persistence through LocalStore.
return [4 /*yield*/, this.localStore.getLastRemoteSnapshotVersion()];
case 1:
// Issue a simple read operation to determine if IndexedDB recovered.
// Ideally, we would expose a health check directly on SimpleDb, but
// RemoteStore only has access to persistence through LocalStore.
_e.sent();
this.indexedDbFailed = false;
return [4 /*yield*/, this.enableNetworkInternal()];
case 2:
_e.sent();
return [2 /*return*/];
}
});
}); });
return [3 /*break*/, 3];
case 2: throw e;
case 3: return [2 /*return*/];
}
});
});
};
/**
* Takes a batch of changes from the Datastore, repackages them as a
* RemoteEvent, and passes that on to the listener, which is typically the
* SyncEngine.
*/
RemoteStore.prototype.raiseWatchSnapshot = function (snapshotVersion) {
var _this = this;
debugAssert(!snapshotVersion.isEqual(SnapshotVersion.min()), "Can't raise event for unknown SnapshotVersion");
var remoteEvent = this.watchChangeAggregator.createRemoteEvent(snapshotVersion);
// Update in-memory resume tokens. LocalStore will update the
// persistent view of these when applying the completed RemoteEvent.
remoteEvent.targetChanges.forEach(function (change, targetId) {
if (change.resumeToken.approximateByteSize() > 0) {
var targetData = _this.listenTargets.get(targetId);
// A watched target might have been removed already.
if (targetData) {
_this.listenTargets.set(targetId, targetData.withResumeToken(change.resumeToken, snapshotVersion));
}
}
});
// Re-establish listens for the targets that have been invalidated by
// existence filter mismatches.
remoteEvent.targetMismatches.forEach(function (targetId) {
var targetData = _this.listenTargets.get(targetId);
if (!targetData) {
// A watched target might have been removed already.
return;
}
// Clear the resume token for the target, since we're in a known mismatch
// state.
_this.listenTargets.set(targetId, targetData.withResumeToken(ByteString.EMPTY_BYTE_STRING, targetData.snapshotVersion));
// Cause a hard reset by unwatching and rewatching immediately, but
// deliberately don't send a resume token so that we get a full update.
_this.sendUnwatchRequest(targetId);
// Mark the target we send as being on behalf of an existence filter
// mismatch, but don't actually retain that in listenTargets. This ensures
// that we flag the first re-listen this way without impacting future
// listens of this target (that might happen e.g. on reconnect).
var requestTargetData = new TargetData(targetData.target, targetId, 1 /* ExistenceFilterMismatch */, targetData.sequenceNumber);
_this.sendWatchRequest(requestTargetData);
});
// Finally raise remote event
return this.syncEngine.applyRemoteEvent(remoteEvent);
};
/** Handles an error on a target */
RemoteStore.prototype.handleTargetError = function (watchChange) {
return tslib.__awaiter(this, void 0, void 0, function () {
var error, _i, _e, targetId;
return tslib.__generator(this, function (_f) {
switch (_f.label) {
case 0:
debugAssert(!!watchChange.cause, 'Handling target error without a cause');
error = watchChange.cause;
_i = 0, _e = watchChange.targetIds;
_f.label = 1;
case 1:
if (!(_i < _e.length)) return [3 /*break*/, 4];
targetId = _e[_i];
if (!this.listenTargets.has(targetId)) return [3 /*break*/, 3];
return [4 /*yield*/, this.syncEngine.rejectListen(targetId, error)];
case 2:
_f.sent();
this.listenTargets.delete(targetId);
this.watchChangeAggregator.removeTarget(targetId);
_f.label = 3;
case 3:
_i++;
return [3 /*break*/, 1];
case 4: return [2 /*return*/];
}
});
});
};
/**
* Attempts to fill our write pipeline with writes from the LocalStore.
*
* Called internally to bootstrap or refill the write pipeline and by
* SyncEngine whenever there are new mutations to process.
*
* Starts the write stream if necessary.
*/
RemoteStore.prototype.fillWritePipeline = function () {
return tslib.__awaiter(this, void 0, void 0, function () {
var lastBatchIdRetrieved, batch;
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
if (!this.canAddToWritePipeline()) return [3 /*break*/, 4];
lastBatchIdRetrieved = this.writePipeline.length > 0
? this.writePipeline[this.writePipeline.length - 1].batchId
: BATCHID_UNKNOWN;
return [4 /*yield*/, this.localStore.nextMutationBatch(lastBatchIdRetrieved)];
case 1:
batch = _e.sent();
if (!(batch === null)) return [3 /*break*/, 2];
if (this.writePipeline.length === 0) {
this.writeStream.markIdle();
}
return [3 /*break*/, 4];
case 2:
this.addToWritePipeline(batch);
return [4 /*yield*/, this.fillWritePipeline()];
case 3:
_e.sent();
_e.label = 4;
case 4:
if (this.shouldStartWriteStream()) {
this.startWriteStream();
}
return [2 /*return*/];
}
});
});
};
/**
* Returns true if we can add to the write pipeline (i.e. the network is
* enabled and the write pipeline is not full).
*/
RemoteStore.prototype.canAddToWritePipeline = function () {
return (this.canUseNetwork() && this.writePipeline.length < MAX_PENDING_WRITES);
};
// For testing
RemoteStore.prototype.outstandingWrites = function () {
return this.writePipeline.length;
};
/**
* Queues additional writes to be sent to the write stream, sending them
* immediately if the write stream is established.
*/
RemoteStore.prototype.addToWritePipeline = function (batch) {
debugAssert(this.canAddToWritePipeline(), 'addToWritePipeline called when pipeline is full');
this.writePipeline.push(batch);
if (this.writeStream.isOpen() && this.writeStream.handshakeComplete) {
this.writeStream.writeMutations(batch.mutations);
}
};
RemoteStore.prototype.shouldStartWriteStream = function () {
return (this.canUseNetwork() &&
!this.writeStream.isStarted() &&
this.writePipeline.length > 0);
};
RemoteStore.prototype.startWriteStream = function () {
debugAssert(this.shouldStartWriteStream(), 'startWriteStream() called when shouldStartWriteStream() is false.');
this.writeStream.start();
};
RemoteStore.prototype.onWriteStreamOpen = function () {
return tslib.__awaiter(this, void 0, void 0, function () {
return tslib.__generator(this, function (_e) {
this.writeStream.writeHandshake();
return [2 /*return*/];
});
});
};
RemoteStore.prototype.onWriteHandshakeComplete = function () {
var _this = this;
// Record the stream token.
return this.localStore
.setLastStreamToken(this.writeStream.lastStreamToken)
.then(function () {
// Send the write pipeline now that the stream is established.
for (var _i = 0, _e = _this.writePipeline; _i < _e.length; _i++) {
var batch = _e[_i];
_this.writeStream.writeMutations(batch.mutations);
}
})
.catch(ignoreIfPrimaryLeaseLoss);
};
RemoteStore.prototype.onMutationResult = function (commitVersion, results) {
var _this = this;
// This is a response to a write containing mutations and should be
// correlated to the first write in our write pipeline.
debugAssert(this.writePipeline.length > 0, 'Got result for empty write pipeline');
var batch = this.writePipeline.shift();
var success = MutationBatchResult.from(batch, commitVersion, results, this.writeStream.lastStreamToken);
return this.syncEngine.applySuccessfulWrite(success).then(function () {
// It's possible that with the completion of this mutation another
// slot has freed up.
return _this.fillWritePipeline();
});
};
RemoteStore.prototype.onWriteStreamClose = function (error) {
return tslib.__awaiter(this, void 0, void 0, function () {
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
if (error === undefined) {
// Graceful stop (due to stop() or idle timeout). Make sure that's
// desirable.
debugAssert(!this.shouldStartWriteStream(), 'Write stream was stopped gracefully while still needed.');
}
if (!(error && this.writePipeline.length > 0)) return [3 /*break*/, 5];
if (!this.writeStream.handshakeComplete) return [3 /*break*/, 2];
// This error affects the actual write.
return [4 /*yield*/, this.handleWriteError(error)];
case 1:
// This error affects the actual write.
_e.sent();
return [3 /*break*/, 4];
case 2:
// If there was an error before the handshake has finished, it's
// possible that the server is unable to process the stream token
// we're sending. (Perhaps it's too old?)
return [4 /*yield*/, this.handleHandshakeError(error)];
case 3:
// If there was an error before the handshake has finished, it's
// possible that the server is unable to process the stream token
// we're sending. (Perhaps it's too old?)
_e.sent();
_e.label = 4;
case 4:
// The write stream might have been started by refilling the write
// pipeline for failed writes
if (this.shouldStartWriteStream()) {
this.startWriteStream();
}
_e.label = 5;
case 5: return [2 /*return*/];
}
});
});
};
RemoteStore.prototype.handleHandshakeError = function (error) {
return tslib.__awaiter(this, void 0, void 0, function () {
return tslib.__generator(this, function (_e) {
// Reset the token if it's a permanent error, signaling the write stream is
// no longer valid. Note that the handshake does not count as a write: see
// comments on isPermanentWriteError for details.
if (isPermanentError(error.code)) {
logDebug(LOG_TAG$b, 'RemoteStore error before completed handshake; resetting stream token: ', this.writeStream.lastStreamToken);
this.writeStream.lastStreamToken = ByteString.EMPTY_BYTE_STRING;
return [2 /*return*/, this.localStore
.setLastStreamToken(ByteString.EMPTY_BYTE_STRING)
.catch(ignoreIfPrimaryLeaseLoss)];
}
return [2 /*return*/];
});
});
};
RemoteStore.prototype.handleWriteError = function (error) {
return tslib.__awaiter(this, void 0, void 0, function () {
var batch;
var _this = this;
return tslib.__generator(this, function (_e) {
// Only handle permanent errors here. If it's transient, just let the retry
// logic kick in.
if (isPermanentWriteError(error.code)) {
batch = this.writePipeline.shift();
// In this case it's also unlikely that the server itself is melting
// down -- this was just a bad request so inhibit backoff on the next
// restart.
this.writeStream.inhibitBackoff();
return [2 /*return*/, this.syncEngine
.rejectFailedWrite(batch.batchId, error)
.then(function () {
// It's possible that with the completion of this mutation
// another slot has freed up.
return _this.fillWritePipeline();
})];
}
return [2 /*return*/];
});
});
};
RemoteStore.prototype.createTransaction = function () {
return new Transaction(this.datastore);
};
RemoteStore.prototype.restartNetwork = function () {
return tslib.__awaiter(this, void 0, void 0, function () {
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
this.networkEnabled = false;
return [4 /*yield*/, this.disableNetworkInternal()];
case 1:
_e.sent();
this.onlineStateTracker.set("Unknown" /* Unknown */);
return [4 /*yield*/, this.enableNetwork()];
case 2:
_e.sent();
return [2 /*return*/];
}
});
});
};
RemoteStore.prototype.handleCredentialChange = function () {
return tslib.__awaiter(this, void 0, void 0, function () {
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
if (!this.canUseNetwork()) return [3 /*break*/, 2];
// Tear down and re-create our network streams. This will ensure we get a fresh auth token
// for the new user and re-fill the write pipeline with new mutations from the LocalStore
// (since mutations are per-user).
logDebug(LOG_TAG$b, 'RemoteStore restarting streams for new credential');
return [4 /*yield*/, this.restartNetwork()];
case 1:
_e.sent();
_e.label = 2;
case 2: return [2 /*return*/];
}
});
});
};
/**
* Toggles the network state when the client gains or loses its primary lease.
*/
RemoteStore.prototype.applyPrimaryState = function (isPrimary) {
return tslib.__awaiter(this, void 0, void 0, function () {
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
this.isPrimary = isPrimary;
if (!(isPrimary && this.networkEnabled)) return [3 /*break*/, 2];
return [4 /*yield*/, this.enableNetwork()];
case 1:
_e.sent();
return [3 /*break*/, 4];
case 2:
if (!!isPrimary) return [3 /*break*/, 4];
return [4 /*yield*/, this.disableNetworkInternal()];
case 3:
_e.sent();
this.onlineStateTracker.set("Unknown" /* Unknown */);
_e.label = 4;
case 4: return [2 /*return*/];
}
});
});
};
return RemoteStore;
}());
/**
* @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.
*/
/**
* Holds the listeners and the last received ViewSnapshot for a query being
* tracked by EventManager.
*/
var QueryListenersInfo = /** @class */ (function () {
function QueryListenersInfo() {
this.viewSnap = undefined;
this.listeners = [];
}
return QueryListenersInfo;
}());
/**
* EventManager is responsible for mapping queries to query event emitters.
* It handles "fan-out". -- Identical queries will re-use the same watch on the
* backend.
*/
var EventManager = /** @class */ (function () {
function EventManager(syncEngine) {
this.syncEngine = syncEngine;
this.queries = new ObjectMap(function (q) { return q.canonicalId(); });
this.onlineState = "Unknown" /* Unknown */;
this.snapshotsInSyncListeners = new Set();
this.syncEngine.subscribe(this);
}
EventManager.prototype.listen = function (listener) {
return tslib.__awaiter(this, void 0, void 0, function () {
var query, firstListen, queryInfo, _e, e_8, firestoreError, raisedEvent, raisedEvent_1;
return tslib.__generator(this, function (_f) {
switch (_f.label) {
case 0:
query = listener.query;
firstListen = false;
queryInfo = this.queries.get(query);
if (!queryInfo) {
firstListen = true;
queryInfo = new QueryListenersInfo();
}
if (!firstListen) return [3 /*break*/, 4];
_f.label = 1;
case 1:
_f.trys.push([1, 3, , 4]);
_e = queryInfo;
return [4 /*yield*/, this.syncEngine.listen(query)];
case 2:
_e.viewSnap = _f.sent();
return [3 /*break*/, 4];
case 3:
e_8 = _f.sent();
firestoreError = wrapInUserErrorIfRecoverable(e_8, "Initialization of query '" + listener.query + "' failed");
listener.onError(firestoreError);
return [2 /*return*/];
case 4:
this.queries.set(query, queryInfo);
queryInfo.listeners.push(listener);
raisedEvent = listener.applyOnlineStateChange(this.onlineState);
debugAssert(!raisedEvent, "applyOnlineStateChange() shouldn't raise an event for brand-new listeners.");
if (queryInfo.viewSnap) {
raisedEvent_1 = listener.onViewSnapshot(queryInfo.viewSnap);
if (raisedEvent_1) {
this.raiseSnapshotsInSyncEvent();
}
}
return [2 /*return*/];
}
});
});
};
EventManager.prototype.unlisten = function (listener) {
return tslib.__awaiter(this, void 0, void 0, function () {
var query, lastListen, queryInfo, i;
return tslib.__generator(this, function (_e) {
query = listener.query;
lastListen = false;
queryInfo = this.queries.get(query);
if (queryInfo) {
i = queryInfo.listeners.indexOf(listener);
if (i >= 0) {
queryInfo.listeners.splice(i, 1);
lastListen = queryInfo.listeners.length === 0;
}
}
if (lastListen) {
this.queries.delete(query);
return [2 /*return*/, this.syncEngine.unlisten(query)];
}
return [2 /*return*/];
});
});
};
EventManager.prototype.onWatchChange = function (viewSnaps) {
var raisedEvent = false;
for (var _i = 0, viewSnaps_1 = viewSnaps; _i < viewSnaps_1.length; _i++) {
var viewSnap = viewSnaps_1[_i];
var query = viewSnap.query;
var queryInfo = this.queries.get(query);
if (queryInfo) {
for (var _e = 0, _f = queryInfo.listeners; _e < _f.length; _e++) {
var listener = _f[_e];
if (listener.onViewSnapshot(viewSnap)) {
raisedEvent = true;
}
}
queryInfo.viewSnap = viewSnap;
}
}
if (raisedEvent) {
this.raiseSnapshotsInSyncEvent();
}
};
EventManager.prototype.onWatchError = function (query, error) {
var queryInfo = this.queries.get(query);
if (queryInfo) {
for (var _i = 0, _e = queryInfo.listeners; _i < _e.length; _i++) {
var listener = _e[_i];
listener.onError(error);
}
}
// Remove all listeners. NOTE: We don't need to call syncEngine.unlisten()
// after an error.
this.queries.delete(query);
};
EventManager.prototype.onOnlineStateChange = function (onlineState) {
this.onlineState = onlineState;
var raisedEvent = false;
this.queries.forEach(function (_, queryInfo) {
for (var _i = 0, _e = queryInfo.listeners; _i < _e.length; _i++) {
var listener = _e[_i];
// Run global snapshot listeners if a consistent snapshot has been emitted.
if (listener.applyOnlineStateChange(onlineState)) {
raisedEvent = true;
}
}
});
if (raisedEvent) {
this.raiseSnapshotsInSyncEvent();
}
};
EventManager.prototype.addSnapshotsInSyncListener = function (observer) {
this.snapshotsInSyncListeners.add(observer);
// Immediately fire an initial event, indicating all existing listeners
// are in-sync.
observer.next();
};
EventManager.prototype.removeSnapshotsInSyncListener = function (observer) {
this.snapshotsInSyncListeners.delete(observer);
};
// Call all global snapshot listeners that have been set.
EventManager.prototype.raiseSnapshotsInSyncEvent = function () {
this.snapshotsInSyncListeners.forEach(function (observer) {
observer.next();
});
};
return EventManager;
}());
/**
* QueryListener takes a series of internal view snapshots and determines
* when to raise the event.
*
* It uses an Observer to dispatch events.
*/
var QueryListener = /** @class */ (function () {
function QueryListener(query, queryObserver, options) {
this.query = query;
this.queryObserver = queryObserver;
/**
* Initial snapshots (e.g. from cache) may not be propagated to the wrapped
* observer. This flag is set to true once we've actually raised an event.
*/
this.raisedInitialEvent = false;
this.snap = null;
this.onlineState = "Unknown" /* Unknown */;
this.options = options || {};
}
/**
* Applies the new ViewSnapshot to this listener, raising a user-facing event
* if applicable (depending on what changed, whether the user has opted into
* metadata-only changes, etc.). Returns true if a user-facing event was
* indeed raised.
*/
QueryListener.prototype.onViewSnapshot = function (snap) {
debugAssert(snap.docChanges.length > 0 || snap.syncStateChanged, 'We got a new snapshot with no changes?');
if (!this.options.includeMetadataChanges) {
// Remove the metadata only changes.
var docChanges = [];
for (var _i = 0, _e = snap.docChanges; _i < _e.length; _i++) {
var docChange = _e[_i];
if (docChange.type !== 3 /* Metadata */) {
docChanges.push(docChange);
}
}
snap = new ViewSnapshot(snap.query, snap.docs, snap.oldDocs, docChanges, snap.mutatedKeys, snap.fromCache, snap.syncStateChanged,
/* excludesMetadataChanges= */ true);
}
var raisedEvent = false;
if (!this.raisedInitialEvent) {
if (this.shouldRaiseInitialEvent(snap, this.onlineState)) {
this.raiseInitialEvent(snap);
raisedEvent = true;
}
}
else if (this.shouldRaiseEvent(snap)) {
this.queryObserver.next(snap);
raisedEvent = true;
}
this.snap = snap;
return raisedEvent;
};
QueryListener.prototype.onError = function (error) {
this.queryObserver.error(error);
};
/** Returns whether a snapshot was raised. */
QueryListener.prototype.applyOnlineStateChange = function (onlineState) {
this.onlineState = onlineState;
var raisedEvent = false;
if (this.snap &&
!this.raisedInitialEvent &&
this.shouldRaiseInitialEvent(this.snap, onlineState)) {
this.raiseInitialEvent(this.snap);
raisedEvent = true;
}
return raisedEvent;
};
QueryListener.prototype.shouldRaiseInitialEvent = function (snap, onlineState) {
debugAssert(!this.raisedInitialEvent, 'Determining whether to raise first event but already had first event');
// Always raise the first event when we're synced
if (!snap.fromCache) {
return true;
}
// NOTE: We consider OnlineState.Unknown as online (it should become Offline
// or Online if we wait long enough).
var maybeOnline = onlineState !== "Offline" /* Offline */;
// Don't raise the event if we're online, aren't synced yet (checked
// above) and are waiting for a sync.
if (this.options.waitForSyncWhenOnline && maybeOnline) {
debugAssert(snap.fromCache, 'Waiting for sync, but snapshot is not from cache');
return false;
}
// Raise data from cache if we have any documents or we are offline
return !snap.docs.isEmpty() || onlineState === "Offline" /* Offline */;
};
QueryListener.prototype.shouldRaiseEvent = function (snap) {
// We don't need to handle includeDocumentMetadataChanges here because
// the Metadata only changes have already been stripped out if needed.
// At this point the only changes we will see are the ones we should
// propagate.
if (snap.docChanges.length > 0) {
return true;
}
var hasPendingWritesChanged = this.snap && this.snap.hasPendingWrites !== snap.hasPendingWrites;
if (snap.syncStateChanged || hasPendingWritesChanged) {
return this.options.includeMetadataChanges === true;
}
// Generally we should have hit one of the cases above, but it's possible
// to get here if there were only metadata docChanges and they got
// stripped out.
return false;
};
QueryListener.prototype.raiseInitialEvent = function (snap) {
debugAssert(!this.raisedInitialEvent, 'Trying to raise initial events for second time');
snap = ViewSnapshot.fromInitialDocuments(snap.query, snap.docs, snap.mutatedKeys, snap.fromCache);
this.raisedInitialEvent = true;
this.queryObserver.next(snap);
};
return QueryListener;
}());
/**
* @license
* Copyright 2019 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.
*/
// TOOD(b/140938512): Drop SimpleQueryEngine and rename IndexFreeQueryEngine.
/**
* A query engine that takes advantage of the target document mapping in the
* QueryCache. The IndexFreeQueryEngine optimizes query execution by only
* reading the documents that previously matched a query plus any documents that were
* edited after the query was last listened to.
*
* There are some cases where Index-Free queries are not guaranteed to produce
* the same results as full collection scans. In these cases, the
* IndexFreeQueryEngine falls back to full query processing. These cases are:
*
* - Limit queries where a document that matched the query previously no longer
* matches the query.
*
* - Limit queries where a document edit may cause the document to sort below
* another document that is in the local cache.
*
* - Queries that have never been CURRENT or free of Limbo documents.
*/
var IndexFreeQueryEngine = /** @class */ (function () {
function IndexFreeQueryEngine() {
}
IndexFreeQueryEngine.prototype.setLocalDocumentsView = function (localDocuments) {
this.localDocumentsView = localDocuments;
};
IndexFreeQueryEngine.prototype.getDocumentsMatchingQuery = function (transaction, query, lastLimboFreeSnapshotVersion, remoteKeys) {
var _this = this;
debugAssert(this.localDocumentsView !== undefined, 'setLocalDocumentsView() not called');
// Queries that match all documents don't benefit from using
// IndexFreeQueries. It is more efficient to scan all documents in a
// collection, rather than to perform individual lookups.
if (query.matchesAllDocuments()) {
return this.executeFullCollectionScan(transaction, query);
}
// Queries that have never seen a snapshot without limbo free documents
// should also be run as a full collection scan.
if (lastLimboFreeSnapshotVersion.isEqual(SnapshotVersion.min())) {
return this.executeFullCollectionScan(transaction, query);
}
return this.localDocumentsView.getDocuments(transaction, remoteKeys).next(function (documents) {
var previousResults = _this.applyQuery(query, documents);
if ((query.hasLimitToFirst() || query.hasLimitToLast()) &&
_this.needsRefill(query.limitType, previousResults, remoteKeys, lastLimboFreeSnapshotVersion)) {
return _this.executeFullCollectionScan(transaction, query);
}
if (getLogLevel() <= logger.LogLevel.DEBUG) {
logDebug('IndexFreeQueryEngine', 'Re-using previous result from %s to execute query: %s', lastLimboFreeSnapshotVersion.toString(), query.toString());
}
// Retrieve all results for documents that were updated since the last
// limbo-document free remote snapshot.
return _this.localDocumentsView.getDocumentsMatchingQuery(transaction, query, lastLimboFreeSnapshotVersion).next(function (updatedResults) {
// We merge `previousResults` into `updateResults`, since
// `updateResults` is already a DocumentMap. If a document is
// contained in both lists, then its contents are the same.
previousResults.forEach(function (doc) {
updatedResults = updatedResults.insert(doc.key, doc);
});
return updatedResults;
});
});
};
/** Applies the query filter and sorting to the provided documents. */
IndexFreeQueryEngine.prototype.applyQuery = function (query, documents) {
// Sort the documents and re-apply the query filter since previously
// matching documents do not necessarily still match the query.
var queryResults = new SortedSet(function (d1, d2) { return query.docComparator(d1, d2); });
documents.forEach(function (_, maybeDoc) {
if (maybeDoc instanceof Document && query.matches(maybeDoc)) {
queryResults = queryResults.add(maybeDoc);
}
});
return queryResults;
};
/**
* Determines if a limit query needs to be refilled from cache, making it
* ineligible for index-free execution.
*
* @param sortedPreviousResults The documents that matched the query when it
* was last synchronized, sorted by the query's comparator.
* @param remoteKeys The document keys that matched the query at the last
* snapshot.
* @param limboFreeSnapshotVersion The version of the snapshot when the query
* was last synchronized.
*/
IndexFreeQueryEngine.prototype.needsRefill = function (limitType, sortedPreviousResults, remoteKeys, limboFreeSnapshotVersion) {
// The query needs to be refilled if a previously matching document no
// longer matches.
if (remoteKeys.size !== sortedPreviousResults.size) {
return true;
}
// Limit queries are not eligible for index-free query execution if there is
// a potential that an older document from cache now sorts before a document
// that was previously part of the limit. This, however, can only happen if
// the document at the edge of the limit goes out of limit.
// If a document that is not the limit boundary sorts differently,
// the boundary of the limit itself did not change and documents from cache
// will continue to be "rejected" by this boundary. Therefore, we can ignore
// any modifications that don't affect the last document.
var docAtLimitEdge = limitType === "F" /* First */
? sortedPreviousResults.last()
: sortedPreviousResults.first();
if (!docAtLimitEdge) {
// We don't need to refill the query if there were already no documents.
return false;
}
return (docAtLimitEdge.hasPendingWrites ||
docAtLimitEdge.version.compareTo(limboFreeSnapshotVersion) > 0);
};
IndexFreeQueryEngine.prototype.executeFullCollectionScan = function (transaction, query) {
if (getLogLevel() <= logger.LogLevel.DEBUG) {
logDebug('IndexFreeQueryEngine', 'Using full collection scan to execute query:', query.toString());
}
return this.localDocumentsView.getDocumentsMatchingQuery(transaction, query, SnapshotVersion.min());
};
return IndexFreeQueryEngine;
}());
/**
* @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.
*/
var MemoryMutationQueue = /** @class */ (function () {
function MemoryMutationQueue(indexManager, referenceDelegate) {
this.indexManager = indexManager;
this.referenceDelegate = referenceDelegate;
/**
* The set of all mutations that have been sent but not yet been applied to
* the backend.
*/
this.mutationQueue = [];
/** Next value to use when assigning sequential IDs to each mutation batch. */
this.nextBatchId = 1;
/** The last received stream token from the server, used to acknowledge which
* responses the client has processed. Stream tokens are opaque checkpoint
* markers whose only real value is their inclusion in the next request.
*/
this.lastStreamToken = ByteString.EMPTY_BYTE_STRING;
/** An ordered mapping between documents and the mutations batch IDs. */
this.batchesByDocumentKey = new SortedSet(DocReference.compareByKey);
}
MemoryMutationQueue.prototype.checkEmpty = function (transaction) {
return PersistencePromise.resolve(this.mutationQueue.length === 0);
};
MemoryMutationQueue.prototype.acknowledgeBatch = function (transaction, batch, streamToken) {
var batchId = batch.batchId;
var batchIndex = this.indexOfExistingBatchId(batchId, 'acknowledged');
hardAssert(batchIndex === 0, 'Can only acknowledge the first batch in the mutation queue');
// Verify that the batch in the queue is the one to be acknowledged.
var check = this.mutationQueue[batchIndex];
debugAssert(batchId === check.batchId, 'Queue ordering failure: expected batch ' +
batchId +
', got batch ' +
check.batchId);
this.lastStreamToken = streamToken;
return PersistencePromise.resolve();
};
MemoryMutationQueue.prototype.getLastStreamToken = function (transaction) {
return PersistencePromise.resolve(this.lastStreamToken);
};
MemoryMutationQueue.prototype.setLastStreamToken = function (transaction, streamToken) {
this.lastStreamToken = streamToken;
return PersistencePromise.resolve();
};
MemoryMutationQueue.prototype.addMutationBatch = function (transaction, localWriteTime, baseMutations, mutations) {
debugAssert(mutations.length !== 0, 'Mutation batches should not be empty');
var batchId = this.nextBatchId;
this.nextBatchId++;
if (this.mutationQueue.length > 0) {
var prior = this.mutationQueue[this.mutationQueue.length - 1];
debugAssert(prior.batchId < batchId, 'Mutation batchIDs must be monotonically increasing order');
}
var batch = new MutationBatch(batchId, localWriteTime, baseMutations, mutations);
this.mutationQueue.push(batch);
// Track references by document key and index collection parents.
for (var _i = 0, mutations_3 = mutations; _i < mutations_3.length; _i++) {
var mutation = mutations_3[_i];
this.batchesByDocumentKey = this.batchesByDocumentKey.add(new DocReference(mutation.key, batchId));
this.indexManager.addToCollectionParentIndex(transaction, mutation.key.path.popLast());
}
return PersistencePromise.resolve(batch);
};
MemoryMutationQueue.prototype.lookupMutationBatch = function (transaction, batchId) {
return PersistencePromise.resolve(this.findMutationBatch(batchId));
};
MemoryMutationQueue.prototype.getNextMutationBatchAfterBatchId = function (transaction, batchId) {
var nextBatchId = batchId + 1;
// The requested batchId may still be out of range so normalize it to the
// start of the queue.
var rawIndex = this.indexOfBatchId(nextBatchId);
var index = rawIndex < 0 ? 0 : rawIndex;
return PersistencePromise.resolve(this.mutationQueue.length > index ? this.mutationQueue[index] : null);
};
MemoryMutationQueue.prototype.getHighestUnacknowledgedBatchId = function () {
return PersistencePromise.resolve(this.mutationQueue.length === 0 ? BATCHID_UNKNOWN : this.nextBatchId - 1);
};
MemoryMutationQueue.prototype.getAllMutationBatches = function (transaction) {
return PersistencePromise.resolve(this.mutationQueue.slice());
};
MemoryMutationQueue.prototype.getAllMutationBatchesAffectingDocumentKey = function (transaction, documentKey) {
var _this = this;
var start = new DocReference(documentKey, 0);
var end = new DocReference(documentKey, Number.POSITIVE_INFINITY);
var result = [];
this.batchesByDocumentKey.forEachInRange([start, end], function (ref) {
debugAssert(documentKey.isEqual(ref.key), "Should only iterate over a single key's batches");
var batch = _this.findMutationBatch(ref.targetOrBatchId);
debugAssert(batch !== null, 'Batches in the index must exist in the main table');
result.push(batch);
});
return PersistencePromise.resolve(result);
};
MemoryMutationQueue.prototype.getAllMutationBatchesAffectingDocumentKeys = function (transaction, documentKeys) {
var _this = this;
var uniqueBatchIDs = new SortedSet(primitiveComparator);
documentKeys.forEach(function (documentKey) {
var start = new DocReference(documentKey, 0);
var end = new DocReference(documentKey, Number.POSITIVE_INFINITY);
_this.batchesByDocumentKey.forEachInRange([start, end], function (ref) {
debugAssert(documentKey.isEqual(ref.key), "For each key, should only iterate over a single key's batches");
uniqueBatchIDs = uniqueBatchIDs.add(ref.targetOrBatchId);
});
});
return PersistencePromise.resolve(this.findMutationBatches(uniqueBatchIDs));
};
MemoryMutationQueue.prototype.getAllMutationBatchesAffectingQuery = function (transaction, query) {
debugAssert(!query.isCollectionGroupQuery(), 'CollectionGroup queries should be handled in LocalDocumentsView');
// Use the query path as a prefix for testing if a document matches the
// query.
var prefix = query.path;
var immediateChildrenPathLength = prefix.length + 1;
// Construct a document reference for actually scanning the index. Unlike
// the prefix the document key in this reference must have an even number of
// segments. The empty segment can be used a suffix of the query path
// because it precedes all other segments in an ordered traversal.
var startPath = prefix;
if (!DocumentKey.isDocumentKey(startPath)) {
startPath = startPath.child('');
}
var start = new DocReference(new DocumentKey(startPath), 0);
// Find unique batchIDs referenced by all documents potentially matching the
// query.
var uniqueBatchIDs = new SortedSet(primitiveComparator);
this.batchesByDocumentKey.forEachWhile(function (ref) {
var rowKeyPath = ref.key.path;
if (!prefix.isPrefixOf(rowKeyPath)) {
return false;
}
else {
// Rows with document keys more than one segment longer than the query
// path can't be matches. For example, a query on 'rooms' can't match
// the document /rooms/abc/messages/xyx.
// TODO(mcg): we'll need a different scanner when we implement
// ancestor queries.
if (rowKeyPath.length === immediateChildrenPathLength) {
uniqueBatchIDs = uniqueBatchIDs.add(ref.targetOrBatchId);
}
return true;
}
}, start);
return PersistencePromise.resolve(this.findMutationBatches(uniqueBatchIDs));
};
MemoryMutationQueue.prototype.findMutationBatches = function (batchIDs) {
var _this = this;
// Construct an array of matching batches, sorted by batchID to ensure that
// multiple mutations affecting the same document key are applied in order.
var result = [];
batchIDs.forEach(function (batchId) {
var batch = _this.findMutationBatch(batchId);
if (batch !== null) {
result.push(batch);
}
});
return result;
};
MemoryMutationQueue.prototype.removeMutationBatch = function (transaction, batch) {
var _this = this;
// Find the position of the first batch for removal.
var batchIndex = this.indexOfExistingBatchId(batch.batchId, 'removed');
hardAssert(batchIndex === 0, 'Can only remove the first entry of the mutation queue');
this.mutationQueue.shift();
var references = this.batchesByDocumentKey;
return PersistencePromise.forEach(batch.mutations, function (mutation) {
var ref = new DocReference(mutation.key, batch.batchId);
references = references.delete(ref);
return _this.referenceDelegate.markPotentiallyOrphaned(transaction, mutation.key);
}).next(function () {
_this.batchesByDocumentKey = references;
});
};
MemoryMutationQueue.prototype.removeCachedMutationKeys = function (batchId) {
// No-op since the memory mutation queue does not maintain a separate cache.
};
MemoryMutationQueue.prototype.containsKey = function (txn, key) {
var ref = new DocReference(key, 0);
var firstRef = this.batchesByDocumentKey.firstAfterOrEqual(ref);
return PersistencePromise.resolve(key.isEqual(firstRef && firstRef.key));
};
MemoryMutationQueue.prototype.performConsistencyCheck = function (txn) {
if (this.mutationQueue.length === 0) {
debugAssert(this.batchesByDocumentKey.isEmpty(), 'Document leak -- detected dangling mutation references when queue is empty.');
}
return PersistencePromise.resolve();
};
/**
* Finds the index of the given batchId in the mutation queue and asserts that
* the resulting index is within the bounds of the queue.
*
* @param batchId The batchId to search for
* @param action A description of what the caller is doing, phrased in passive
* form (e.g. "acknowledged" in a routine that acknowledges batches).
*/
MemoryMutationQueue.prototype.indexOfExistingBatchId = function (batchId, action) {
var index = this.indexOfBatchId(batchId);
debugAssert(index >= 0 && index < this.mutationQueue.length, 'Batches must exist to be ' + action);
return index;
};
/**
* Finds the index of the given batchId in the mutation queue. This operation
* is O(1).
*
* @return The computed index of the batch with the given batchId, based on
* the state of the queue. Note this index can be negative if the requested
* batchId has already been remvoed from the queue or past the end of the
* queue if the batchId is larger than the last added batch.
*/
MemoryMutationQueue.prototype.indexOfBatchId = function (batchId) {
if (this.mutationQueue.length === 0) {
// As an index this is past the end of the queue
return 0;
}
// Examine the front of the queue to figure out the difference between the
// batchId and indexes in the array. Note that since the queue is ordered
// by batchId, if the first batch has a larger batchId then the requested
// batchId doesn't exist in the queue.
var firstBatchId = this.mutationQueue[0].batchId;
return batchId - firstBatchId;
};
/**
* A version of lookupMutationBatch that doesn't return a promise, this makes
* other functions that uses this code easier to read and more efficent.
*/
MemoryMutationQueue.prototype.findMutationBatch = function (batchId) {
var index = this.indexOfBatchId(batchId);
if (index < 0 || index >= this.mutationQueue.length) {
return null;
}
var batch = this.mutationQueue[index];
debugAssert(batch.batchId === batchId, 'If found batch must match');
return batch;
};
return MemoryMutationQueue;
}());
/**
* @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 documentEntryMap() {
return new SortedMap(DocumentKey.comparator);
}
var MemoryRemoteDocumentCache = /** @class */ (function () {
/**
* @param sizer Used to assess the size of a document. For eager GC, this is expected to just
* return 0 to avoid unnecessarily doing the work of calculating the size.
*/
function MemoryRemoteDocumentCache(indexManager, sizer) {
this.indexManager = indexManager;
this.sizer = sizer;
/** Underlying cache of documents and their read times. */
this.docs = documentEntryMap();
/** Size of all cached documents. */
this.size = 0;
}
/**
* Adds the supplied entry to the cache and updates the cache size as appropriate.
*
* All calls of `addEntry` are required to go through the RemoteDocumentChangeBuffer
* returned by `newChangeBuffer()`.
*/
MemoryRemoteDocumentCache.prototype.addEntry = function (transaction, doc, readTime) {
debugAssert(!readTime.isEqual(SnapshotVersion.min()), 'Cannot add a document with a read time of zero');
var key = doc.key;
var entry = this.docs.get(key);
var previousSize = entry ? entry.size : 0;
var currentSize = this.sizer(doc);
this.docs = this.docs.insert(key, {
maybeDocument: doc,
size: currentSize,
readTime: readTime
});
this.size += currentSize - previousSize;
return this.indexManager.addToCollectionParentIndex(transaction, key.path.popLast());
};
/**
* Removes the specified entry from the cache and updates the cache size as appropriate.
*
* All calls of `removeEntry` are required to go through the RemoteDocumentChangeBuffer
* returned by `newChangeBuffer()`.
*/
MemoryRemoteDocumentCache.prototype.removeEntry = function (documentKey) {
var entry = this.docs.get(documentKey);
if (entry) {
this.docs = this.docs.remove(documentKey);
this.size -= entry.size;
}
};
MemoryRemoteDocumentCache.prototype.getEntry = function (transaction, documentKey) {
var entry = this.docs.get(documentKey);
return PersistencePromise.resolve(entry ? entry.maybeDocument : null);
};
MemoryRemoteDocumentCache.prototype.getEntries = function (transaction, documentKeys) {
var _this = this;
var results = nullableMaybeDocumentMap();
documentKeys.forEach(function (documentKey) {
var entry = _this.docs.get(documentKey);
results = results.insert(documentKey, entry ? entry.maybeDocument : null);
});
return PersistencePromise.resolve(results);
};
MemoryRemoteDocumentCache.prototype.getDocumentsMatchingQuery = function (transaction, query, sinceReadTime) {
debugAssert(!query.isCollectionGroupQuery(), 'CollectionGroup queries should be handled in LocalDocumentsView');
var results = documentMap();
// Documents are ordered by key, so we can use a prefix scan to narrow down
// the documents we need to match the query against.
var prefix = new DocumentKey(query.path.child(''));
var iterator = this.docs.getIteratorFrom(prefix);
while (iterator.hasNext()) {
var _e = iterator.getNext(), key = _e.key, _f = _e.value, maybeDocument = _f.maybeDocument, readTime = _f.readTime;
if (!query.path.isPrefixOf(key.path)) {
break;
}
if (readTime.compareTo(sinceReadTime) <= 0) {
continue;
}
if (maybeDocument instanceof Document && query.matches(maybeDocument)) {
results = results.insert(maybeDocument.key, maybeDocument);
}
}
return PersistencePromise.resolve(results);
};
MemoryRemoteDocumentCache.prototype.forEachDocumentKey = function (transaction, f) {
return PersistencePromise.forEach(this.docs, function (key) { return f(key); });
};
MemoryRemoteDocumentCache.prototype.newChangeBuffer = function (options) {
// `trackRemovals` is ignores since the MemoryRemoteDocumentCache keeps
// a separate changelog and does not need special handling for removals.
return new MemoryRemoteDocumentCache.RemoteDocumentChangeBuffer(this);
};
MemoryRemoteDocumentCache.prototype.getSize = function (txn) {
return PersistencePromise.resolve(this.size);
};
return MemoryRemoteDocumentCache;
}());
/**
* Handles the details of adding and updating documents in the MemoryRemoteDocumentCache.
*/
MemoryRemoteDocumentCache.RemoteDocumentChangeBuffer = /** @class */ (function (_super) {
tslib.__extends(RemoteDocumentChangeBuffer, _super);
function RemoteDocumentChangeBuffer(documentCache) {
var _this = _super.call(this) || this;
_this.documentCache = documentCache;
return _this;
}
RemoteDocumentChangeBuffer.prototype.applyChanges = function (transaction) {
var _this = this;
var promises = [];
this.changes.forEach(function (key, doc) {
if (doc) {
promises.push(_this.documentCache.addEntry(transaction, doc, _this.readTime));
}
else {
_this.documentCache.removeEntry(key);
}
});
return PersistencePromise.waitFor(promises);
};
RemoteDocumentChangeBuffer.prototype.getFromCache = function (transaction, documentKey) {
return this.documentCache.getEntry(transaction, documentKey);
};
RemoteDocumentChangeBuffer.prototype.getAllFromCache = function (transaction, documentKeys) {
return this.documentCache.getEntries(transaction, documentKeys);
};
return RemoteDocumentChangeBuffer;
}(RemoteDocumentChangeBuffer));
/**
* @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.
*/
var MemoryTargetCache = /** @class */ (function () {
function MemoryTargetCache(persistence) {
this.persistence = persistence;
/**
* Maps a target to the data about that target
*/
this.targets = new ObjectMap(function (t) { return t.canonicalId(); });
/** The last received snapshot version. */
this.lastRemoteSnapshotVersion = SnapshotVersion.min();
/** The highest numbered target ID encountered. */
this.highestTargetId = 0;
/** The highest sequence number encountered. */
this.highestSequenceNumber = 0;
/**
* A ordered bidirectional mapping between documents and the remote target
* IDs.
*/
this.references = new ReferenceSet();
this.targetCount = 0;
this.targetIdGenerator = TargetIdGenerator.forTargetCache();
}
MemoryTargetCache.prototype.forEachTarget = function (txn, f) {
this.targets.forEach(function (_, targetData) { return f(targetData); });
return PersistencePromise.resolve();
};
MemoryTargetCache.prototype.getLastRemoteSnapshotVersion = function (transaction) {
return PersistencePromise.resolve(this.lastRemoteSnapshotVersion);
};
MemoryTargetCache.prototype.getHighestSequenceNumber = function (transaction) {
return PersistencePromise.resolve(this.highestSequenceNumber);
};
MemoryTargetCache.prototype.allocateTargetId = function (transaction) {
this.highestTargetId = this.targetIdGenerator.next();
return PersistencePromise.resolve(this.highestTargetId);
};
MemoryTargetCache.prototype.setTargetsMetadata = function (transaction, highestListenSequenceNumber, lastRemoteSnapshotVersion) {
if (lastRemoteSnapshotVersion) {
this.lastRemoteSnapshotVersion = lastRemoteSnapshotVersion;
}
if (highestListenSequenceNumber > this.highestSequenceNumber) {
this.highestSequenceNumber = highestListenSequenceNumber;
}
return PersistencePromise.resolve();
};
MemoryTargetCache.prototype.saveTargetData = function (targetData) {
this.targets.set(targetData.target, targetData);
var targetId = targetData.targetId;
if (targetId > this.highestTargetId) {
this.targetIdGenerator = new TargetIdGenerator(targetId);
this.highestTargetId = targetId;
}
if (targetData.sequenceNumber > this.highestSequenceNumber) {
this.highestSequenceNumber = targetData.sequenceNumber;
}
};
MemoryTargetCache.prototype.addTargetData = function (transaction, targetData) {
debugAssert(!this.targets.has(targetData.target), 'Adding a target that already exists');
this.saveTargetData(targetData);
this.targetCount += 1;
return PersistencePromise.resolve();
};
MemoryTargetCache.prototype.updateTargetData = function (transaction, targetData) {
debugAssert(this.targets.has(targetData.target), 'Updating a non-existent target');
this.saveTargetData(targetData);
return PersistencePromise.resolve();
};
MemoryTargetCache.prototype.removeTargetData = function (transaction, targetData) {
debugAssert(this.targetCount > 0, 'Removing a target from an empty cache');
debugAssert(this.targets.has(targetData.target), 'Removing a non-existent target from the cache');
this.targets.delete(targetData.target);
this.references.removeReferencesForId(targetData.targetId);
this.targetCount -= 1;
return PersistencePromise.resolve();
};
MemoryTargetCache.prototype.removeTargets = function (transaction, upperBound, activeTargetIds) {
var _this = this;
var count = 0;
var removals = [];
this.targets.forEach(function (key, targetData) {
if (targetData.sequenceNumber <= upperBound &&
activeTargetIds.get(targetData.targetId) === null) {
_this.targets.delete(key);
removals.push(_this.removeMatchingKeysForTargetId(transaction, targetData.targetId));
count++;
}
});
return PersistencePromise.waitFor(removals).next(function () { return count; });
};
MemoryTargetCache.prototype.getTargetCount = function (transaction) {
return PersistencePromise.resolve(this.targetCount);
};
MemoryTargetCache.prototype.getTargetData = function (transaction, target) {
var targetData = this.targets.get(target) || null;
return PersistencePromise.resolve(targetData);
};
MemoryTargetCache.prototype.addMatchingKeys = function (txn, keys, targetId) {
this.references.addReferences(keys, targetId);
return PersistencePromise.resolve();
};
MemoryTargetCache.prototype.removeMatchingKeys = function (txn, keys, targetId) {
this.references.removeReferences(keys, targetId);
var referenceDelegate = this.persistence.referenceDelegate;
var promises = [];
if (referenceDelegate) {
keys.forEach(function (key) {
promises.push(referenceDelegate.markPotentiallyOrphaned(txn, key));
});
}
return PersistencePromise.waitFor(promises);
};
MemoryTargetCache.prototype.removeMatchingKeysForTargetId = function (txn, targetId) {
this.references.removeReferencesForId(targetId);
return PersistencePromise.resolve();
};
MemoryTargetCache.prototype.getMatchingKeysForTargetId = function (txn, targetId) {
var matchingKeys = this.references.referencesForId(targetId);
return PersistencePromise.resolve(matchingKeys);
};
MemoryTargetCache.prototype.containsKey = function (txn, key) {
return PersistencePromise.resolve(this.references.containsKey(key));
};
return MemoryTargetCache;
}());
/**
* @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.
*/
var LOG_TAG$c = 'MemoryPersistence';
/**
* A memory-backed instance of Persistence. Data is stored only in RAM and
* not persisted across sessions.
*/
var MemoryPersistence = /** @class */ (function () {
/**
* The constructor accepts a factory for creating a reference delegate. This
* allows both the delegate and this instance to have strong references to
* each other without having nullable fields that would then need to be
* checked or asserted on every access.
*/
function MemoryPersistence(referenceDelegateFactory) {
var _this = this;
this.mutationQueues = {};
this.listenSequence = new ListenSequence(0);
this._started = false;
this._started = true;
this.referenceDelegate = referenceDelegateFactory(this);
this.targetCache = new MemoryTargetCache(this);
var sizer = function (doc) { return _this.referenceDelegate.documentSize(doc); };
this.indexManager = new MemoryIndexManager();
this.remoteDocumentCache = new MemoryRemoteDocumentCache(this.indexManager, sizer);
}
MemoryPersistence.prototype.start = function () {
return Promise.resolve();
};
MemoryPersistence.prototype.shutdown = function () {
// No durable state to ensure is closed on shutdown.
this._started = false;
return Promise.resolve();
};
Object.defineProperty(MemoryPersistence.prototype, "started", {
get: function () {
return this._started;
},
enumerable: true,
configurable: true
});
MemoryPersistence.prototype.setDatabaseDeletedListener = function () {
// No op.
};
MemoryPersistence.prototype.getIndexManager = function () {
return this.indexManager;
};
MemoryPersistence.prototype.getMutationQueue = function (user) {
var queue = this.mutationQueues[user.toKey()];
if (!queue) {
queue = new MemoryMutationQueue(this.indexManager, this.referenceDelegate);
this.mutationQueues[user.toKey()] = queue;
}
return queue;
};
MemoryPersistence.prototype.getTargetCache = function () {
return this.targetCache;
};
MemoryPersistence.prototype.getRemoteDocumentCache = function () {
return this.remoteDocumentCache;
};
MemoryPersistence.prototype.runTransaction = function (action, mode, transactionOperation) {
var _this = this;
logDebug(LOG_TAG$c, 'Starting transaction:', action);
var txn = new MemoryTransaction(this.listenSequence.next());
this.referenceDelegate.onTransactionStarted();
return transactionOperation(txn)
.next(function (result) {
return _this.referenceDelegate
.onTransactionCommitted(txn)
.next(function () { return result; });
})
.toPromise()
.then(function (result) {
txn.raiseOnCommittedEvent();
return result;
});
};
MemoryPersistence.prototype.mutationQueuesContainKey = function (transaction, key) {
return PersistencePromise.or(Object.values(this.mutationQueues).map(function (queue) { return function () { return queue.containsKey(transaction, key); }; }));
};
return MemoryPersistence;
}());
/**
* Memory persistence is not actually transactional, but future implementations
* may have transaction-scoped state.
*/
var MemoryTransaction = /** @class */ (function (_super) {
tslib.__extends(MemoryTransaction, _super);
function MemoryTransaction(currentSequenceNumber) {
var _this = _super.call(this) || this;
_this.currentSequenceNumber = currentSequenceNumber;
return _this;
}
return MemoryTransaction;
}(PersistenceTransaction));
var MemoryEagerDelegate = /** @class */ (function () {
function MemoryEagerDelegate(persistence) {
this.persistence = persistence;
/** Tracks all documents that are active in Query views. */
this.localViewReferences = new ReferenceSet();
/** The list of documents that are potentially GCed after each transaction. */
this._orphanedDocuments = null;
}
MemoryEagerDelegate.factory = function (persistence) {
return new MemoryEagerDelegate(persistence);
};
Object.defineProperty(MemoryEagerDelegate.prototype, "orphanedDocuments", {
get: function () {
if (!this._orphanedDocuments) {
throw fail('orphanedDocuments is only valid during a transaction.');
}
else {
return this._orphanedDocuments;
}
},
enumerable: true,
configurable: true
});
MemoryEagerDelegate.prototype.addReference = function (txn, targetId, key) {
this.localViewReferences.addReference(key, targetId);
this.orphanedDocuments.delete(key);
return PersistencePromise.resolve();
};
MemoryEagerDelegate.prototype.removeReference = function (txn, targetId, key) {
this.localViewReferences.removeReference(key, targetId);
this.orphanedDocuments.add(key);
return PersistencePromise.resolve();
};
MemoryEagerDelegate.prototype.markPotentiallyOrphaned = function (txn, key) {
this.orphanedDocuments.add(key);
return PersistencePromise.resolve();
};
MemoryEagerDelegate.prototype.removeTarget = function (txn, targetData) {
var _this = this;
var orphaned = this.localViewReferences.removeReferencesForId(targetData.targetId);
orphaned.forEach(function (key) { return _this.orphanedDocuments.add(key); });
var cache = this.persistence.getTargetCache();
return cache
.getMatchingKeysForTargetId(txn, targetData.targetId)
.next(function (keys) {
keys.forEach(function (key) { return _this.orphanedDocuments.add(key); });
})
.next(function () { return cache.removeTargetData(txn, targetData); });
};
MemoryEagerDelegate.prototype.onTransactionStarted = function () {
this._orphanedDocuments = new Set();
};
MemoryEagerDelegate.prototype.onTransactionCommitted = function (txn) {
var _this = this;
// Remove newly orphaned documents.
var cache = this.persistence.getRemoteDocumentCache();
var changeBuffer = cache.newChangeBuffer();
return PersistencePromise.forEach(this.orphanedDocuments, function (key) {
return _this.isReferenced(txn, key).next(function (isReferenced) {
if (!isReferenced) {
changeBuffer.removeEntry(key);
}
});
}).next(function () {
_this._orphanedDocuments = null;
return changeBuffer.apply(txn);
});
};
MemoryEagerDelegate.prototype.updateLimboDocument = function (txn, key) {
var _this = this;
return this.isReferenced(txn, key).next(function (isReferenced) {
if (isReferenced) {
_this.orphanedDocuments.delete(key);
}
else {
_this.orphanedDocuments.add(key);
}
});
};
MemoryEagerDelegate.prototype.documentSize = function (doc) {
// For eager GC, we don't care about the document size, there are no size thresholds.
return 0;
};
MemoryEagerDelegate.prototype.isReferenced = function (txn, key) {
var _this = this;
return PersistencePromise.or([
function () { return PersistencePromise.resolve(_this.localViewReferences.containsKey(key)); },
function () { return _this.persistence.getTargetCache().containsKey(txn, key); },
function () { return _this.persistence.mutationQueuesContainKey(txn, key); }
]);
};
return MemoryEagerDelegate;
}());
/**
* @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.
*/
var MEMORY_ONLY_PERSISTENCE_ERROR_MESSAGE = 'You are using the memory-only build of Firestore. Persistence support is ' +
'only available via the @firebase/firestore bundle or the ' +
'firebase-firestore.js build.';
/**
* Provides all components needed for Firestore with in-memory persistence.
* Uses EagerGC garbage collection.
*/
var MemoryComponentProvider = /** @class */ (function () {
function MemoryComponentProvider() {
}
MemoryComponentProvider.prototype.initialize = function (cfg) {
return tslib.__awaiter(this, void 0, void 0, function () {
var _this = this;
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
this.sharedClientState = this.createSharedClientState(cfg);
this.persistence = this.createPersistence(cfg);
return [4 /*yield*/, this.persistence.start()];
case 1:
_e.sent();
this.gcScheduler = this.createGarbageCollectionScheduler(cfg);
this.localStore = this.createLocalStore(cfg);
this.remoteStore = this.createRemoteStore(cfg);
this.syncEngine = this.createSyncEngine(cfg);
this.eventManager = this.createEventManager(cfg);
this.sharedClientState.onlineStateHandler = function (onlineState) { return _this.syncEngine.applyOnlineStateChange(onlineState, 1 /* SharedClientState */); };
this.remoteStore.syncEngine = this.syncEngine;
return [4 /*yield*/, this.localStore.start()];
case 2:
_e.sent();
return [4 /*yield*/, this.sharedClientState.start()];
case 3:
_e.sent();
return [4 /*yield*/, this.remoteStore.start()];
case 4:
_e.sent();
return [4 /*yield*/, this.remoteStore.applyPrimaryState(this.syncEngine.isPrimaryClient)];
case 5:
_e.sent();
return [2 /*return*/];
}
});
});
};
MemoryComponentProvider.prototype.createEventManager = function (cfg) {
return new EventManager(this.syncEngine);
};
MemoryComponentProvider.prototype.createGarbageCollectionScheduler = function (cfg) {
return null;
};
MemoryComponentProvider.prototype.createLocalStore = function (cfg) {
return new LocalStore(this.persistence, new IndexFreeQueryEngine(), cfg.initialUser);
};
MemoryComponentProvider.prototype.createPersistence = function (cfg) {
debugAssert(!cfg.persistenceSettings.durable, 'Can only start memory persistence');
return new MemoryPersistence(MemoryEagerDelegate.factory);
};
MemoryComponentProvider.prototype.createRemoteStore = function (cfg) {
var _this = this;
return new RemoteStore(this.localStore, cfg.datastore, cfg.asyncQueue, function (onlineState) { return _this.syncEngine.applyOnlineStateChange(onlineState, 0 /* RemoteStore */); }, cfg.platform.newConnectivityMonitor());
};
MemoryComponentProvider.prototype.createSharedClientState = function (cfg) {
return new MemorySharedClientState();
};
MemoryComponentProvider.prototype.createSyncEngine = function (cfg) {
return new SyncEngine(this.localStore, this.remoteStore, this.sharedClientState, cfg.initialUser, cfg.maxConcurrentLimboResolutions);
};
MemoryComponentProvider.prototype.clearPersistence = function (databaseInfo) {
throw new FirestoreError(Code.FAILED_PRECONDITION, MEMORY_ONLY_PERSISTENCE_ERROR_MESSAGE);
};
return MemoryComponentProvider;
}());
/**
* Provides all components needed for Firestore with IndexedDB persistence.
*/
var IndexedDbComponentProvider = /** @class */ (function (_super) {
tslib.__extends(IndexedDbComponentProvider, _super);
function IndexedDbComponentProvider() {
return _super !== null && _super.apply(this, arguments) || this;
}
IndexedDbComponentProvider.prototype.initialize = function (cfg) {
return tslib.__awaiter(this, void 0, void 0, function () {
var _this = this;
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0: return [4 /*yield*/, _super.prototype.initialize.call(this, cfg)];
case 1:
_e.sent();
// NOTE: This will immediately call the listener, so we make sure to
// set it after localStore / remoteStore are started.
return [4 /*yield*/, this.persistence.setPrimaryStateListener(function (isPrimary) { return tslib.__awaiter(_this, void 0, void 0, function () {
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0: return [4 /*yield*/, this.syncEngine.applyPrimaryState(isPrimary)];
case 1:
_e.sent();
if (this.gcScheduler) {
if (isPrimary && !this.gcScheduler.started) {
this.gcScheduler.start(this.localStore);
}
else if (!isPrimary) {
this.gcScheduler.stop();
}
}
return [2 /*return*/];
}
});
}); })];
case 2:
// NOTE: This will immediately call the listener, so we make sure to
// set it after localStore / remoteStore are started.
_e.sent();
return [2 /*return*/];
}
});
});
};
IndexedDbComponentProvider.prototype.createLocalStore = function (cfg) {
return new MultiTabLocalStore(this.persistence, new IndexFreeQueryEngine(), cfg.initialUser);
};
IndexedDbComponentProvider.prototype.createSyncEngine = function (cfg) {
var syncEngine = new MultiTabSyncEngine(this.localStore, this.remoteStore, this.sharedClientState, cfg.initialUser, cfg.maxConcurrentLimboResolutions);
if (this.sharedClientState instanceof WebStorageSharedClientState) {
this.sharedClientState.syncEngine = syncEngine;
}
return syncEngine;
};
IndexedDbComponentProvider.prototype.createGarbageCollectionScheduler = function (cfg) {
var garbageCollector = this.persistence.referenceDelegate
.garbageCollector;
return new LruScheduler(garbageCollector, cfg.asyncQueue);
};
IndexedDbComponentProvider.prototype.createPersistence = function (cfg) {
debugAssert(cfg.persistenceSettings.durable, 'Can only start durable persistence');
var persistenceKey = IndexedDbPersistence.buildStoragePrefix(cfg.databaseInfo);
var serializer = cfg.platform.newSerializer(cfg.databaseInfo.databaseId);
return new IndexedDbPersistence(cfg.persistenceSettings.synchronizeTabs, persistenceKey, cfg.clientId, cfg.platform, LruParams.withCacheSize(cfg.persistenceSettings.cacheSizeBytes), cfg.asyncQueue, serializer, this.sharedClientState);
};
IndexedDbComponentProvider.prototype.createSharedClientState = function (cfg) {
if (cfg.persistenceSettings.durable &&
cfg.persistenceSettings.synchronizeTabs) {
if (!WebStorageSharedClientState.isAvailable(cfg.platform)) {
throw new FirestoreError(Code.UNIMPLEMENTED, 'IndexedDB persistence is only available on platforms that support LocalStorage.');
}
var persistenceKey = IndexedDbPersistence.buildStoragePrefix(cfg.databaseInfo);
return new WebStorageSharedClientState(cfg.asyncQueue, cfg.platform, persistenceKey, cfg.clientId, cfg.initialUser);
}
return new MemorySharedClientState();
};
IndexedDbComponentProvider.prototype.clearPersistence = function (databaseInfo) {
var persistenceKey = IndexedDbPersistence.buildStoragePrefix(databaseInfo);
return IndexedDbPersistence.clearPersistence(persistenceKey);
};
return IndexedDbComponentProvider;
}(MemoryComponentProvider));
/**
* @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.
*/
var LOG_TAG$d = 'FirestoreClient';
var MAX_CONCURRENT_LIMBO_RESOLUTIONS = 100;
/** DOMException error code constants. */
var DOM_EXCEPTION_INVALID_STATE = 11;
var DOM_EXCEPTION_ABORTED = 20;
var DOM_EXCEPTION_QUOTA_EXCEEDED = 22;
/**
* FirestoreClient is a top-level class that constructs and owns all of the
* pieces of the client SDK architecture. It is responsible for creating the
* async queue that is shared by all of the other components in the system.
*/
var FirestoreClient = /** @class */ (function () {
function FirestoreClient(platform, databaseInfo, credentials,
/**
* Asynchronous queue responsible for all of our internal processing. When
* we get incoming work from the user (via public API) or the network
* (incoming GRPC messages), we should always schedule onto this queue.
* This ensures all of our work is properly serialized (e.g. we don't
* start processing a new operation while the previous one is waiting for
* an async I/O to complete).
*/
asyncQueue) {
this.platform = platform;
this.databaseInfo = databaseInfo;
this.credentials = credentials;
this.asyncQueue = asyncQueue;
this.clientId = AutoId.newId();
}
/**
* Starts up the FirestoreClient, returning only whether or not enabling
* persistence succeeded.
*
* The intent here is to "do the right thing" as far as users are concerned.
* Namely, in cases where offline persistence is requested and possible,
* enable it, but otherwise fall back to persistence disabled. For the most
* part we expect this to succeed one way or the other so we don't expect our
* users to actually wait on the firestore.enablePersistence Promise since
* they generally won't care.
*
* Of course some users actually do care about whether or not persistence
* was successfully enabled, so the Promise returned from this method
* indicates this outcome.
*
* This presents a problem though: even before enablePersistence resolves or
* rejects, users may have made calls to e.g. firestore.collection() which
* means that the FirestoreClient in there will be available and will be
* enqueuing actions on the async queue.
*
* Meanwhile any failure of an operation on the async queue causes it to
* panic and reject any further work, on the premise that unhandled errors
* are fatal.
*
* Consequently the fallback is handled internally here in start, and if the
* fallback succeeds we signal success to the async queue even though the
* start() itself signals failure.
*
* @param componentProvider Provider that returns all core components.
* @param persistenceSettings Settings object to configure offline
* persistence.
* @returns A deferred result indicating the user-visible result of enabling
* offline persistence. This method will reject this if IndexedDB fails to
* start for any reason. If usePersistence is false this is
* unconditionally resolved.
*/
FirestoreClient.prototype.start = function (componentProvider, persistenceSettings) {
var _this = this;
this.verifyNotTerminated();
// We defer our initialization until we get the current user from
// setChangeListener(). We block the async queue until we got the initial
// user and the initialization is completed. This will prevent any scheduled
// work from happening before initialization is completed.
//
// If initializationDone resolved then the FirestoreClient is in a usable
// state.
var initializationDone = new Deferred();
// If usePersistence is true, certain classes of errors while starting are
// recoverable but only by falling back to persistence disabled.
//
// If there's an error in the first case but not in recovery we cannot
// reject the promise blocking the async queue because this will cause the
// async queue to panic.
var persistenceResult = new Deferred();
var initialized = false;
this.credentials.setChangeListener(function (user) {
if (!initialized) {
initialized = true;
logDebug(LOG_TAG$d, 'Initializing. user=', user.uid);
return _this.initializeComponents(componentProvider, persistenceSettings, user, persistenceResult).then(initializationDone.resolve, initializationDone.reject);
}
else {
_this.asyncQueue.enqueueRetryable(function () {
return _this.handleCredentialChange(user);
});
}
});
// Block the async queue until initialization is done
this.asyncQueue.enqueueAndForget(function () {
return initializationDone.promise;
});
// Return only the result of enabling persistence. Note that this does not
// need to await the completion of initializationDone because the result of
// this method should not reflect any other kind of failure to start.
return persistenceResult.promise;
};
/** Enables the network connection and requeues all pending operations. */
FirestoreClient.prototype.enableNetwork = function () {
var _this = this;
this.verifyNotTerminated();
return this.asyncQueue.enqueue(function () {
return _this.syncEngine.enableNetwork();
});
};
/**
* Initializes persistent storage, attempting to use IndexedDB if
* usePersistence is true or memory-only if false.
*
* If IndexedDB fails because it's already open in another tab or because the
* platform can't possibly support our implementation then this method rejects
* the persistenceResult and falls back on memory-only persistence.
*
* @param componentProvider The provider that provides all core componennts
* for IndexedDB or memory-backed persistence
* @param persistenceSettings Settings object to configure offline persistence
* @param user The initial user
* @param persistenceResult A deferred result indicating the user-visible
* result of enabling offline persistence. This method will reject this if
* IndexedDB fails to start for any reason. If usePersistence is false
* this is unconditionally resolved.
* @returns a Promise indicating whether or not initialization should
* continue, i.e. that one of the persistence implementations actually
* succeeded.
*/
FirestoreClient.prototype.initializeComponents = function (componentProvider, persistenceSettings, user, persistenceResult) {
return tslib.__awaiter(this, void 0, void 0, function () {
var connection, serializer, datastore, error_5;
var _this = this;
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
_e.trys.push([0, 3, , 4]);
return [4 /*yield*/, this.platform.loadConnection(this.databaseInfo)];
case 1:
connection = _e.sent();
serializer = this.platform.newSerializer(this.databaseInfo.databaseId);
datastore = newDatastore(connection, this.credentials, serializer);
return [4 /*yield*/, componentProvider.initialize({
asyncQueue: this.asyncQueue,
databaseInfo: this.databaseInfo,
platform: this.platform,
datastore: datastore,
clientId: this.clientId,
initialUser: user,
maxConcurrentLimboResolutions: MAX_CONCURRENT_LIMBO_RESOLUTIONS,
persistenceSettings: persistenceSettings
})];
case 2:
_e.sent();
this.persistence = componentProvider.persistence;
this.sharedClientState = componentProvider.sharedClientState;
this.localStore = componentProvider.localStore;
this.remoteStore = componentProvider.remoteStore;
this.syncEngine = componentProvider.syncEngine;
this.gcScheduler = componentProvider.gcScheduler;
this.eventMgr = componentProvider.eventManager;
// When a user calls clearPersistence() in one client, all other clients
// need to be terminated to allow the delete to succeed.
this.persistence.setDatabaseDeletedListener(function () { return tslib.__awaiter(_this, void 0, void 0, function () {
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0: return [4 /*yield*/, this.terminate()];
case 1:
_e.sent();
return [2 /*return*/];
}
});
}); });
persistenceResult.resolve();
return [3 /*break*/, 4];
case 3:
error_5 = _e.sent();
// Regardless of whether or not the retry succeeds, from an user
// perspective, offline persistence has failed.
persistenceResult.reject(error_5);
// An unknown failure on the first stage shuts everything down.
if (!this.canFallback(error_5)) {
throw error_5;
}
console.warn('Error enabling offline persistence. Falling back to' +
' persistence disabled: ' +
error_5);
return [2 /*return*/, this.initializeComponents(new MemoryComponentProvider(), { durable: false }, user, persistenceResult)];
case 4: return [2 /*return*/];
}
});
});
};
/**
* Decides whether the provided error allows us to gracefully disable
* persistence (as opposed to crashing the client).
*/
FirestoreClient.prototype.canFallback = function (error) {
if (error.name === 'FirebaseError') {
return (error.code === Code.FAILED_PRECONDITION ||
error.code === Code.UNIMPLEMENTED);
}
else if (typeof DOMException !== 'undefined' &&
error instanceof DOMException) {
// There are a few known circumstances where we can open IndexedDb but
// trying to read/write will fail (e.g. quota exceeded). For
// well-understood cases, we attempt to detect these and then gracefully
// fall back to memory persistence.
// NOTE: Rather than continue to add to this list, we could decide to
// always fall back, with the risk that we might accidentally hide errors
// representing actual SDK bugs.
return (
// When the browser is out of quota we could get either quota exceeded
// or an aborted error depending on whether the error happened during
// schema migration.
error.code === DOM_EXCEPTION_QUOTA_EXCEEDED ||
error.code === DOM_EXCEPTION_ABORTED ||
// Firefox Private Browsing mode disables IndexedDb and returns
// INVALID_STATE for any usage.
error.code === DOM_EXCEPTION_INVALID_STATE);
}
return true;
};
/**
* Checks that the client has not been terminated. Ensures that other methods on
* this class cannot be called after the client is terminated.
*/
FirestoreClient.prototype.verifyNotTerminated = function () {
if (this.asyncQueue.isShuttingDown) {
throw new FirestoreError(Code.FAILED_PRECONDITION, 'The client has already been terminated.');
}
};
FirestoreClient.prototype.handleCredentialChange = function (user) {
this.asyncQueue.verifyOperationInProgress();
logDebug(LOG_TAG$d, 'Credential Changed. Current user: ' + user.uid);
return this.syncEngine.handleCredentialChange(user);
};
/** Disables the network connection. Pending operations will not complete. */
FirestoreClient.prototype.disableNetwork = function () {
var _this = this;
this.verifyNotTerminated();
return this.asyncQueue.enqueue(function () {
return _this.syncEngine.disableNetwork();
});
};
FirestoreClient.prototype.terminate = function () {
var _this = this;
return this.asyncQueue.enqueueAndInitiateShutdown(function () { return tslib.__awaiter(_this, void 0, void 0, function () {
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
// PORTING NOTE: LocalStore does not need an explicit shutdown on web.
if (this.gcScheduler) {
this.gcScheduler.stop();
}
return [4 /*yield*/, this.remoteStore.shutdown()];
case 1:
_e.sent();
return [4 /*yield*/, this.sharedClientState.shutdown()];
case 2:
_e.sent();
return [4 /*yield*/, this.persistence.shutdown()];
case 3:
_e.sent();
// `removeChangeListener` must be called after shutting down the
// RemoteStore as it will prevent the RemoteStore from retrieving
// auth tokens.
this.credentials.removeChangeListener();
return [2 /*return*/];
}
});
}); });
};
/**
* Returns a Promise that resolves when all writes that were pending at the time this
* method was called received server acknowledgement. An acknowledgement can be either acceptance
* or rejection.
*/
FirestoreClient.prototype.waitForPendingWrites = function () {
var _this = this;
this.verifyNotTerminated();
var deferred = new Deferred();
this.asyncQueue.enqueueAndForget(function () {
return _this.syncEngine.registerPendingWritesCallback(deferred);
});
return deferred.promise;
};
FirestoreClient.prototype.listen = function (query, observer, options) {
var _this = this;
this.verifyNotTerminated();
var listener = new QueryListener(query, observer, options);
this.asyncQueue.enqueueAndForget(function () { return _this.eventMgr.listen(listener); });
return listener;
};
FirestoreClient.prototype.unlisten = function (listener) {
var _this = this;
// Checks for termination but does not raise error, allowing unlisten after
// termination to be a no-op.
if (this.clientTerminated) {
return;
}
this.asyncQueue.enqueueAndForget(function () {
return _this.eventMgr.unlisten(listener);
});
};
FirestoreClient.prototype.getDocumentFromLocalCache = function (docKey) {
return tslib.__awaiter(this, void 0, void 0, function () {
var deferred;
var _this = this;
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
this.verifyNotTerminated();
deferred = new Deferred();
return [4 /*yield*/, this.asyncQueue.enqueue(function () { return tslib.__awaiter(_this, void 0, void 0, function () {
var maybeDoc, e_9, firestoreError;
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
_e.trys.push([0, 2, , 3]);
return [4 /*yield*/, this.localStore.readDocument(docKey)];
case 1:
maybeDoc = _e.sent();
if (maybeDoc instanceof Document) {
deferred.resolve(maybeDoc);
}
else if (maybeDoc instanceof NoDocument) {
deferred.resolve(null);
}
else {
deferred.reject(new FirestoreError(Code.UNAVAILABLE, 'Failed to get document from cache. (However, this document may ' +
"exist on the server. Run again without setting 'source' in " +
'the GetOptions to attempt to retrieve the document from the ' +
'server.)'));
}
return [3 /*break*/, 3];
case 2:
e_9 = _e.sent();
firestoreError = wrapInUserErrorIfRecoverable(e_9, "Failed to get document '" + docKey + " from cache");
deferred.reject(firestoreError);
return [3 /*break*/, 3];
case 3: return [2 /*return*/];
}
});
}); })];
case 1:
_e.sent();
return [2 /*return*/, deferred.promise];
}
});
});
};
FirestoreClient.prototype.getDocumentsFromLocalCache = function (query) {
return tslib.__awaiter(this, void 0, void 0, function () {
var deferred;
var _this = this;
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
this.verifyNotTerminated();
deferred = new Deferred();
return [4 /*yield*/, this.asyncQueue.enqueue(function () { return tslib.__awaiter(_this, void 0, void 0, function () {
var queryResult, view, viewDocChanges, viewChange, e_10, firestoreError;
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
_e.trys.push([0, 2, , 3]);
return [4 /*yield*/, this.localStore.executeQuery(query,
/* usePreviousResults= */ true)];
case 1:
queryResult = _e.sent();
view = new View(query, queryResult.remoteKeys);
viewDocChanges = view.computeDocChanges(queryResult.documents);
viewChange = view.applyChanges(viewDocChanges,
/* updateLimboDocuments= */ false);
deferred.resolve(viewChange.snapshot);
return [3 /*break*/, 3];
case 2:
e_10 = _e.sent();
firestoreError = wrapInUserErrorIfRecoverable(e_10, "Failed to execute query '" + query + " against cache");
deferred.reject(firestoreError);
return [3 /*break*/, 3];
case 3: return [2 /*return*/];
}
});
}); })];
case 1:
_e.sent();
return [2 /*return*/, deferred.promise];
}
});
});
};
FirestoreClient.prototype.write = function (mutations) {
var _this = this;
this.verifyNotTerminated();
var deferred = new Deferred();
this.asyncQueue.enqueueAndForget(function () { return _this.syncEngine.write(mutations, deferred); });
return deferred.promise;
};
FirestoreClient.prototype.databaseId = function () {
return this.databaseInfo.databaseId;
};
FirestoreClient.prototype.addSnapshotsInSyncListener = function (observer) {
var _this = this;
this.verifyNotTerminated();
this.asyncQueue.enqueueAndForget(function () {
_this.eventMgr.addSnapshotsInSyncListener(observer);
return Promise.resolve();
});
};
FirestoreClient.prototype.removeSnapshotsInSyncListener = function (observer) {
var _this = this;
// Checks for shutdown but does not raise error, allowing remove after
// shutdown to be a no-op.
if (this.clientTerminated) {
return;
}
this.asyncQueue.enqueueAndForget(function () {
_this.eventMgr.removeSnapshotsInSyncListener(observer);
return Promise.resolve();
});
};
Object.defineProperty(FirestoreClient.prototype, "clientTerminated", {
get: function () {
// Technically, the asyncQueue is still running, but only accepting operations
// related to termination or supposed to be run after termination. It is effectively
// terminated to the eyes of users.
return this.asyncQueue.isShuttingDown;
},
enumerable: true,
configurable: true
});
FirestoreClient.prototype.transaction = function (updateFunction) {
var _this = this;
this.verifyNotTerminated();
var deferred = new Deferred();
this.asyncQueue.enqueueAndForget(function () {
_this.syncEngine.runTransaction(_this.asyncQueue, updateFunction, deferred);
return Promise.resolve();
});
return deferred.promise;
};
return FirestoreClient;
}());
/**
* @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 wrapper implementation of Observer<T> that will dispatch events
* asynchronously. To allow immediate silencing, a mute call is added which
* causes events scheduled to no longer be raised.
*/
var AsyncObserver = /** @class */ (function () {
function AsyncObserver(observer) {
this.observer = observer;
/**
* When set to true, will not raise future events. Necessary to deal with
* async detachment of listener.
*/
this.muted = false;
}
AsyncObserver.prototype.next = function (value) {
this.scheduleEvent(this.observer.next, value);
};
AsyncObserver.prototype.error = function (error) {
this.scheduleEvent(this.observer.error, error);
};
AsyncObserver.prototype.mute = function () {
this.muted = true;
};
AsyncObserver.prototype.scheduleEvent = function (eventHandler, event) {
var _this = this;
if (!this.muted) {
setTimeout(function () {
if (!_this.muted) {
eventHandler(event);
}
}, 0);
}
};
return AsyncObserver;
}());
/**
* @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.
*/
/**
* Validates that no arguments were passed in the invocation of functionName.
*
* Forward the magic "arguments" variable as second parameter on which the
* parameter validation is performed:
* validateNoArgs('myFunction', arguments);
*/
function validateNoArgs(functionName, args) {
if (args.length !== 0) {
throw new FirestoreError(Code.INVALID_ARGUMENT, "Function " + functionName + "() does not support arguments, " +
'but was called with ' +
formatPlural(args.length, 'argument') +
'.');
}
}
/**
* Validates the invocation of functionName has the exact number of arguments.
*
* Forward the magic "arguments" variable as second parameter on which the
* parameter validation is performed:
* validateExactNumberOfArgs('myFunction', arguments, 2);
*/
function validateExactNumberOfArgs(functionName, args, numberOfArgs) {
if (args.length !== numberOfArgs) {
throw new FirestoreError(Code.INVALID_ARGUMENT, "Function " + functionName + "() requires " +
formatPlural(numberOfArgs, 'argument') +
', but was called with ' +
formatPlural(args.length, 'argument') +
'.');
}
}
/**
* Validates the invocation of functionName has at least the provided number of
* arguments (but can have many more).
*
* Forward the magic "arguments" variable as second parameter on which the
* parameter validation is performed:
* validateAtLeastNumberOfArgs('myFunction', arguments, 2);
*/
function validateAtLeastNumberOfArgs(functionName, args, minNumberOfArgs) {
if (args.length < minNumberOfArgs) {
throw new FirestoreError(Code.INVALID_ARGUMENT, "Function " + functionName + "() requires at least " +
formatPlural(minNumberOfArgs, 'argument') +
', but was called with ' +
formatPlural(args.length, 'argument') +
'.');
}
}
/**
* Validates the invocation of functionName has number of arguments between
* the values provided.
*
* Forward the magic "arguments" variable as second parameter on which the
* parameter validation is performed:
* validateBetweenNumberOfArgs('myFunction', arguments, 2, 3);
*/
function validateBetweenNumberOfArgs(functionName, args, minNumberOfArgs, maxNumberOfArgs) {
if (args.length < minNumberOfArgs || args.length > maxNumberOfArgs) {
throw new FirestoreError(Code.INVALID_ARGUMENT, "Function " + functionName + "() requires between " + minNumberOfArgs + " and " +
(maxNumberOfArgs + " arguments, but was called with ") +
formatPlural(args.length, 'argument') +
'.');
}
}
/**
* Validates the provided argument is an array and has as least the expected
* number of elements.
*/
function validateNamedArrayAtLeastNumberOfElements(functionName, value, name, minNumberOfElements) {
if (!(value instanceof Array) || value.length < minNumberOfElements) {
throw new FirestoreError(Code.INVALID_ARGUMENT, "Function " + functionName + "() requires its " + name + " argument to be an " +
'array with at least ' +
(formatPlural(minNumberOfElements, 'element') + "."));
}
}
/**
* Validates the provided positional argument has the native JavaScript type
* using typeof checks.
*/
function validateArgType(functionName, type, position, argument) {
validateType(functionName, type, ordinal(position) + " argument", argument);
}
/**
* Validates the provided argument has the native JavaScript type using
* typeof checks or is undefined.
*/
function validateOptionalArgType(functionName, type, position, argument) {
if (argument !== undefined) {
validateArgType(functionName, type, position, argument);
}
}
/**
* Validates the provided named option has the native JavaScript type using
* typeof checks.
*/
function validateNamedType(functionName, type, optionName, argument) {
validateType(functionName, type, optionName + " option", argument);
}
/**
* Validates the provided named option has the native JavaScript type using
* typeof checks or is undefined.
*/
function validateNamedOptionalType(functionName, type, optionName, argument) {
if (argument !== undefined) {
validateNamedType(functionName, type, optionName, argument);
}
}
function validateArrayElements(functionName, optionName, typeDescription, argument, validator) {
if (!(argument instanceof Array)) {
throw new FirestoreError(Code.INVALID_ARGUMENT, "Function " + functionName + "() requires its " + optionName + " " +
("option to be an array, but it was: " + valueDescription(argument)));
}
for (var i = 0; i < argument.length; ++i) {
if (!validator(argument[i])) {
throw new FirestoreError(Code.INVALID_ARGUMENT, "Function " + functionName + "() requires all " + optionName + " " +
("elements to be " + typeDescription + ", but the value at index " + i + " ") +
("was: " + valueDescription(argument[i])));
}
}
}
function validateOptionalArrayElements(functionName, optionName, typeDescription, argument, validator) {
if (argument !== undefined) {
validateArrayElements(functionName, optionName, typeDescription, argument, validator);
}
}
/**
* Validates that the provided named option equals one of the expected values.
*/
function validateNamedPropertyEquals(functionName, inputName, optionName, input, expected) {
var expectedDescription = [];
for (var _i = 0, expected_1 = expected; _i < expected_1.length; _i++) {
var val = expected_1[_i];
if (val === input) {
return;
}
expectedDescription.push(valueDescription(val));
}
var actualDescription = valueDescription(input);
throw new FirestoreError(Code.INVALID_ARGUMENT, "Invalid value " + actualDescription + " provided to function " + functionName + "() for option " +
("\"" + optionName + "\". Acceptable values: " + expectedDescription.join(', ')));
}
/**
* Validates that the provided named option equals one of the expected values or
* is undefined.
*/
function validateNamedOptionalPropertyEquals(functionName, inputName, optionName, input, expected) {
if (input !== undefined) {
validateNamedPropertyEquals(functionName, inputName, optionName, input, expected);
}
}
/**
* Validates that the provided argument is a valid enum.
*
* @param functionName Function making the validation call.
* @param enums Array containing all possible values for the enum.
* @param position Position of the argument in `functionName`.
* @param argument Argument to validate.
* @return The value as T if the argument can be converted.
*/
function validateStringEnum(functionName, enums, position, argument) {
if (!enums.some(function (element) { return element === argument; })) {
throw new FirestoreError(Code.INVALID_ARGUMENT, "Invalid value " + valueDescription(argument) + " provided to function " +
(functionName + "() for its " + ordinal(position) + " argument. Acceptable ") +
("values: " + enums.join(', ')));
}
return argument;
}
/** Helper to validate the type of a provided input. */
function validateType(functionName, type, inputName, input) {
var valid = false;
if (type === 'object') {
valid = isPlainObject(input);
}
else if (type === 'non-empty string') {
valid = typeof input === 'string' && input !== '';
}
else {
valid = typeof input === type;
}
if (!valid) {
var description = valueDescription(input);
throw new FirestoreError(Code.INVALID_ARGUMENT, "Function " + functionName + "() requires its " + inputName + " " +
("to be of type " + type + ", but it was: " + description));
}
}
/**
* Returns true if it's a non-null object without a custom prototype
* (i.e. excludes Array, Date, etc.).
*/
function isPlainObject(input) {
return (typeof input === 'object' &&
input !== null &&
(Object.getPrototypeOf(input) === Object.prototype ||
Object.getPrototypeOf(input) === null));
}
/** Returns a string describing the type / value of the provided input. */
function valueDescription(input) {
if (input === undefined) {
return 'undefined';
}
else if (input === null) {
return 'null';
}
else if (typeof input === 'string') {
if (input.length > 20) {
input = input.substring(0, 20) + "...";
}
return JSON.stringify(input);
}
else if (typeof input === 'number' || typeof input === 'boolean') {
return '' + input;
}
else if (typeof input === 'object') {
if (input instanceof Array) {
return 'an array';
}
else {
var customObjectName = tryGetCustomObjectType(input);
if (customObjectName) {
return "a custom " + customObjectName + " object";
}
else {
return 'an object';
}
}
}
else if (typeof input === 'function') {
return 'a function';
}
else {
return fail('Unknown wrong type: ' + typeof input);
}
}
/** Hacky method to try to get the constructor name for an object. */
function tryGetCustomObjectType(input) {
if (input.constructor) {
var funcNameRegex = /function\s+([^\s(]+)\s*\(/;
var results = funcNameRegex.exec(input.constructor.toString());
if (results && results.length > 1) {
return results[1];
}
}
return null;
}
/** Validates the provided argument is defined. */
function validateDefined(functionName, position, argument) {
if (argument === undefined) {
throw new FirestoreError(Code.INVALID_ARGUMENT, "Function " + functionName + "() requires a valid " + ordinal(position) + " " +
"argument, but it was undefined.");
}
}
/**
* Validates the provided positional argument is an object, and its keys and
* values match the expected keys and types provided in optionTypes.
*/
function validateOptionNames(functionName, options, optionNames) {
forEach(options, function (key, _) {
if (optionNames.indexOf(key) < 0) {
throw new FirestoreError(Code.INVALID_ARGUMENT, "Unknown option '" + key + "' passed to function " + functionName + "(). " +
'Available options: ' +
optionNames.join(', '));
}
});
}
/**
* Helper method to throw an error that the provided argument did not pass
* an instanceof check.
*/
function invalidClassError(functionName, type, position, argument) {
var description = valueDescription(argument);
return new FirestoreError(Code.INVALID_ARGUMENT, "Function " + functionName + "() requires its " + ordinal(position) + " " +
("argument to be a " + type + ", but it was: " + description));
}
function validatePositiveNumber(functionName, position, n) {
if (n <= 0) {
throw new FirestoreError(Code.INVALID_ARGUMENT, "Function " + functionName + "() requires its " + ordinal(position) + " argument to be a positive number, but it was: " + n + ".");
}
}
/** Converts a number to its english word representation */
function ordinal(num) {
switch (num) {
case 1:
return 'first';
case 2:
return 'second';
case 3:
return 'third';
default:
return num + 'th';
}
}
/**
* Formats the given word as plural conditionally given the preceding number.
*/
function formatPlural(num, str) {
return num + " " + str + (num === 1 ? '' : 's');
}
/**
* @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 objects that are a part of this API are exposed to third-parties as
// compiled javascript so we want to flag our private members with a leading
// underscore to discourage their use.
/**
* A FieldPath refers to a field in a document. The path may consist of a single
* field name (referring to a top-level field in the document), or a list of
* field names (referring to a nested field in the document).
*/
var FieldPath$1 = /** @class */ (function () {
/**
* Creates a FieldPath from the provided field names. If more than one field
* name is provided, the path will point to a nested field in a document.
*
* @param fieldNames A list of field names.
*/
function FieldPath$1() {
var fieldNames = [];
for (var _i = 0; _i < arguments.length; _i++) {
fieldNames[_i] = arguments[_i];
}
validateNamedArrayAtLeastNumberOfElements('FieldPath', fieldNames, 'fieldNames', 1);
for (var i = 0; i < fieldNames.length; ++i) {
validateArgType('FieldPath', 'string', i, fieldNames[i]);
if (fieldNames[i].length === 0) {
throw new FirestoreError(Code.INVALID_ARGUMENT, "Invalid field name at argument $(i + 1). " +
'Field names must not be empty.');
}
}
this._internalPath = new FieldPath(fieldNames);
}
FieldPath$1.documentId = function () {
return FieldPath$1._DOCUMENT_ID;
};
FieldPath$1.prototype.isEqual = function (other) {
if (!(other instanceof FieldPath$1)) {
throw invalidClassError('isEqual', 'FieldPath', 1, other);
}
return this._internalPath.isEqual(other._internalPath);
};
return FieldPath$1;
}());
/**
* Internal Note: The backend doesn't technically support querying by
* document ID. Instead it queries by the entire document name (full path
* included), but in the cases we currently support documentId(), the net
* effect is the same.
*/
FieldPath$1._DOCUMENT_ID = new FieldPath$1(FieldPath.keyField().canonicalString());
/**
* Matches any characters in a field path string that are reserved.
*/
var RESERVED = new RegExp('[~\\*/\\[\\]]');
/**
* Parses a field path string into a FieldPath, treating dots as separators.
*/
function fromDotSeparatedString(path) {
var found = path.search(RESERVED);
if (found >= 0) {
throw new FirestoreError(Code.INVALID_ARGUMENT, "Invalid field path (" + path + "). Paths must not contain " +
"'~', '*', '/', '[', or ']'");
}
try {
return new (FieldPath$1.bind.apply(FieldPath$1, tslib.__spreadArrays([void 0], path.split('.'))))();
}
catch (e) {
throw new FirestoreError(Code.INVALID_ARGUMENT, "Invalid field path (" + path + "). Paths must not be empty, " +
"begin with '.', end with '.', or contain '..'");
}
}
/**
* @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.
*/
var OAuthToken = /** @class */ (function () {
function OAuthToken(value, user) {
this.user = user;
this.type = 'OAuth';
this.authHeaders = {};
// Set the headers using Object Literal notation to avoid minification
this.authHeaders['Authorization'] = "Bearer " + value;
}
return OAuthToken;
}());
/** A CredentialsProvider that always yields an empty token. */
var EmptyCredentialsProvider = /** @class */ (function () {
function EmptyCredentialsProvider() {
/**
* Stores the listener registered with setChangeListener()
* This isn't actually necessary since the UID never changes, but we use this
* to verify the listen contract is adhered to in tests.
*/
this.changeListener = null;
}
EmptyCredentialsProvider.prototype.getToken = function () {
return Promise.resolve(null);
};
EmptyCredentialsProvider.prototype.invalidateToken = function () { };
EmptyCredentialsProvider.prototype.setChangeListener = function (changeListener) {
debugAssert(!this.changeListener, 'Can only call setChangeListener() once.');
this.changeListener = changeListener;
// Fire with initial user.
changeListener(User.UNAUTHENTICATED);
};
EmptyCredentialsProvider.prototype.removeChangeListener = function () {
debugAssert(this.changeListener !== null, 'removeChangeListener() when no listener registered');
this.changeListener = null;
};
return EmptyCredentialsProvider;
}());
var FirebaseCredentialsProvider = /** @class */ (function () {
function FirebaseCredentialsProvider(authProvider) {
var _this = this;
/**
* The auth token listener registered with FirebaseApp, retained here so we
* can unregister it.
*/
this.tokenListener = null;
/** Tracks the current User. */
this.currentUser = User.UNAUTHENTICATED;
this.receivedInitialUser = false;
/**
* Counter used to detect if the token changed while a getToken request was
* outstanding.
*/
this.tokenCounter = 0;
/** The listener registered with setChangeListener(). */
this.changeListener = null;
this.forceRefresh = false;
this.tokenListener = function () {
_this.tokenCounter++;
_this.currentUser = _this.getUser();
_this.receivedInitialUser = true;
if (_this.changeListener) {
_this.changeListener(_this.currentUser);
}
};
this.tokenCounter = 0;
this.auth = authProvider.getImmediate({ optional: true });
if (this.auth) {
this.auth.addAuthTokenListener(this.tokenListener);
}
else {
// if auth is not available, invoke tokenListener once with null token
this.tokenListener(null);
authProvider.get().then(function (auth) {
_this.auth = auth;
if (_this.tokenListener) {
// tokenListener can be removed by removeChangeListener()
_this.auth.addAuthTokenListener(_this.tokenListener);
}
}, function () {
/* this.authProvider.get() never rejects */
});
}
}
FirebaseCredentialsProvider.prototype.getToken = function () {
var _this = this;
debugAssert(this.tokenListener != null, 'getToken cannot be called after listener removed.');
// Take note of the current value of the tokenCounter so that this method
// can fail (with an ABORTED error) if there is a token change while the
// request is outstanding.
var initialTokenCounter = this.tokenCounter;
var forceRefresh = this.forceRefresh;
this.forceRefresh = false;
if (!this.auth) {
return Promise.resolve(null);
}
return this.auth.getToken(forceRefresh).then(function (tokenData) {
// Cancel the request since the token changed while the request was
// outstanding so the response is potentially for a previous user (which
// user, we can't be sure).
if (_this.tokenCounter !== initialTokenCounter) {
throw new FirestoreError(Code.ABORTED, 'getToken aborted due to token change.');
}
else {
if (tokenData) {
hardAssert(typeof tokenData.accessToken === 'string', 'Invalid tokenData returned from getToken():' + tokenData);
return new OAuthToken(tokenData.accessToken, _this.currentUser);
}
else {
return null;
}
}
});
};
FirebaseCredentialsProvider.prototype.invalidateToken = function () {
this.forceRefresh = true;
};
FirebaseCredentialsProvider.prototype.setChangeListener = function (changeListener) {
debugAssert(!this.changeListener, 'Can only call setChangeListener() once.');
this.changeListener = changeListener;
// Fire the initial event
if (this.receivedInitialUser) {
changeListener(this.currentUser);
}
};
FirebaseCredentialsProvider.prototype.removeChangeListener = function () {
debugAssert(this.tokenListener != null, 'removeChangeListener() called twice');
debugAssert(this.changeListener !== null, 'removeChangeListener() called when no listener registered');
if (this.auth) {
this.auth.removeAuthTokenListener(this.tokenListener);
}
this.tokenListener = null;
this.changeListener = null;
};
// Auth.getUid() can return null even with a user logged in. It is because
// getUid() is synchronous, but the auth code populating Uid is asynchronous.
// This method should only be called in the AuthTokenListener callback
// to guarantee to get the actual user.
FirebaseCredentialsProvider.prototype.getUser = function () {
var currentUid = this.auth && this.auth.getUid();
hardAssert(currentUid === null || typeof currentUid === 'string', 'Received invalid UID: ' + currentUid);
return new User(currentUid);
};
return FirebaseCredentialsProvider;
}());
/*
* FirstPartyToken provides a fresh token each time its value
* is requested, because if the token is too old, requests will be rejected.
* Technically this may no longer be necessary since the SDK should gracefully
* recover from unauthenticated errors (see b/33147818 for context), but it's
* safer to keep the implementation as-is.
*/
var FirstPartyToken = /** @class */ (function () {
function FirstPartyToken(gapi, sessionIndex) {
this.gapi = gapi;
this.sessionIndex = sessionIndex;
this.type = 'FirstParty';
this.user = User.FIRST_PARTY;
}
Object.defineProperty(FirstPartyToken.prototype, "authHeaders", {
get: function () {
var headers = {
'X-Goog-AuthUser': this.sessionIndex
};
var authHeader = this.gapi.auth.getAuthHeaderValueForFirstParty([]);
if (authHeader) {
headers['Authorization'] = authHeader;
}
return headers;
},
enumerable: true,
configurable: true
});
return FirstPartyToken;
}());
/*
* Provides user credentials required for the Firestore JavaScript SDK
* to authenticate the user, using technique that is only available
* to applications hosted by Google.
*/
var FirstPartyCredentialsProvider = /** @class */ (function () {
function FirstPartyCredentialsProvider(gapi, sessionIndex) {
this.gapi = gapi;
this.sessionIndex = sessionIndex;
}
FirstPartyCredentialsProvider.prototype.getToken = function () {
return Promise.resolve(new FirstPartyToken(this.gapi, this.sessionIndex));
};
FirstPartyCredentialsProvider.prototype.setChangeListener = function (changeListener) {
// Fire with initial uid.
changeListener(User.FIRST_PARTY);
};
FirstPartyCredentialsProvider.prototype.removeChangeListener = function () { };
FirstPartyCredentialsProvider.prototype.invalidateToken = function () { };
return FirstPartyCredentialsProvider;
}());
/**
* Builds a CredentialsProvider depending on the type of
* the credentials passed in.
*/
function makeCredentialsProvider(credentials) {
if (!credentials) {
return new EmptyCredentialsProvider();
}
switch (credentials.type) {
case 'gapi':
var client = credentials.client;
// Make sure this really is a Gapi client.
hardAssert(!!(typeof client === 'object' &&
client !== null &&
client['auth'] &&
client['auth']['getAuthHeaderValueForFirstParty']), 'unexpected gapi interface');
return new FirstPartyCredentialsProvider(client, credentials.sessionIndex || '0');
case 'provider':
return credentials.client;
default:
throw new FirestoreError(Code.INVALID_ARGUMENT, 'makeCredentialsProvider failed due to invalid credential type');
}
}
/**
* @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 isPartialObserver(obj) {
return implementsAnyMethods(obj, ['next', 'error', 'complete']);
}
/**
* Returns true if obj is an object and contains at least one of the specified
* methods.
*/
function implementsAnyMethods(obj, methods) {
if (typeof obj !== 'object' || obj === null) {
return false;
}
var object = obj;
for (var _i = 0, methods_1 = methods; _i < methods_1.length; _i++) {
var method = methods_1[_i];
if (method in object && typeof object[method] === 'function') {
return true;
}
}
return false;
}
/**
* @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.
*/
/** Helper function to assert Uint8Array is available at runtime. */
function assertUint8ArrayAvailable() {
if (typeof Uint8Array === 'undefined') {
throw new FirestoreError(Code.UNIMPLEMENTED, 'Uint8Arrays are not available in this environment.');
}
}
/** Helper function to assert Base64 functions are available at runtime. */
function assertBase64Available() {
if (!PlatformSupport.getPlatform().base64Available) {
throw new FirestoreError(Code.UNIMPLEMENTED, 'Blobs are unavailable in Firestore in this environment.');
}
}
/**
* Immutable class holding a blob (binary data).
* This class is directly exposed in the public API.
*
* Note that while you can't hide the constructor in JavaScript code, we are
* using the hack above to make sure no-one outside this module can call it.
*/
var Blob = /** @class */ (function () {
function Blob(byteString) {
assertBase64Available();
this._byteString = byteString;
}
Blob.fromBase64String = function (base64) {
validateExactNumberOfArgs('Blob.fromBase64String', arguments, 1);
validateArgType('Blob.fromBase64String', 'string', 1, base64);
assertBase64Available();
try {
return new Blob(ByteString.fromBase64String(base64));
}
catch (e) {
throw new FirestoreError(Code.INVALID_ARGUMENT, 'Failed to construct Blob from Base64 string: ' + e);
}
};
Blob.fromUint8Array = function (array) {
validateExactNumberOfArgs('Blob.fromUint8Array', arguments, 1);
assertUint8ArrayAvailable();
if (!(array instanceof Uint8Array)) {
throw invalidClassError('Blob.fromUint8Array', 'Uint8Array', 1, array);
}
return new Blob(ByteString.fromUint8Array(array));
};
Blob.prototype.toBase64 = function () {
validateExactNumberOfArgs('Blob.toBase64', arguments, 0);
assertBase64Available();
return this._byteString.toBase64();
};
Blob.prototype.toUint8Array = function () {
validateExactNumberOfArgs('Blob.toUint8Array', arguments, 0);
assertUint8ArrayAvailable();
return this._byteString.toUint8Array();
};
Blob.prototype.toString = function () {
return 'Blob(base64: ' + this.toBase64() + ')';
};
Blob.prototype.isEqual = function (other) {
return this._byteString.isEqual(other._byteString);
};
return Blob;
}());
/**
* @license
* Copyright 2018 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.
*/
/** Transforms a value into a server-generated timestamp. */
var ServerTimestampTransform = /** @class */ (function () {
function ServerTimestampTransform() {
}
ServerTimestampTransform.prototype.applyToLocalView = function (previousValue, localWriteTime) {
return serverTimestamp(localWriteTime, previousValue);
};
ServerTimestampTransform.prototype.applyToRemoteDocument = function (previousValue, transformResult) {
return transformResult;
};
ServerTimestampTransform.prototype.computeBaseValue = function (previousValue) {
return null; // Server timestamps are idempotent and don't require a base value.
};
ServerTimestampTransform.prototype.isEqual = function (other) {
return other instanceof ServerTimestampTransform;
};
return ServerTimestampTransform;
}());
ServerTimestampTransform.instance = new ServerTimestampTransform();
/** Transforms an array value via a union operation. */
var ArrayUnionTransformOperation = /** @class */ (function () {
function ArrayUnionTransformOperation(elements) {
this.elements = elements;
}
ArrayUnionTransformOperation.prototype.applyToLocalView = function (previousValue, localWriteTime) {
return this.apply(previousValue);
};
ArrayUnionTransformOperation.prototype.applyToRemoteDocument = function (previousValue, transformResult) {
// The server just sends null as the transform result for array operations,
// so we have to calculate a result the same as we do for local
// applications.
return this.apply(previousValue);
};
ArrayUnionTransformOperation.prototype.apply = function (previousValue) {
var values = coercedFieldValuesArray(previousValue);
var _loop_4 = function (toUnion) {
if (!values.some(function (element) { return valueEquals(element, toUnion); })) {
values.push(toUnion);
}
};
for (var _i = 0, _e = this.elements; _i < _e.length; _i++) {
var toUnion = _e[_i];
_loop_4(toUnion);
}
return { arrayValue: { values: values } };
};
ArrayUnionTransformOperation.prototype.computeBaseValue = function (previousValue) {
return null; // Array transforms are idempotent and don't require a base value.
};
ArrayUnionTransformOperation.prototype.isEqual = function (other) {
return (other instanceof ArrayUnionTransformOperation &&
arrayEquals(this.elements, other.elements, valueEquals));
};
return ArrayUnionTransformOperation;
}());
/** Transforms an array value via a remove operation. */
var ArrayRemoveTransformOperation = /** @class */ (function () {
function ArrayRemoveTransformOperation(elements) {
this.elements = elements;
}
ArrayRemoveTransformOperation.prototype.applyToLocalView = function (previousValue, localWriteTime) {
return this.apply(previousValue);
};
ArrayRemoveTransformOperation.prototype.applyToRemoteDocument = function (previousValue, transformResult) {
// The server just sends null as the transform result for array operations,
// so we have to calculate a result the same as we do for local
// applications.
return this.apply(previousValue);
};
ArrayRemoveTransformOperation.prototype.apply = function (previousValue) {
var values = coercedFieldValuesArray(previousValue);
var _loop_5 = function (toRemove) {
values = values.filter(function (element) { return !valueEquals(element, toRemove); });
};
for (var _i = 0, _e = this.elements; _i < _e.length; _i++) {
var toRemove = _e[_i];
_loop_5(toRemove);
}
return { arrayValue: { values: values } };
};
ArrayRemoveTransformOperation.prototype.computeBaseValue = function (previousValue) {
return null; // Array transforms are idempotent and don't require a base value.
};
ArrayRemoveTransformOperation.prototype.isEqual = function (other) {
return (other instanceof ArrayRemoveTransformOperation &&
arrayEquals(this.elements, other.elements, valueEquals));
};
return ArrayRemoveTransformOperation;
}());
/**
* Implements the backend semantics for locally computed NUMERIC_ADD (increment)
* transforms. Converts all field values to integers or doubles, but unlike the
* backend does not cap integer values at 2^63. Instead, JavaScript number
* arithmetic is used and precision loss can occur for values greater than 2^53.
*/
var NumericIncrementTransformOperation = /** @class */ (function () {
function NumericIncrementTransformOperation(serializer, operand) {
this.serializer = serializer;
this.operand = operand;
debugAssert(isNumber(operand), 'NumericIncrementTransform transform requires a NumberValue');
}
NumericIncrementTransformOperation.prototype.applyToLocalView = function (previousValue, localWriteTime) {
// PORTING NOTE: Since JavaScript's integer arithmetic is limited to 53 bit
// precision and resolves overflows by reducing precision, we do not
// manually cap overflows at 2^63.
var baseValue = this.computeBaseValue(previousValue);
var sum = this.asNumber(baseValue) + this.asNumber(this.operand);
if (isInteger(baseValue) && isInteger(this.operand)) {
return this.serializer.toInteger(sum);
}
else {
return this.serializer.toDouble(sum);
}
};
NumericIncrementTransformOperation.prototype.applyToRemoteDocument = function (previousValue, transformResult) {
debugAssert(transformResult !== null, "Didn't receive transformResult for NUMERIC_ADD transform");
return transformResult;
};
/**
* Inspects the provided value, returning the provided value if it is already
* a NumberValue, otherwise returning a coerced value of 0.
*/
NumericIncrementTransformOperation.prototype.computeBaseValue = function (previousValue) {
return isNumber(previousValue) ? previousValue : { integerValue: 0 };
};
NumericIncrementTransformOperation.prototype.isEqual = function (other) {
return (other instanceof NumericIncrementTransformOperation &&
valueEquals(this.operand, other.operand));
};
NumericIncrementTransformOperation.prototype.asNumber = function (value) {
return normalizeNumber(value.integerValue || value.doubleValue);
};
return NumericIncrementTransformOperation;
}());
function coercedFieldValuesArray(value) {
return isArray(value) && value.arrayValue.values
? value.arrayValue.values.slice()
: [];
}
/**
* @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 opaque base class for FieldValue sentinel objects in our public API,
* with public static methods for creating said sentinel objects.
*/
var FieldValueImpl = /** @class */ (function () {
function FieldValueImpl(_methodName) {
this._methodName = _methodName;
}
return FieldValueImpl;
}());
var DeleteFieldValueImpl = /** @class */ (function (_super) {
tslib.__extends(DeleteFieldValueImpl, _super);
function DeleteFieldValueImpl() {
return _super.call(this, 'FieldValue.delete') || this;
}
DeleteFieldValueImpl.prototype.toFieldTransform = function (context) {
if (context.dataSource === 2 /* MergeSet */) {
// No transform to add for a delete, but we need to add it to our
// fieldMask so it gets deleted.
context.fieldMask.push(context.path);
}
else if (context.dataSource === 1 /* Update */) {
debugAssert(context.path.length > 0, 'FieldValue.delete() at the top level should have already' +
' been handled.');
throw context.createError('FieldValue.delete() can only appear at the top level ' +
'of your update data');
}
else {
// We shouldn't encounter delete sentinels for queries or non-merge set() calls.
throw context.createError('FieldValue.delete() cannot be used with set() unless you pass ' +
'{merge:true}');
}
return null;
};
DeleteFieldValueImpl.prototype.isEqual = function (other) {
return other instanceof DeleteFieldValueImpl;
};
return DeleteFieldValueImpl;
}(FieldValueImpl));
var ServerTimestampFieldValueImpl = /** @class */ (function (_super) {
tslib.__extends(ServerTimestampFieldValueImpl, _super);
function ServerTimestampFieldValueImpl() {
return _super.call(this, 'FieldValue.serverTimestamp') || this;
}
ServerTimestampFieldValueImpl.prototype.toFieldTransform = function (context) {
return new FieldTransform(context.path, ServerTimestampTransform.instance);
};
ServerTimestampFieldValueImpl.prototype.isEqual = function (other) {
return other instanceof ServerTimestampFieldValueImpl;
};
return ServerTimestampFieldValueImpl;
}(FieldValueImpl));
var ArrayUnionFieldValueImpl = /** @class */ (function (_super) {
tslib.__extends(ArrayUnionFieldValueImpl, _super);
function ArrayUnionFieldValueImpl(_elements) {
var _this = _super.call(this, 'FieldValue.arrayUnion') || this;
_this._elements = _elements;
return _this;
}
ArrayUnionFieldValueImpl.prototype.toFieldTransform = function (context) {
// Although array transforms are used with writes, the actual elements
// being uniomed or removed are not considered writes since they cannot
// contain any FieldValue sentinels, etc.
var parseContext = new ParseContext({
dataSource: 3 /* Argument */,
methodName: this._methodName,
arrayElement: true
}, context.databaseId, context.serializer, context.ignoreUndefinedProperties);
var parsedElements = this._elements.map(function (element) { return parseData(element, parseContext); });
var arrayUnion = new ArrayUnionTransformOperation(parsedElements);
return new FieldTransform(context.path, arrayUnion);
};
ArrayUnionFieldValueImpl.prototype.isEqual = function (other) {
// TODO(mrschmidt): Implement isEquals
return this === other;
};
return ArrayUnionFieldValueImpl;
}(FieldValueImpl));
var ArrayRemoveFieldValueImpl = /** @class */ (function (_super) {
tslib.__extends(ArrayRemoveFieldValueImpl, _super);
function ArrayRemoveFieldValueImpl(_elements) {
var _this = _super.call(this, 'FieldValue.arrayRemove') || this;
_this._elements = _elements;
return _this;
}
ArrayRemoveFieldValueImpl.prototype.toFieldTransform = function (context) {
// Although array transforms are used with writes, the actual elements
// being unioned or removed are not considered writes since they cannot
// contain any FieldValue sentinels, etc.
var parseContext = new ParseContext({
dataSource: 3 /* Argument */,
methodName: this._methodName,
arrayElement: true
}, context.databaseId, context.serializer, context.ignoreUndefinedProperties);
var parsedElements = this._elements.map(function (element) { return parseData(element, parseContext); });
var arrayUnion = new ArrayRemoveTransformOperation(parsedElements);
return new FieldTransform(context.path, arrayUnion);
};
ArrayRemoveFieldValueImpl.prototype.isEqual = function (other) {
// TODO(mrschmidt): Implement isEquals
return this === other;
};
return ArrayRemoveFieldValueImpl;
}(FieldValueImpl));
var NumericIncrementFieldValueImpl = /** @class */ (function (_super) {
tslib.__extends(NumericIncrementFieldValueImpl, _super);
function NumericIncrementFieldValueImpl(_operand) {
var _this = _super.call(this, 'FieldValue.increment') || this;
_this._operand = _operand;
return _this;
}
NumericIncrementFieldValueImpl.prototype.toFieldTransform = function (context) {
var parseContext = new ParseContext({
dataSource: 3 /* Argument */,
methodName: this._methodName
}, context.databaseId, context.serializer, context.ignoreUndefinedProperties);
var operand = parseData(this._operand, parseContext);
var numericIncrement = new NumericIncrementTransformOperation(context.serializer, operand);
return new FieldTransform(context.path, numericIncrement);
};
NumericIncrementFieldValueImpl.prototype.isEqual = function (other) {
// TODO(mrschmidt): Implement isEquals
return this === other;
};
return NumericIncrementFieldValueImpl;
}(FieldValueImpl));
var FieldValue = /** @class */ (function () {
function FieldValue() {
}
FieldValue.delete = function () {
validateNoArgs('FieldValue.delete', arguments);
return new DeleteFieldValueImpl();
};
FieldValue.serverTimestamp = function () {
validateNoArgs('FieldValue.serverTimestamp', arguments);
return new ServerTimestampFieldValueImpl();
};
FieldValue.arrayUnion = function () {
var elements = [];
for (var _i = 0; _i < arguments.length; _i++) {
elements[_i] = arguments[_i];
}
validateAtLeastNumberOfArgs('FieldValue.arrayUnion', arguments, 1);
// NOTE: We don't actually parse the data until it's used in set() or
// update() since we need access to the Firestore instance.
return new ArrayUnionFieldValueImpl(elements);
};
FieldValue.arrayRemove = function () {
var elements = [];
for (var _i = 0; _i < arguments.length; _i++) {
elements[_i] = arguments[_i];
}
validateAtLeastNumberOfArgs('FieldValue.arrayRemove', arguments, 1);
// NOTE: We don't actually parse the data until it's used in set() or
// update() since we need access to the Firestore instance.
return new ArrayRemoveFieldValueImpl(elements);
};
FieldValue.increment = function (n) {
validateArgType('FieldValue.increment', 'number', 1, n);
validateExactNumberOfArgs('FieldValue.increment', arguments, 1);
return new NumericIncrementFieldValueImpl(n);
};
FieldValue.prototype.isEqual = function (other) {
return this === other;
};
return FieldValue;
}());
/**
* @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.
*/
/**
* Immutable class representing a geo point as latitude-longitude pair.
* This class is directly exposed in the public API, including its constructor.
*/
var GeoPoint = /** @class */ (function () {
function GeoPoint(latitude, longitude) {
validateExactNumberOfArgs('GeoPoint', arguments, 2);
validateArgType('GeoPoint', 'number', 1, latitude);
validateArgType('GeoPoint', 'number', 2, longitude);
if (!isFinite(latitude) || latitude < -90 || latitude > 90) {
throw new FirestoreError(Code.INVALID_ARGUMENT, 'Latitude must be a number between -90 and 90, but was: ' + latitude);
}
if (!isFinite(longitude) || longitude < -180 || longitude > 180) {
throw new FirestoreError(Code.INVALID_ARGUMENT, 'Longitude must be a number between -180 and 180, but was: ' + longitude);
}
this._lat = latitude;
this._long = longitude;
}
Object.defineProperty(GeoPoint.prototype, "latitude", {
/**
* Returns the latitude of this geo point, a number between -90 and 90.
*/
get: function () {
return this._lat;
},
enumerable: true,
configurable: true
});
Object.defineProperty(GeoPoint.prototype, "longitude", {
/**
* Returns the longitude of this geo point, a number between -180 and 180.
*/
get: function () {
return this._long;
},
enumerable: true,
configurable: true
});
GeoPoint.prototype.isEqual = function (other) {
return this._lat === other._lat && this._long === other._long;
};
/**
* Actually private to JS consumers of our API, so this function is prefixed
* with an underscore.
*/
GeoPoint.prototype._compareTo = function (other) {
return (primitiveComparator(this._lat, other._lat) ||
primitiveComparator(this._long, other._long));
};
return GeoPoint;
}());
/**
* @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.
*/
var RESERVED_FIELD_REGEX = /^__.*__$/;
/** The result of parsing document data (e.g. for a setData call). */
var ParsedSetData = /** @class */ (function () {
function ParsedSetData(data, fieldMask, fieldTransforms) {
this.data = data;
this.fieldMask = fieldMask;
this.fieldTransforms = fieldTransforms;
}
ParsedSetData.prototype.toMutations = function (key, precondition) {
var mutations = [];
if (this.fieldMask !== null) {
mutations.push(new PatchMutation(key, this.data, this.fieldMask, precondition));
}
else {
mutations.push(new SetMutation(key, this.data, precondition));
}
if (this.fieldTransforms.length > 0) {
mutations.push(new TransformMutation(key, this.fieldTransforms));
}
return mutations;
};
return ParsedSetData;
}());
/** The result of parsing "update" data (i.e. for an updateData call). */
var ParsedUpdateData = /** @class */ (function () {
function ParsedUpdateData(data, fieldMask, fieldTransforms) {
this.data = data;
this.fieldMask = fieldMask;
this.fieldTransforms = fieldTransforms;
}
ParsedUpdateData.prototype.toMutations = function (key, precondition) {
var mutations = [
new PatchMutation(key, this.data, this.fieldMask, precondition)
];
if (this.fieldTransforms.length > 0) {
mutations.push(new TransformMutation(key, this.fieldTransforms));
}
return mutations;
};
return ParsedUpdateData;
}());
function isWrite(dataSource) {
switch (dataSource) {
case 0 /* Set */: // fall through
case 2 /* MergeSet */: // fall through
case 1 /* Update */:
return true;
case 3 /* Argument */:
case 4 /* ArrayArgument */:
return false;
default:
throw fail("Unexpected case for UserDataSource: " + dataSource);
}
}
/** A "context" object passed around while parsing user data. */
var ParseContext = /** @class */ (function () {
/**
* Initializes a ParseContext with the given source and path.
*
* @param settings The settings for the parser.
* @param databaseId The database ID of the Firestore instance.
* @param serializer The serializer to use to generate the Value proto.
* @param ignoreUndefinedProperties Whether to ignore undefined properties
* rather than throw.
* @param fieldTransforms A mutable list of field transforms encountered while
* parsing the data.
* @param fieldMask A mutable list of field paths encountered while parsing
* the data.
*
* TODO(b/34871131): We don't support array paths right now, so path can be
* null to indicate the context represents any location within an array (in
* which case certain features will not work and errors will be somewhat
* compromised).
*/
function ParseContext(settings, databaseId, serializer, ignoreUndefinedProperties, fieldTransforms, fieldMask) {
this.settings = settings;
this.databaseId = databaseId;
this.serializer = serializer;
this.ignoreUndefinedProperties = ignoreUndefinedProperties;
// Minor hack: If fieldTransforms is undefined, we assume this is an
// external call and we need to validate the entire path.
if (fieldTransforms === undefined) {
this.validatePath();
}
this.fieldTransforms = fieldTransforms || [];
this.fieldMask = fieldMask || [];
}
Object.defineProperty(ParseContext.prototype, "path", {
get: function () {
return this.settings.path;
},
enumerable: true,
configurable: true
});
Object.defineProperty(ParseContext.prototype, "dataSource", {
get: function () {
return this.settings.dataSource;
},
enumerable: true,
configurable: true
});
/** Returns a new context with the specified settings overwritten. */
ParseContext.prototype.contextWith = function (configuration) {
return new ParseContext(Object.assign(Object.assign({}, this.settings), configuration), this.databaseId, this.serializer, this.ignoreUndefinedProperties, this.fieldTransforms, this.fieldMask);
};
ParseContext.prototype.childContextForField = function (field) {
var _a;
var childPath = (_a = this.path) === null || _a === void 0 ? void 0 : _a.child(field);
var context = this.contextWith({ path: childPath, arrayElement: false });
context.validatePathSegment(field);
return context;
};
ParseContext.prototype.childContextForFieldPath = function (field) {
var _a;
var childPath = (_a = this.path) === null || _a === void 0 ? void 0 : _a.child(field);
var context = this.contextWith({ path: childPath, arrayElement: false });
context.validatePath();
return context;
};
ParseContext.prototype.childContextForArray = function (index) {
// TODO(b/34871131): We don't support array paths right now; so make path
// undefined.
return this.contextWith({ path: undefined, arrayElement: true });
};
ParseContext.prototype.createError = function (reason) {
var fieldDescription = !this.path || this.path.isEmpty()
? ''
: " (found in field " + this.path.toString() + ")";
return new FirestoreError(Code.INVALID_ARGUMENT, "Function " + this.settings.methodName + "() called with invalid data. " +
reason +
fieldDescription);
};
/** Returns 'true' if 'fieldPath' was traversed when creating this context. */
ParseContext.prototype.contains = function (fieldPath) {
return (this.fieldMask.find(function (field) { return fieldPath.isPrefixOf(field); }) !== undefined ||
this.fieldTransforms.find(function (transform) { return fieldPath.isPrefixOf(transform.field); }) !== undefined);
};
ParseContext.prototype.validatePath = function () {
// TODO(b/34871131): Remove null check once we have proper paths for fields
// within arrays.
if (!this.path) {
return;
}
for (var i = 0; i < this.path.length; i++) {
this.validatePathSegment(this.path.get(i));
}
};
ParseContext.prototype.validatePathSegment = function (segment) {
if (segment.length === 0) {
throw this.createError('Document fields must not be empty');
}
if (isWrite(this.dataSource) && RESERVED_FIELD_REGEX.test(segment)) {
throw this.createError('Document fields cannot begin and end with "__"');
}
};
return ParseContext;
}());
/**
* Helper for parsing raw user input (provided via the API) into internal model
* classes.
*/
var UserDataReader = /** @class */ (function () {
function UserDataReader(databaseId, ignoreUndefinedProperties, serializer) {
this.databaseId = databaseId;
this.ignoreUndefinedProperties = ignoreUndefinedProperties;
this.serializer =
serializer || PlatformSupport.getPlatform().newSerializer(databaseId);
}
/** Parse document data from a non-merge set() call. */
UserDataReader.prototype.parseSetData = function (methodName, input) {
var context = this.createContext(0 /* Set */, methodName);
validatePlainObject('Data must be an object, but it was:', context, input);
var updateData = parseObject(input, context);
return new ParsedSetData(new ObjectValue(updateData),
/* fieldMask= */ null, context.fieldTransforms);
};
/** Parse document data from a set() call with '{merge:true}'. */
UserDataReader.prototype.parseMergeData = function (methodName, input, fieldPaths) {
var context = this.createContext(2 /* MergeSet */, methodName);
validatePlainObject('Data must be an object, but it was:', context, input);
var updateData = parseObject(input, context);
var fieldMask;
var fieldTransforms;
if (!fieldPaths) {
fieldMask = new FieldMask(context.fieldMask);
fieldTransforms = context.fieldTransforms;
}
else {
var validatedFieldPaths = [];
for (var _i = 0, fieldPaths_1 = fieldPaths; _i < fieldPaths_1.length; _i++) {
var stringOrFieldPath = fieldPaths_1[_i];
var fieldPath = void 0;
if (stringOrFieldPath instanceof FieldPath$1) {
fieldPath = stringOrFieldPath._internalPath;
}
else if (typeof stringOrFieldPath === 'string') {
fieldPath = fieldPathFromDotSeparatedString(methodName, stringOrFieldPath);
}
else {
throw fail('Expected stringOrFieldPath to be a string or a FieldPath');
}
if (!context.contains(fieldPath)) {
throw new FirestoreError(Code.INVALID_ARGUMENT, "Field '" + fieldPath + "' is specified in your field mask but missing from your input data.");
}
if (!fieldMaskContains(validatedFieldPaths, fieldPath)) {
validatedFieldPaths.push(fieldPath);
}
}
fieldMask = new FieldMask(validatedFieldPaths);
fieldTransforms = context.fieldTransforms.filter(function (transform) { return fieldMask.covers(transform.field); });
}
return new ParsedSetData(new ObjectValue(updateData), fieldMask, fieldTransforms);
};
/** Parse update data from an update() call. */
UserDataReader.prototype.parseUpdateData = function (methodName, input) {
var context = this.createContext(1 /* Update */, methodName);
validatePlainObject('Data must be an object, but it was:', context, input);
var fieldMaskPaths = [];
var updateData = new ObjectValueBuilder();
forEach(input, function (key, value) {
var path = fieldPathFromDotSeparatedString(methodName, key);
var childContext = context.childContextForFieldPath(path);
if (value instanceof DeleteFieldValueImpl) {
// Add it to the field mask, but don't add anything to updateData.
fieldMaskPaths.push(path);
}
else {
var parsedValue = parseData(value, childContext);
if (parsedValue != null) {
fieldMaskPaths.push(path);
updateData.set(path, parsedValue);
}
}
});
var mask = new FieldMask(fieldMaskPaths);
return new ParsedUpdateData(updateData.build(), mask, context.fieldTransforms);
};
/** Parse update data from a list of field/value arguments. */
UserDataReader.prototype.parseUpdateVarargs = function (methodName, field, value, moreFieldsAndValues) {
var context = this.createContext(1 /* Update */, methodName);
var keys = [fieldPathFromArgument(methodName, field)];
var values = [value];
if (moreFieldsAndValues.length % 2 !== 0) {
throw new FirestoreError(Code.INVALID_ARGUMENT, "Function " + methodName + "() needs to be called with an even number " +
'of arguments that alternate between field names and values.');
}
for (var i = 0; i < moreFieldsAndValues.length; i += 2) {
keys.push(fieldPathFromArgument(methodName, moreFieldsAndValues[i]));
values.push(moreFieldsAndValues[i + 1]);
}
var fieldMaskPaths = [];
var updateData = new ObjectValueBuilder();
// We iterate in reverse order to pick the last value for a field if the
// user specified the field multiple times.
for (var i = keys.length - 1; i >= 0; --i) {
if (!fieldMaskContains(fieldMaskPaths, keys[i])) {
var path = keys[i];
var value_1 = values[i];
var childContext = context.childContextForFieldPath(path);
if (value_1 instanceof DeleteFieldValueImpl) {
// Add it to the field mask, but don't add anything to updateData.
fieldMaskPaths.push(path);
}
else {
var parsedValue = parseData(value_1, childContext);
if (parsedValue != null) {
fieldMaskPaths.push(path);
updateData.set(path, parsedValue);
}
}
}
}
var mask = new FieldMask(fieldMaskPaths);
return new ParsedUpdateData(updateData.build(), mask, context.fieldTransforms);
};
/** Creates a new top-level parse context. */
UserDataReader.prototype.createContext = function (dataSource, methodName) {
return new ParseContext({
dataSource: dataSource,
methodName: methodName,
path: FieldPath.EMPTY_PATH,
arrayElement: false
}, this.databaseId, this.serializer, this.ignoreUndefinedProperties);
};
/**
* Parse a "query value" (e.g. value in a where filter or a value in a cursor
* bound).
*
* @param allowArrays Whether the query value is an array that may directly
* contain additional arrays (e.g. the operand of an `in` query).
*/
UserDataReader.prototype.parseQueryValue = function (methodName, input, allowArrays) {
if (allowArrays === void 0) { allowArrays = false; }
var context = this.createContext(allowArrays ? 4 /* ArrayArgument */ : 3 /* Argument */, methodName);
var parsed = parseData(input, context);
debugAssert(parsed != null, 'Parsed data should not be null.');
debugAssert(context.fieldTransforms.length === 0, 'Field transforms should have been disallowed.');
return parsed;
};
return UserDataReader;
}());
/**
* Parses user data to Protobuf Values.
*
* @param input Data to be parsed.
* @param context A context object representing the current path being parsed,
* the source of the data being parsed, etc.
* @return The parsed value, or null if the value was a FieldValue sentinel
* that should not be included in the resulting parsed data.
*/
function parseData(input, context) {
if (looksLikeJsonObject(input)) {
validatePlainObject('Unsupported field value:', context, input);
return parseObject(input, context);
}
else if (input instanceof FieldValueImpl) {
// FieldValues usually parse into transforms (except FieldValue.delete())
// in which case we do not want to include this field in our parsed data
// (as doing so will overwrite the field directly prior to the transform
// trying to transform it). So we don't add this location to
// context.fieldMask and we return null as our parsing result.
parseSentinelFieldValue(input, context);
return null;
}
else {
// If context.path is null we are inside an array and we don't support
// field mask paths more granular than the top-level array.
if (context.path) {
context.fieldMask.push(context.path);
}
if (input instanceof Array) {
// TODO(b/34871131): Include the path containing the array in the error
// message.
// In the case of IN queries, the parsed data is an array (representing
// the set of values to be included for the IN query) that may directly
// contain additional arrays (each representing an individual field
// value), so we disable this validation.
if (context.settings.arrayElement &&
context.dataSource !== 4 /* ArrayArgument */) {
throw context.createError('Nested arrays are not supported');
}
return parseArray(input, context);
}
else {
return parseScalarValue(input, context);
}
}
}
function parseObject(obj, context) {
var fields = {};
if (isEmpty(obj)) {
// If we encounter an empty object, we explicitly add it to the update
// mask to ensure that the server creates a map entry.
if (context.path && context.path.length > 0) {
context.fieldMask.push(context.path);
}
}
else {
forEach(obj, function (key, val) {
var parsedValue = parseData(val, context.childContextForField(key));
if (parsedValue != null) {
fields[key] = parsedValue;
}
});
}
return { mapValue: { fields: fields } };
}
function parseArray(array, context) {
var values = [];
var entryIndex = 0;
for (var _i = 0, array_1 = array; _i < array_1.length; _i++) {
var entry = array_1[_i];
var parsedEntry = parseData(entry, context.childContextForArray(entryIndex));
if (parsedEntry == null) {
// Just include nulls in the array for fields being replaced with a
// sentinel.
parsedEntry = { nullValue: 'NULL_VALUE' };
}
values.push(parsedEntry);
entryIndex++;
}
return { arrayValue: { values: values } };
}
/**
* "Parses" the provided FieldValueImpl, adding any necessary transforms to
* context.fieldTransforms.
*/
function parseSentinelFieldValue(value, context) {
// Sentinels are only supported with writes, and not within arrays.
if (!isWrite(context.dataSource)) {
throw context.createError(value._methodName + "() can only be used with update() and set()");
}
if (context.path === null) {
throw context.createError(value._methodName + "() is not currently supported inside arrays");
}
var fieldTransform = value.toFieldTransform(context);
if (fieldTransform) {
context.fieldTransforms.push(fieldTransform);
}
}
/**
* Helper to parse a scalar value (i.e. not an Object, Array, or FieldValue)
*
* @return The parsed value
*/
function parseScalarValue(value, context) {
if (value === null) {
return { nullValue: 'NULL_VALUE' };
}
else if (typeof value === 'number') {
return context.serializer.toNumber(value);
}
else if (typeof value === 'boolean') {
return { booleanValue: value };
}
else if (typeof value === 'string') {
return { stringValue: value };
}
else if (value instanceof Date) {
var timestamp = Timestamp.fromDate(value);
return { timestampValue: context.serializer.toTimestamp(timestamp) };
}
else if (value instanceof Timestamp) {
// Firestore backend truncates precision down to microseconds. To ensure
// offline mode works the same with regards to truncation, perform the
// truncation immediately without waiting for the backend to do that.
var timestamp = new Timestamp(value.seconds, Math.floor(value.nanoseconds / 1000) * 1000);
return { timestampValue: context.serializer.toTimestamp(timestamp) };
}
else if (value instanceof GeoPoint) {
return {
geoPointValue: {
latitude: value.latitude,
longitude: value.longitude
}
};
}
else if (value instanceof Blob) {
return { bytesValue: context.serializer.toBytes(value) };
}
else if (value instanceof DocumentReference) {
var thisDb = context.databaseId;
var otherDb = value.firestore._databaseId;
if (!otherDb.isEqual(thisDb)) {
throw context.createError('Document reference is for database ' +
(otherDb.projectId + "/" + otherDb.database + " but should be ") +
("for database " + thisDb.projectId + "/" + thisDb.database));
}
return {
referenceValue: context.serializer.toResourceName(value._key.path, value.firestore._databaseId)
};
}
else if (value === undefined && context.ignoreUndefinedProperties) {
return null;
}
else {
throw context.createError("Unsupported field value: " + valueDescription(value));
}
}
/**
* Checks whether an object looks like a JSON object that should be converted
* into a struct. Normal class/prototype instances are considered to look like
* JSON objects since they should be converted to a struct value. Arrays, Dates,
* GeoPoints, etc. are not considered to look like JSON objects since they map
* to specific FieldValue types other than ObjectValue.
*/
function looksLikeJsonObject(input) {
return (typeof input === 'object' &&
input !== null &&
!(input instanceof Array) &&
!(input instanceof Date) &&
!(input instanceof Timestamp) &&
!(input instanceof GeoPoint) &&
!(input instanceof Blob) &&
!(input instanceof DocumentReference) &&
!(input instanceof FieldValueImpl));
}
function validatePlainObject(message, context, input) {
if (!looksLikeJsonObject(input) || !isPlainObject(input)) {
var description = valueDescription(input);
if (description === 'an object') {
// Massage the error if it was an object.
throw context.createError(message + ' a custom object');
}
else {
throw context.createError(message + ' ' + description);
}
}
}
/**
* Helper that calls fromDotSeparatedString() but wraps any error thrown.
*/
function fieldPathFromArgument(methodName, path) {
if (path instanceof FieldPath$1) {
return path._internalPath;
}
else if (typeof path === 'string') {
return fieldPathFromDotSeparatedString(methodName, path);
}
else {
var message = 'Field path arguments must be of type string or FieldPath.';
throw new FirestoreError(Code.INVALID_ARGUMENT, "Function " + methodName + "() called with invalid data. " + message);
}
}
/**
* Wraps fromDotSeparatedString with an error message about the method that
* was thrown.
* @param methodName The publicly visible method name
* @param path The dot-separated string form of a field path which will be split
* on dots.
*/
function fieldPathFromDotSeparatedString(methodName, path) {
try {
return fromDotSeparatedString(path)._internalPath;
}
catch (e) {
var message = errorMessage(e);
throw new FirestoreError(Code.INVALID_ARGUMENT, "Function " + methodName + "() called with invalid data. " + message);
}
}
/**
* Extracts the message from a caught exception, which should be an Error object
* though JS doesn't guarantee that.
*/
function errorMessage(error) {
return error instanceof Error ? error.message : error.toString();
}
/** Checks `haystack` if FieldPath `needle` is present. Runs in O(n). */
function fieldMaskContains(haystack, needle) {
return haystack.some(function (v) { return v.isEqual(needle); });
}
/**
* @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.
*/
var ExistenceFilter = /** @class */ (function () {
// TODO(b/33078163): just use simplest form of existence filter for now
function ExistenceFilter(count) {
this.count = count;
}
return ExistenceFilter;
}());
/**
* @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.
*/
var DIRECTIONS = (function () {
var dirs = {};
dirs["asc" /* ASCENDING */] = 'ASCENDING';
dirs["desc" /* DESCENDING */] = 'DESCENDING';
return dirs;
})();
var OPERATORS = (function () {
var ops = {};
ops["<" /* LESS_THAN */] = 'LESS_THAN';
ops["<=" /* LESS_THAN_OR_EQUAL */] = 'LESS_THAN_OR_EQUAL';
ops[">" /* GREATER_THAN */] = 'GREATER_THAN';
ops[">=" /* GREATER_THAN_OR_EQUAL */] = 'GREATER_THAN_OR_EQUAL';
ops["==" /* EQUAL */] = 'EQUAL';
ops["array-contains" /* ARRAY_CONTAINS */] = 'ARRAY_CONTAINS';
ops["in" /* IN */] = 'IN';
ops["array-contains-any" /* ARRAY_CONTAINS_ANY */] = 'ARRAY_CONTAINS_ANY';
return ops;
})();
function assertPresent(value, description) {
debugAssert(!isNullOrUndefined(value), description + ' is missing');
}
/**
* Generates JsonObject values for the Datastore API suitable for sending to
* either GRPC stub methods or via the JSON/HTTP REST API.
* TODO(klimt): We can remove the databaseId argument if we keep the full
* resource name in documents.
*/
var JsonProtoSerializer = /** @class */ (function () {
function JsonProtoSerializer(databaseId, options) {
this.databaseId = databaseId;
this.options = options;
}
JsonProtoSerializer.prototype.fromRpcStatus = function (status) {
var code = status.code === undefined
? Code.UNKNOWN
: mapCodeFromRpcCode(status.code);
return new FirestoreError(code, status.message || '');
};
/**
* Returns a value for a number (or null) that's appropriate to put into
* a google.protobuf.Int32Value proto.
* DO NOT USE THIS FOR ANYTHING ELSE.
* This method cheats. It's typed as returning "number" because that's what
* our generated proto interfaces say Int32Value must be. But GRPC actually
* expects a { value: <number> } struct.
*/
JsonProtoSerializer.prototype.toInt32Proto = function (val) {
if (this.options.useProto3Json || isNullOrUndefined(val)) {
return val;
}
else {
return { value: val };
}
};
/**
* Returns a number (or null) from a google.protobuf.Int32Value proto.
*/
JsonProtoSerializer.prototype.fromInt32Proto = function (val) {
var result;
if (typeof val === 'object') {
result = val.value;
}
else {
result = val;
}
return isNullOrUndefined(result) ? null : result;
};
/**
* Returns an IntegerValue for `value`.
*/
JsonProtoSerializer.prototype.toInteger = function (value) {
return { integerValue: '' + value };
};
/**
* Returns an DoubleValue for `value` that is encoded based the serializer's
* `useProto3Json` setting.
*/
JsonProtoSerializer.prototype.toDouble = function (value) {
if (this.options.useProto3Json) {
if (isNaN(value)) {
return { doubleValue: 'NaN' };
}
else if (value === Infinity) {
return { doubleValue: 'Infinity' };
}
else if (value === -Infinity) {
return { doubleValue: '-Infinity' };
}
}
return { doubleValue: isNegativeZero(value) ? '-0' : value };
};
/**
* Returns a value for a number that's appropriate to put into a proto.
* The return value is an IntegerValue if it can safely represent the value,
* otherwise a DoubleValue is returned.
*/
JsonProtoSerializer.prototype.toNumber = function (value) {
return isSafeInteger(value) ? this.toInteger(value) : this.toDouble(value);
};
/**
* Returns a value for a Date that's appropriate to put into a proto.
*/
JsonProtoSerializer.prototype.toTimestamp = function (timestamp) {
if (this.options.useProto3Json) {
// Serialize to ISO-8601 date format, but with full nano resolution.
// Since JS Date has only millis, let's only use it for the seconds and
// then manually add the fractions to the end.
var jsDateStr = new Date(timestamp.seconds * 1000).toISOString();
// Remove .xxx frac part and Z in the end.
var strUntilSeconds = jsDateStr.replace(/\.\d*/, '').replace('Z', '');
// Pad the fraction out to 9 digits (nanos).
var nanoStr = ('000000000' + timestamp.nanoseconds).slice(-9);
return strUntilSeconds + "." + nanoStr + "Z";
}
else {
return {
seconds: '' + timestamp.seconds,
nanos: timestamp.nanoseconds
// eslint-disable-next-line @typescript-eslint/no-explicit-any
};
}
};
JsonProtoSerializer.prototype.fromTimestamp = function (date) {
var timestamp = normalizeTimestamp(date);
return new Timestamp(timestamp.seconds, timestamp.nanos);
};
/**
* Returns a value for bytes that's appropriate to put in a proto.
*
* Visible for testing.
*/
JsonProtoSerializer.prototype.toBytes = function (bytes) {
if (this.options.useProto3Json) {
return bytes.toBase64();
}
else {
return bytes.toUint8Array();
}
};
/**
* Returns a ByteString based on the proto string value.
*/
JsonProtoSerializer.prototype.fromBytes = function (value) {
if (this.options.useProto3Json) {
hardAssert(value === undefined || typeof value === 'string', 'value must be undefined or a string when using proto3 Json');
return ByteString.fromBase64String(value ? value : '');
}
else {
hardAssert(value === undefined || value instanceof Uint8Array, 'value must be undefined or Uint8Array');
return ByteString.fromUint8Array(value ? value : new Uint8Array());
}
};
JsonProtoSerializer.prototype.toVersion = function (version) {
return this.toTimestamp(version.toTimestamp());
};
JsonProtoSerializer.prototype.fromVersion = function (version) {
hardAssert(!!version, "Trying to deserialize version that isn't set");
return SnapshotVersion.fromTimestamp(this.fromTimestamp(version));
};
JsonProtoSerializer.prototype.toResourceName = function (path, databaseId) {
return this.fullyQualifiedPrefixPath(databaseId || this.databaseId)
.child('documents')
.child(path)
.canonicalString();
};
JsonProtoSerializer.prototype.fromResourceName = function (name) {
var resource = ResourcePath.fromString(name);
hardAssert(isValidResourceName(resource), 'Tried to deserialize invalid key ' + resource.toString());
return resource;
};
JsonProtoSerializer.prototype.toName = function (key) {
return this.toResourceName(key.path);
};
JsonProtoSerializer.prototype.fromName = function (name) {
var resource = this.fromResourceName(name);
hardAssert(resource.get(1) === this.databaseId.projectId, 'Tried to deserialize key from different project: ' +
resource.get(1) +
' vs ' +
this.databaseId.projectId);
hardAssert((!resource.get(3) && !this.databaseId.database) ||
resource.get(3) === this.databaseId.database, 'Tried to deserialize key from different database: ' +
resource.get(3) +
' vs ' +
this.databaseId.database);
return new DocumentKey(this.extractLocalPathFromResourceName(resource));
};
JsonProtoSerializer.prototype.toQueryPath = function (path) {
return this.toResourceName(path);
};
JsonProtoSerializer.prototype.fromQueryPath = function (name) {
var resourceName = this.fromResourceName(name);
// In v1beta1 queries for collections at the root did not have a trailing
// "/documents". In v1 all resource paths contain "/documents". Preserve the
// ability to read the v1beta1 form for compatibility with queries persisted
// in the local target cache.
if (resourceName.length === 4) {
return ResourcePath.EMPTY_PATH;
}
return this.extractLocalPathFromResourceName(resourceName);
};
Object.defineProperty(JsonProtoSerializer.prototype, "encodedDatabaseId", {
get: function () {
var path = new ResourcePath([
'projects',
this.databaseId.projectId,
'databases',
this.databaseId.database
]);
return path.canonicalString();
},
enumerable: true,
configurable: true
});
JsonProtoSerializer.prototype.fullyQualifiedPrefixPath = function (databaseId) {
return new ResourcePath([
'projects',
databaseId.projectId,
'databases',
databaseId.database
]);
};
JsonProtoSerializer.prototype.extractLocalPathFromResourceName = function (resourceName) {
hardAssert(resourceName.length > 4 && resourceName.get(4) === 'documents', 'tried to deserialize invalid key ' + resourceName.toString());
return resourceName.popFirst(5);
};
/** Creates an api.Document from key and fields (but no create/update time) */
JsonProtoSerializer.prototype.toMutationDocument = function (key, fields) {
return {
name: this.toName(key),
fields: fields.proto.mapValue.fields
};
};
JsonProtoSerializer.prototype.toDocument = function (document) {
debugAssert(!document.hasLocalMutations, "Can't serialize documents with mutations.");
return {
name: this.toName(document.key),
fields: document.toProto().mapValue.fields,
updateTime: this.toTimestamp(document.version.toTimestamp())
};
};
JsonProtoSerializer.prototype.fromDocument = function (document, hasCommittedMutations) {
var key = this.fromName(document.name);
var version = this.fromVersion(document.updateTime);
var data = new ObjectValue({ mapValue: { fields: document.fields } });
return new Document(key, version, data, {
hasCommittedMutations: !!hasCommittedMutations
});
};
JsonProtoSerializer.prototype.fromFound = function (doc) {
hardAssert(!!doc.found, 'Tried to deserialize a found document from a missing document.');
assertPresent(doc.found.name, 'doc.found.name');
assertPresent(doc.found.updateTime, 'doc.found.updateTime');
var key = this.fromName(doc.found.name);
var version = this.fromVersion(doc.found.updateTime);
var data = new ObjectValue({ mapValue: { fields: doc.found.fields } });
return new Document(key, version, data, {});
};
JsonProtoSerializer.prototype.fromMissing = function (result) {
hardAssert(!!result.missing, 'Tried to deserialize a missing document from a found document.');
hardAssert(!!result.readTime, 'Tried to deserialize a missing document without a read time.');
var key = this.fromName(result.missing);
var version = this.fromVersion(result.readTime);
return new NoDocument(key, version);
};
JsonProtoSerializer.prototype.fromMaybeDocument = function (result) {
if ('found' in result) {
return this.fromFound(result);
}
else if ('missing' in result) {
return this.fromMissing(result);
}
return fail('invalid batch get response: ' + JSON.stringify(result));
};
JsonProtoSerializer.prototype.fromWatchChange = function (change) {
var watchChange;
if ('targetChange' in change) {
assertPresent(change.targetChange, 'targetChange');
// proto3 default value is unset in JSON (undefined), so use 'NO_CHANGE'
// if unset
var state = this.fromWatchTargetChangeState(change.targetChange.targetChangeType || 'NO_CHANGE');
var targetIds = change.targetChange.targetIds || [];
var resumeToken = this.fromBytes(change.targetChange.resumeToken);
var causeProto = change.targetChange.cause;
var cause = causeProto && this.fromRpcStatus(causeProto);
watchChange = new WatchTargetChange(state, targetIds, resumeToken, cause || null);
}
else if ('documentChange' in change) {
assertPresent(change.documentChange, 'documentChange');
var entityChange = change.documentChange;
assertPresent(entityChange.document, 'documentChange.name');
assertPresent(entityChange.document.name, 'documentChange.document.name');
assertPresent(entityChange.document.updateTime, 'documentChange.document.updateTime');
var key = this.fromName(entityChange.document.name);
var version_3 = this.fromVersion(entityChange.document.updateTime);
var data = new ObjectValue({
mapValue: { fields: entityChange.document.fields }
});
var doc = new Document(key, version_3, data, {});
var updatedTargetIds = entityChange.targetIds || [];
var removedTargetIds = entityChange.removedTargetIds || [];
watchChange = new DocumentWatchChange(updatedTargetIds, removedTargetIds, doc.key, doc);
}
else if ('documentDelete' in change) {
assertPresent(change.documentDelete, 'documentDelete');
var docDelete = change.documentDelete;
assertPresent(docDelete.document, 'documentDelete.document');
var key = this.fromName(docDelete.document);
var version_4 = docDelete.readTime
? this.fromVersion(docDelete.readTime)
: SnapshotVersion.min();
var doc = new NoDocument(key, version_4);
var removedTargetIds = docDelete.removedTargetIds || [];
watchChange = new DocumentWatchChange([], removedTargetIds, doc.key, doc);
}
else if ('documentRemove' in change) {
assertPresent(change.documentRemove, 'documentRemove');
var docRemove = change.documentRemove;
assertPresent(docRemove.document, 'documentRemove');
var key = this.fromName(docRemove.document);
var removedTargetIds = docRemove.removedTargetIds || [];
watchChange = new DocumentWatchChange([], removedTargetIds, key, null);
}
else if ('filter' in change) {
// TODO(dimond): implement existence filter parsing with strategy.
assertPresent(change.filter, 'filter');
var filter = change.filter;
assertPresent(filter.targetId, 'filter.targetId');
var count = filter.count || 0;
var existenceFilter = new ExistenceFilter(count);
var targetId = filter.targetId;
watchChange = new ExistenceFilterChange(targetId, existenceFilter);
}
else {
return fail('Unknown change type ' + JSON.stringify(change));
}
return watchChange;
};
JsonProtoSerializer.prototype.fromWatchTargetChangeState = function (state) {
if (state === 'NO_CHANGE') {
return 0 /* NoChange */;
}
else if (state === 'ADD') {
return 1 /* Added */;
}
else if (state === 'REMOVE') {
return 2 /* Removed */;
}
else if (state === 'CURRENT') {
return 3 /* Current */;
}
else if (state === 'RESET') {
return 4 /* Reset */;
}
else {
return fail('Got unexpected TargetChange.state: ' + state);
}
};
JsonProtoSerializer.prototype.versionFromListenResponse = function (change) {
// We have only reached a consistent snapshot for the entire stream if there
// is a read_time set and it applies to all targets (i.e. the list of
// targets is empty). The backend is guaranteed to send such responses.
if (!('targetChange' in change)) {
return SnapshotVersion.min();
}
var targetChange = change.targetChange;
if (targetChange.targetIds && targetChange.targetIds.length) {
return SnapshotVersion.min();
}
if (!targetChange.readTime) {
return SnapshotVersion.min();
}
return this.fromVersion(targetChange.readTime);
};
JsonProtoSerializer.prototype.toMutation = function (mutation) {
var _this = this;
var result;
if (mutation instanceof SetMutation) {
result = {
update: this.toMutationDocument(mutation.key, mutation.value)
};
}
else if (mutation instanceof DeleteMutation) {
result = { delete: this.toName(mutation.key) };
}
else if (mutation instanceof PatchMutation) {
result = {
update: this.toMutationDocument(mutation.key, mutation.data),
updateMask: this.toDocumentMask(mutation.fieldMask)
};
}
else if (mutation instanceof TransformMutation) {
result = {
transform: {
document: this.toName(mutation.key),
fieldTransforms: mutation.fieldTransforms.map(function (transform) { return _this.toFieldTransform(transform); })
}
};
}
else if (mutation instanceof VerifyMutation) {
result = {
verify: this.toName(mutation.key)
};
}
else {
return fail('Unknown mutation type ' + mutation.type);
}
if (!mutation.precondition.isNone) {
result.currentDocument = this.toPrecondition(mutation.precondition);
}
return result;
};
JsonProtoSerializer.prototype.fromMutation = function (proto) {
var _this = this;
var precondition = proto.currentDocument
? this.fromPrecondition(proto.currentDocument)
: Precondition.none();
if (proto.update) {
assertPresent(proto.update.name, 'name');
var key = this.fromName(proto.update.name);
var value = new ObjectValue({
mapValue: { fields: proto.update.fields }
});
if (proto.updateMask) {
var fieldMask = this.fromDocumentMask(proto.updateMask);
return new PatchMutation(key, value, fieldMask, precondition);
}
else {
return new SetMutation(key, value, precondition);
}
}
else if (proto.delete) {
var key = this.fromName(proto.delete);
return new DeleteMutation(key, precondition);
}
else if (proto.transform) {
var key = this.fromName(proto.transform.document);
var fieldTransforms = proto.transform.fieldTransforms.map(function (transform) { return _this.fromFieldTransform(transform); });
hardAssert(precondition.exists === true, 'Transforms only support precondition "exists == true"');
return new TransformMutation(key, fieldTransforms);
}
else if (proto.verify) {
var key = this.fromName(proto.verify);
return new VerifyMutation(key, precondition);
}
else {
return fail('unknown mutation proto: ' + JSON.stringify(proto));
}
};
JsonProtoSerializer.prototype.toPrecondition = function (precondition) {
debugAssert(!precondition.isNone, "Can't serialize an empty precondition");
if (precondition.updateTime !== undefined) {
return {
updateTime: this.toVersion(precondition.updateTime)
};
}
else if (precondition.exists !== undefined) {
return { exists: precondition.exists };
}
else {
return fail('Unknown precondition');
}
};
JsonProtoSerializer.prototype.fromPrecondition = function (precondition) {
if (precondition.updateTime !== undefined) {
return Precondition.updateTime(this.fromVersion(precondition.updateTime));
}
else if (precondition.exists !== undefined) {
return Precondition.exists(precondition.exists);
}
else {
return Precondition.none();
}
};
JsonProtoSerializer.prototype.fromWriteResult = function (proto, commitTime) {
// NOTE: Deletes don't have an updateTime.
var version = proto.updateTime
? this.fromVersion(proto.updateTime)
: this.fromVersion(commitTime);
if (version.isEqual(SnapshotVersion.min())) {
// The Firestore Emulator currently returns an update time of 0 for
// deletes of non-existing documents (rather than null). This breaks the
// test "get deleted doc while offline with source=cache" as NoDocuments
// with version 0 are filtered by IndexedDb's RemoteDocumentCache.
// TODO(#2149): Remove this when Emulator is fixed
version = this.fromVersion(commitTime);
}
var transformResults = null;
if (proto.transformResults && proto.transformResults.length > 0) {
transformResults = proto.transformResults;
}
return new MutationResult(version, transformResults);
};
JsonProtoSerializer.prototype.fromWriteResults = function (protos, commitTime) {
var _this = this;
if (protos && protos.length > 0) {
hardAssert(commitTime !== undefined, 'Received a write result without a commit time');
return protos.map(function (proto) { return _this.fromWriteResult(proto, commitTime); });
}
else {
return [];
}
};
JsonProtoSerializer.prototype.toFieldTransform = function (fieldTransform) {
var transform = fieldTransform.transform;
if (transform instanceof ServerTimestampTransform) {
return {
fieldPath: fieldTransform.field.canonicalString(),
setToServerValue: 'REQUEST_TIME'
};
}
else if (transform instanceof ArrayUnionTransformOperation) {
return {
fieldPath: fieldTransform.field.canonicalString(),
appendMissingElements: {
values: transform.elements
}
};
}
else if (transform instanceof ArrayRemoveTransformOperation) {
return {
fieldPath: fieldTransform.field.canonicalString(),
removeAllFromArray: {
values: transform.elements
}
};
}
else if (transform instanceof NumericIncrementTransformOperation) {
return {
fieldPath: fieldTransform.field.canonicalString(),
increment: transform.operand
};
}
else {
throw fail('Unknown transform: ' + fieldTransform.transform);
}
};
JsonProtoSerializer.prototype.fromFieldTransform = function (proto) {
var transform = null;
if ('setToServerValue' in proto) {
hardAssert(proto.setToServerValue === 'REQUEST_TIME', 'Unknown server value transform proto: ' + JSON.stringify(proto));
transform = ServerTimestampTransform.instance;
}
else if ('appendMissingElements' in proto) {
var values = proto.appendMissingElements.values || [];
transform = new ArrayUnionTransformOperation(values);
}
else if ('removeAllFromArray' in proto) {
var values = proto.removeAllFromArray.values || [];
transform = new ArrayRemoveTransformOperation(values);
}
else if ('increment' in proto) {
transform = new NumericIncrementTransformOperation(this, proto.increment);
}
else {
fail('Unknown transform proto: ' + JSON.stringify(proto));
}
var fieldPath = FieldPath.fromServerFormat(proto.fieldPath);
return new FieldTransform(fieldPath, transform);
};
JsonProtoSerializer.prototype.toDocumentsTarget = function (target) {
return { documents: [this.toQueryPath(target.path)] };
};
JsonProtoSerializer.prototype.fromDocumentsTarget = function (documentsTarget) {
var count = documentsTarget.documents.length;
hardAssert(count === 1, 'DocumentsTarget contained other than 1 document: ' + count);
var name = documentsTarget.documents[0];
return Query.atPath(this.fromQueryPath(name)).toTarget();
};
JsonProtoSerializer.prototype.toQueryTarget = function (target) {
// Dissect the path into parent, collectionId, and optional key filter.
var result = { structuredQuery: {} };
var path = target.path;
if (target.collectionGroup !== null) {
debugAssert(path.length % 2 === 0, 'Collection Group queries should be within a document path or root.');
result.parent = this.toQueryPath(path);
result.structuredQuery.from = [
{
collectionId: target.collectionGroup,
allDescendants: true
}
];
}
else {
debugAssert(path.length % 2 !== 0, 'Document queries with filters are not supported.');
result.parent = this.toQueryPath(path.popLast());
result.structuredQuery.from = [{ collectionId: path.lastSegment() }];
}
var where = this.toFilter(target.filters);
if (where) {
result.structuredQuery.where = where;
}
var orderBy = this.toOrder(target.orderBy);
if (orderBy) {
result.structuredQuery.orderBy = orderBy;
}
var limit = this.toInt32Proto(target.limit);
if (limit !== null) {
result.structuredQuery.limit = limit;
}
if (target.startAt) {
result.structuredQuery.startAt = this.toCursor(target.startAt);
}
if (target.endAt) {
result.structuredQuery.endAt = this.toCursor(target.endAt);
}
return result;
};
JsonProtoSerializer.prototype.fromQueryTarget = function (target) {
var path = this.fromQueryPath(target.parent);
var query = target.structuredQuery;
var fromCount = query.from ? query.from.length : 0;
var collectionGroup = null;
if (fromCount > 0) {
hardAssert(fromCount === 1, 'StructuredQuery.from with more than one collection is not supported.');
var from = query.from[0];
if (from.allDescendants) {
collectionGroup = from.collectionId;
}
else {
path = path.child(from.collectionId);
}
}
var filterBy = [];
if (query.where) {
filterBy = this.fromFilter(query.where);
}
var orderBy = [];
if (query.orderBy) {
orderBy = this.fromOrder(query.orderBy);
}
var limit = null;
if (query.limit) {
limit = this.fromInt32Proto(query.limit);
}
var startAt = null;
if (query.startAt) {
startAt = this.fromCursor(query.startAt);
}
var endAt = null;
if (query.endAt) {
endAt = this.fromCursor(query.endAt);
}
return new Query(path, collectionGroup, orderBy, filterBy, limit, "F" /* First */, startAt, endAt).toTarget();
};
JsonProtoSerializer.prototype.toListenRequestLabels = function (targetData) {
var value = this.toLabel(targetData.purpose);
if (value == null) {
return null;
}
else {
return {
'goog-listen-tags': value
};
}
};
JsonProtoSerializer.prototype.toLabel = function (purpose) {
switch (purpose) {
case 0 /* Listen */:
return null;
case 1 /* ExistenceFilterMismatch */:
return 'existence-filter-mismatch';
case 2 /* LimboResolution */:
return 'limbo-document';
default:
return fail('Unrecognized query purpose: ' + purpose);
}
};
JsonProtoSerializer.prototype.toTarget = function (targetData) {
var result;
var target = targetData.target;
if (target.isDocumentQuery()) {
result = { documents: this.toDocumentsTarget(target) };
}
else {
result = { query: this.toQueryTarget(target) };
}
result.targetId = targetData.targetId;
if (targetData.resumeToken.approximateByteSize() > 0) {
result.resumeToken = this.toBytes(targetData.resumeToken);
}
return result;
};
JsonProtoSerializer.prototype.toFilter = function (filters) {
var _this = this;
if (filters.length === 0) {
return;
}
var protos = filters.map(function (filter) {
if (filter instanceof FieldFilter) {
return _this.toUnaryOrFieldFilter(filter);
}
else {
return fail('Unrecognized filter: ' + JSON.stringify(filter));
}
});
if (protos.length === 1) {
return protos[0];
}
return { compositeFilter: { op: 'AND', filters: protos } };
};
JsonProtoSerializer.prototype.fromFilter = function (filter) {
var _this = this;
if (!filter) {
return [];
}
else if (filter.unaryFilter !== undefined) {
return [this.fromUnaryFilter(filter)];
}
else if (filter.fieldFilter !== undefined) {
return [this.fromFieldFilter(filter)];
}
else if (filter.compositeFilter !== undefined) {
return filter.compositeFilter
.filters.map(function (f) { return _this.fromFilter(f); })
.reduce(function (accum, current) { return accum.concat(current); });
}
else {
return fail('Unknown filter: ' + JSON.stringify(filter));
}
};
JsonProtoSerializer.prototype.toOrder = function (orderBys) {
var _this = this;
if (orderBys.length === 0) {
return;
}
return orderBys.map(function (order) { return _this.toPropertyOrder(order); });
};
JsonProtoSerializer.prototype.fromOrder = function (orderBys) {
var _this = this;
return orderBys.map(function (order) { return _this.fromPropertyOrder(order); });
};
JsonProtoSerializer.prototype.toCursor = function (cursor) {
return {
before: cursor.before,
values: cursor.position
};
};
JsonProtoSerializer.prototype.fromCursor = function (cursor) {
var before = !!cursor.before;
var position = cursor.values || [];
return new Bound(position, before);
};
// visible for testing
JsonProtoSerializer.prototype.toDirection = function (dir) {
return DIRECTIONS[dir];
};
// visible for testing
JsonProtoSerializer.prototype.fromDirection = function (dir) {
switch (dir) {
case 'ASCENDING':
return "asc" /* ASCENDING */;
case 'DESCENDING':
return "desc" /* DESCENDING */;
default:
return undefined;
}
};
// visible for testing
JsonProtoSerializer.prototype.toOperatorName = function (op) {
return OPERATORS[op];
};
JsonProtoSerializer.prototype.fromOperatorName = function (op) {
switch (op) {
case 'EQUAL':
return "==" /* EQUAL */;
case 'GREATER_THAN':
return ">" /* GREATER_THAN */;
case 'GREATER_THAN_OR_EQUAL':
return ">=" /* GREATER_THAN_OR_EQUAL */;
case 'LESS_THAN':
return "<" /* LESS_THAN */;
case 'LESS_THAN_OR_EQUAL':
return "<=" /* LESS_THAN_OR_EQUAL */;
case 'ARRAY_CONTAINS':
return "array-contains" /* ARRAY_CONTAINS */;
case 'IN':
return "in" /* IN */;
case 'ARRAY_CONTAINS_ANY':
return "array-contains-any" /* ARRAY_CONTAINS_ANY */;
case 'OPERATOR_UNSPECIFIED':
return fail('Unspecified operator');
default:
return fail('Unknown operator');
}
};
JsonProtoSerializer.prototype.toFieldPathReference = function (path) {
return { fieldPath: path.canonicalString() };
};
JsonProtoSerializer.prototype.fromFieldPathReference = function (fieldReference) {
return FieldPath.fromServerFormat(fieldReference.fieldPath);
};
// visible for testing
JsonProtoSerializer.prototype.toPropertyOrder = function (orderBy) {
return {
field: this.toFieldPathReference(orderBy.field),
direction: this.toDirection(orderBy.dir)
};
};
JsonProtoSerializer.prototype.fromPropertyOrder = function (orderBy) {
return new OrderBy(this.fromFieldPathReference(orderBy.field), this.fromDirection(orderBy.direction));
};
JsonProtoSerializer.prototype.fromFieldFilter = function (filter) {
return FieldFilter.create(this.fromFieldPathReference(filter.fieldFilter.field), this.fromOperatorName(filter.fieldFilter.op), filter.fieldFilter.value);
};
// visible for testing
JsonProtoSerializer.prototype.toUnaryOrFieldFilter = function (filter) {
if (filter.op === "==" /* EQUAL */) {
if (isNanValue(filter.value)) {
return {
unaryFilter: {
field: this.toFieldPathReference(filter.field),
op: 'IS_NAN'
}
};
}
else if (isNullValue(filter.value)) {
return {
unaryFilter: {
field: this.toFieldPathReference(filter.field),
op: 'IS_NULL'
}
};
}
}
return {
fieldFilter: {
field: this.toFieldPathReference(filter.field),
op: this.toOperatorName(filter.op),
value: filter.value
}
};
};
JsonProtoSerializer.prototype.fromUnaryFilter = function (filter) {
switch (filter.unaryFilter.op) {
case 'IS_NAN':
var nanField = this.fromFieldPathReference(filter.unaryFilter.field);
return FieldFilter.create(nanField, "==" /* EQUAL */, {
doubleValue: NaN
});
case 'IS_NULL':
var nullField = this.fromFieldPathReference(filter.unaryFilter.field);
return FieldFilter.create(nullField, "==" /* EQUAL */, {
nullValue: 'NULL_VALUE'
});
case 'OPERATOR_UNSPECIFIED':
return fail('Unspecified filter');
default:
return fail('Unknown filter');
}
};
JsonProtoSerializer.prototype.toDocumentMask = function (fieldMask) {
var canonicalFields = [];
fieldMask.fields.forEach(function (field) { return canonicalFields.push(field.canonicalString()); });
return {
fieldPaths: canonicalFields
};
};
JsonProtoSerializer.prototype.fromDocumentMask = function (proto) {
var paths = proto.fieldPaths || [];
return new FieldMask(paths.map(function (path) { return FieldPath.fromServerFormat(path); }));
};
return JsonProtoSerializer;
}());
function isValidResourceName(path) {
// Resource names have at least 4 components (project ID, database ID)
return (path.length >= 4 &&
path.get(0) === 'projects' &&
path.get(2) === 'databases');
}
/**
* @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.
*/
/**
* Converts Firestore's internal types to the JavaScript types that we expose
* to the user.
*/
var UserDataWriter = /** @class */ (function () {
function UserDataWriter(firestore, timestampsInSnapshots, serverTimestampBehavior, converter) {
this.firestore = firestore;
this.timestampsInSnapshots = timestampsInSnapshots;
this.serverTimestampBehavior = serverTimestampBehavior;
this.converter = converter;
}
UserDataWriter.prototype.convertValue = function (value) {
switch (typeOrder(value)) {
case 0 /* NullValue */:
return null;
case 1 /* BooleanValue */:
return value.booleanValue;
case 2 /* NumberValue */:
return normalizeNumber(value.integerValue || value.doubleValue);
case 3 /* TimestampValue */:
return this.convertTimestamp(value.timestampValue);
case 4 /* ServerTimestampValue */:
return this.convertServerTimestamp(value);
case 5 /* StringValue */:
return value.stringValue;
case 6 /* BlobValue */:
return new Blob(normalizeByteString(value.bytesValue));
case 7 /* RefValue */:
return this.convertReference(value.referenceValue);
case 8 /* GeoPointValue */:
return this.convertGeoPoint(value.geoPointValue);
case 9 /* ArrayValue */:
return this.convertArray(value.arrayValue);
case 10 /* ObjectValue */:
return this.convertObject(value.mapValue);
default:
throw fail('Invalid value type: ' + JSON.stringify(value));
}
};
UserDataWriter.prototype.convertObject = function (mapValue) {
var _this = this;
var result = {};
forEach(mapValue.fields || {}, function (key, value) {
result[key] = _this.convertValue(value);
});
return result;
};
UserDataWriter.prototype.convertGeoPoint = function (value) {
return new GeoPoint(normalizeNumber(value.latitude), normalizeNumber(value.longitude));
};
UserDataWriter.prototype.convertArray = function (arrayValue) {
var _this = this;
return (arrayValue.values || []).map(function (value) { return _this.convertValue(value); });
};
UserDataWriter.prototype.convertServerTimestamp = function (value) {
switch (this.serverTimestampBehavior) {
case 'previous':
var previousValue = getPreviousValue(value);
if (previousValue == null) {
return null;
}
return this.convertValue(previousValue);
case 'estimate':
return this.convertTimestamp(getLocalWriteTime(value));
default:
return null;
}
};
UserDataWriter.prototype.convertTimestamp = function (value) {
var normalizedValue = normalizeTimestamp(value);
var timestamp = new Timestamp(normalizedValue.seconds, normalizedValue.nanos);
if (this.timestampsInSnapshots) {
return timestamp;
}
else {
return timestamp.toDate();
}
};
UserDataWriter.prototype.convertReference = function (name) {
var resourcePath = ResourcePath.fromString(name);
hardAssert(isValidResourceName(resourcePath), 'ReferenceValue is not valid ' + name);
var databaseId = new DatabaseId(resourcePath.get(1), resourcePath.get(3));
var key = new DocumentKey(resourcePath.popFirst(5));
if (!databaseId.isEqual(this.firestore._databaseId)) {
// TODO(b/64130202): Somehow support foreign references.
logError("Document " + key + " contains a document " +
"reference within a different database (" +
(databaseId.projectId + "/" + databaseId.database + ") which is not ") +
"supported. It will be treated as a reference in the current " +
("database (" + this.firestore._databaseId.projectId + "/" + this.firestore._databaseId.database + ") ") +
"instead.");
}
return new DocumentReference(key, this.firestore, this.converter);
};
return UserDataWriter;
}());
/**
* @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.
*/
// settings() defaults:
var DEFAULT_HOST = 'firestore.googleapis.com';
var DEFAULT_SSL = true;
var DEFAULT_TIMESTAMPS_IN_SNAPSHOTS = true;
var DEFAULT_FORCE_LONG_POLLING = false;
var DEFAULT_IGNORE_UNDEFINED_PROPERTIES = false;
/**
* Constant used to indicate the LRU garbage collection should be disabled.
* Set this value as the `cacheSizeBytes` on the settings passed to the
* `Firestore` instance.
*/
var CACHE_SIZE_UNLIMITED = LruParams.COLLECTION_DISABLED;
// enablePersistence() defaults:
var DEFAULT_SYNCHRONIZE_TABS = false;
/**
* A concrete type describing all the values that can be applied via a
* user-supplied firestore.Settings object. This is a separate type so that
* defaults can be supplied and the value can be checked for equality.
*/
var FirestoreSettings = /** @class */ (function () {
function FirestoreSettings(settings) {
var _a, _b, _c, _d;
if (settings.host === undefined) {
if (settings.ssl !== undefined) {
throw new FirestoreError(Code.INVALID_ARGUMENT, "Can't provide ssl option if host option is not set");
}
this.host = DEFAULT_HOST;
this.ssl = DEFAULT_SSL;
}
else {
validateNamedType('settings', 'non-empty string', 'host', settings.host);
this.host = settings.host;
validateNamedOptionalType('settings', 'boolean', 'ssl', settings.ssl);
this.ssl = (_a = settings.ssl) !== null && _a !== void 0 ? _a : DEFAULT_SSL;
}
validateOptionNames('settings', settings, [
'host',
'ssl',
'credentials',
'timestampsInSnapshots',
'cacheSizeBytes',
'experimentalForceLongPolling',
'ignoreUndefinedProperties'
]);
validateNamedOptionalType('settings', 'object', 'credentials', settings.credentials);
this.credentials = settings.credentials;
validateNamedOptionalType('settings', 'boolean', 'timestampsInSnapshots', settings.timestampsInSnapshots);
validateNamedOptionalType('settings', 'boolean', 'ignoreUndefinedProperties', settings.ignoreUndefinedProperties);
// Nobody should set timestampsInSnapshots anymore, but the error depends on
// whether they set it to true or false...
if (settings.timestampsInSnapshots === true) {
logError("The setting 'timestampsInSnapshots: true' is no longer required " +
'and should be removed.');
}
else if (settings.timestampsInSnapshots === false) {
logError("Support for 'timestampsInSnapshots: false' will be removed soon. " +
'You must update your code to handle Timestamp objects.');
}
this.timestampsInSnapshots = (_b = settings.timestampsInSnapshots) !== null && _b !== void 0 ? _b : DEFAULT_TIMESTAMPS_IN_SNAPSHOTS;
this.ignoreUndefinedProperties = (_c = settings.ignoreUndefinedProperties) !== null && _c !== void 0 ? _c : DEFAULT_IGNORE_UNDEFINED_PROPERTIES;
validateNamedOptionalType('settings', 'number', 'cacheSizeBytes', settings.cacheSizeBytes);
if (settings.cacheSizeBytes === undefined) {
this.cacheSizeBytes = LruParams.DEFAULT_CACHE_SIZE_BYTES;
}
else {
if (settings.cacheSizeBytes !== CACHE_SIZE_UNLIMITED &&
settings.cacheSizeBytes < LruParams.MINIMUM_CACHE_SIZE_BYTES) {
throw new FirestoreError(Code.INVALID_ARGUMENT, "cacheSizeBytes must be at least " + LruParams.MINIMUM_CACHE_SIZE_BYTES);
}
else {
this.cacheSizeBytes = settings.cacheSizeBytes;
}
}
validateNamedOptionalType('settings', 'boolean', 'experimentalForceLongPolling', settings.experimentalForceLongPolling);
this.forceLongPolling = (_d = settings.experimentalForceLongPolling) !== null && _d !== void 0 ? _d : DEFAULT_FORCE_LONG_POLLING;
}
FirestoreSettings.prototype.isEqual = function (other) {
return (this.host === other.host &&
this.ssl === other.ssl &&
this.timestampsInSnapshots === other.timestampsInSnapshots &&
this.credentials === other.credentials &&
this.cacheSizeBytes === other.cacheSizeBytes &&
this.forceLongPolling === other.forceLongPolling &&
this.ignoreUndefinedProperties === other.ignoreUndefinedProperties);
};
return FirestoreSettings;
}());
/**
* The root reference to the database.
*/
var Firestore = /** @class */ (function () {
// Note: We are using `MemoryComponentProvider` as a default
// ComponentProvider to ensure backwards compatibility with the format
// expected by the console build.
function Firestore(databaseIdOrApp, authProvider, componentProvider) {
var _this = this;
if (componentProvider === void 0) { componentProvider = new MemoryComponentProvider(); }
this._firebaseApp = null;
// Public for use in tests.
// TODO(mikelehen): Use modularized initialization instead.
this._queue = new AsyncQueue();
this.INTERNAL = {
delete: function () { return tslib.__awaiter(_this, void 0, void 0, function () {
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
// The client must be initalized to ensure that all subsequent API usage
// throws an exception.
this.ensureClientConfigured();
return [4 /*yield*/, this._firestoreClient.terminate()];
case 1:
_e.sent();
return [2 /*return*/];
}
});
}); }
};
if (typeof databaseIdOrApp.options === 'object') {
// This is very likely a Firebase app object
// TODO(b/34177605): Can we somehow use instanceof?
var app = databaseIdOrApp;
this._firebaseApp = app;
this._databaseId = Firestore.databaseIdFromApp(app);
this._persistenceKey = app.name;
this._credentials = new FirebaseCredentialsProvider(authProvider);
}
else {
var external_1 = databaseIdOrApp;
if (!external_1.projectId) {
throw new FirestoreError(Code.INVALID_ARGUMENT, 'Must provide projectId');
}
this._databaseId = new DatabaseId(external_1.projectId, external_1.database);
// Use a default persistenceKey that lines up with FirebaseApp.
this._persistenceKey = '[DEFAULT]';
this._credentials = new EmptyCredentialsProvider();
}
this._componentProvider = componentProvider;
this._settings = new FirestoreSettings({});
}
Object.defineProperty(Firestore.prototype, "_dataReader", {
get: function () {
debugAssert(!!this._firestoreClient, 'Cannot obtain UserDataReader before instance is intitialized');
if (!this._userDataReader) {
// Lazy initialize UserDataReader once the settings are frozen
this._userDataReader = new UserDataReader(this._databaseId, this._settings.ignoreUndefinedProperties);
}
return this._userDataReader;
},
enumerable: true,
configurable: true
});
Firestore.prototype.settings = function (settingsLiteral) {
validateExactNumberOfArgs('Firestore.settings', arguments, 1);
validateArgType('Firestore.settings', 'object', 1, settingsLiteral);
var newSettings = new FirestoreSettings(settingsLiteral);
if (this._firestoreClient && !this._settings.isEqual(newSettings)) {
throw new FirestoreError(Code.FAILED_PRECONDITION, 'Firestore has already been started and its settings can no longer ' +
'be changed. You can only call settings() before calling any other ' +
'methods on a Firestore object.');
}
this._settings = newSettings;
if (newSettings.credentials !== undefined) {
this._credentials = makeCredentialsProvider(newSettings.credentials);
}
};
Firestore.prototype.enableNetwork = function () {
this.ensureClientConfigured();
return this._firestoreClient.enableNetwork();
};
Firestore.prototype.disableNetwork = function () {
this.ensureClientConfigured();
return this._firestoreClient.disableNetwork();
};
Firestore.prototype.enablePersistence = function (settings) {
var _a, _b;
if (this._firestoreClient) {
throw new FirestoreError(Code.FAILED_PRECONDITION, 'Firestore has already been started and persistence can no longer ' +
'be enabled. You can only call enablePersistence() before calling ' +
'any other methods on a Firestore object.');
}
var synchronizeTabs = false;
if (settings) {
if (settings.experimentalTabSynchronization !== undefined) {
logError("The 'experimentalTabSynchronization' setting will be removed. Use 'synchronizeTabs' instead.");
}
synchronizeTabs = (_b = (_a = settings.synchronizeTabs) !== null && _a !== void 0 ? _a : settings.experimentalTabSynchronization) !== null && _b !== void 0 ? _b : DEFAULT_SYNCHRONIZE_TABS;
}
return this.configureClient(this._componentProvider, {
durable: true,
cacheSizeBytes: this._settings.cacheSizeBytes,
synchronizeTabs: synchronizeTabs
});
};
Firestore.prototype.clearPersistence = function () {
return tslib.__awaiter(this, void 0, void 0, function () {
var deferred;
var _this = this;
return tslib.__generator(this, function (_e) {
if (this._firestoreClient !== undefined &&
!this._firestoreClient.clientTerminated) {
throw new FirestoreError(Code.FAILED_PRECONDITION, 'Persistence cannot be cleared after this Firestore instance is initialized.');
}
deferred = new Deferred();
this._queue.enqueueAndForgetEvenAfterShutdown(function () { return tslib.__awaiter(_this, void 0, void 0, function () {
var databaseInfo, e_11;
return tslib.__generator(this, function (_e) {
switch (_e.label) {
case 0:
_e.trys.push([0, 2, , 3]);
databaseInfo = this.makeDatabaseInfo();
return [4 /*yield*/, this._componentProvider.clearPersistence(databaseInfo)];
case 1:
_e.sent();
deferred.resolve();
return [3 /*break*/, 3];
case 2:
e_11 = _e.sent();
deferred.reject(e_11);
return [3 /*break*/, 3];
case 3: return [2 /*return*/];
}
});
}); });
return [2 /*return*/, deferred.promise];
});
});
};
Firestore.prototype.terminate = function () {
this.app._removeServiceInstance('firestore');
return this.INTERNAL.delete();
};
Object.defineProperty(Firestore.prototype, "_isTerminated", {
get: function () {
this.ensureClientConfigured();
return this._firestoreClient.clientTerminated;
},
enumerable: true,
configurable: true
});
Firestore.prototype.waitForPendingWrites = function () {
this.ensureClientConfigured();
return this._firestoreClient.waitForPendingWrites();
};
Firestore.prototype.onSnapshotsInSync = function (arg) {
this.ensureClientConfigured();
if (isPartialObserver(arg)) {
return this.onSnapshotsInSyncInternal(arg);
}
else {
validateArgType('Firestore.onSnapshotsInSync', 'function', 1, arg);
var observer = {
next: arg
};
return this.onSnapshotsInSyncInternal(observer);
}
};
Firestore.prototype.onSnapshotsInSyncInternal = function (observer) {
var _this = this;
var errHandler = function (err) {
throw fail('Uncaught Error in onSnapshotsInSync');
};
var asyncObserver = new AsyncObserver({
next: function () {
if (observer.next) {
observer.next();
}
},
error: errHandler
});
this._firestoreClient.addSnapshotsInSyncListener(asyncObserver);
return function () {
asyncObserver.mute();
_this._firestoreClient.removeSnapshotsInSyncListener(asyncObserver);
};
};
Firestore.prototype.ensureClientConfigured = function () {
if (!this._firestoreClient) {
// Kick off starting the client but don't actually wait for it.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.configureClient(new MemoryComponentProvider(), {
durable: false
});
}
return this._firestoreClient;
};
Firestore.prototype.makeDatabaseInfo = function () {
return new DatabaseInfo(this._databaseId, this._persistenceKey, this._settings.host, this._settings.ssl, this._settings.forceLongPolling);
};
Firestore.prototype.configureClient = function (componentProvider, persistenceSettings) {
debugAssert(!!this._settings.host, 'FirestoreSettings.host is not set');
debugAssert(!this._firestoreClient, 'configureClient() called multiple times');
var databaseInfo = this.makeDatabaseInfo();
this._firestoreClient = new FirestoreClient(PlatformSupport.getPlatform(), databaseInfo, this._credentials, this._queue);
return this._firestoreClient.start(componentProvider, persistenceSettings);
};
Firestore.databaseIdFromApp = function (app) {
if (!contains(app.options, 'projectId')) {
throw new FirestoreError(Code.INVALID_ARGUMENT, '"projectId" not provided in firebase.initializeApp.');
}
var projectId = app.options.projectId;
if (!projectId || typeof projectId !== 'string') {
throw new FirestoreError(Code.INVALID_ARGUMENT, 'projectId must be a string in FirebaseApp.options');
}
return new DatabaseId(projectId);
};
Object.defineProperty(Firestore.prototype, "app", {
get: function () {
if (!this._firebaseApp) {
throw new FirestoreError(Code.FAILED_PRECONDITION, "Firestore was not initialized using the Firebase SDK. 'app' is " +
'not available');
}
return this._firebaseApp;
},
enumerable: true,
configurable: true
});
Firestore.prototype.collection = function (pathString) {
validateExactNumberOfArgs('Firestore.collection', arguments, 1);
validateArgType('Firestore.collection', 'non-empty string', 1, pathString);
this.ensureClientConfigured();
return new CollectionReference(ResourcePath.fromString(pathString), this);
};
Firestore.prototype.doc = function (pathString) {
validateExactNumberOfArgs('Firestore.doc', arguments, 1);
validateArgType('Firestore.doc', 'non-empty string', 1, pathString);
this.ensureClientConfigured();
return DocumentReference.forPath(ResourcePath.fromString(pathString), this);
};
Firestore.prototype.collectionGroup = function (collectionId) {
validateExactNumberOfArgs('Firestore.collectionGroup', arguments, 1);
validateArgType('Firestore.collectionGroup', 'non-empty string', 1, collectionId);
if (collectionId.indexOf('/') >= 0) {
throw new FirestoreError(Code.INVALID_ARGUMENT, "Invalid collection ID '" + collectionId + "' passed to function " +
"Firestore.collectionGroup(). Collection IDs must not contain '/'.");
}
this.ensureClientConfigured();
return new Query$1(new Query(ResourcePath.EMPTY_PATH, collectionId), this);
};
Firestore.prototype.runTransaction = function (updateFunction) {
var _this = this;
validateExactNumberOfArgs('Firestore.runTransaction', arguments, 1);
validateArgType('Firestore.runTransaction', 'function', 1, updateFunction);
return this.ensureClientConfigured().transaction(function (transaction) {
return updateFunction(new Transaction$1(_this, transaction));
});
};
Firestore.prototype.batch = function () {
this.ensureClientConfigured();
return new WriteBatch(this);
};
Object.defineProperty(Firestore, "logLevel", {
get: function () {
switch (getLogLevel()) {
case logger.LogLevel.DEBUG:
return 'debug';
case logger.LogLevel.SILENT:
return 'silent';
default:
// The default log level is error
return 'error';
}
},
enumerable: true,
configurable: true
});
Firestore.setLogLevel = function (level) {
validateExactNumberOfArgs('Firestore.setLogLevel', arguments, 1);
validateArgType('Firestore.setLogLevel', 'non-empty string', 1, level);
switch (level) {
case 'debug':
setLogLevel(logger.LogLevel.DEBUG);
break;
case 'error':
setLogLevel(logger.LogLevel.ERROR);
break;
case 'silent':
setLogLevel(logger.LogLevel.SILENT);
break;
default:
throw new FirestoreError(Code.INVALID_ARGUMENT, 'Invalid log level: ' + level);
}
};
// Note: this is not a property because the minifier can't work correctly with
// the way TypeScript compiler outputs properties.
Firestore.prototype._areTimestampsInSnapshotsEnabled = function () {
return this._settings.timestampsInSnapshots;
};
return Firestore;
}());
/**
* A reference to a transaction.
*/
var Transaction$1 = /** @class */ (function () {
function Transaction$1(_firestore, _transaction) {
this._firestore = _firestore;
this._transaction = _transaction;
}
Transaction$1.prototype.get = function (documentRef) {
var _this = this;
validateExactNumberOfArgs('Transaction.get', arguments, 1);
var ref = validateReference('Transaction.get', documentRef, this._firestore);
return this._transaction
.lookup([ref._key])
.then(function (docs) {
if (!docs || docs.length !== 1) {
return fail('Mismatch in docs returned from document lookup.');
}
var doc = docs[0];
if (doc instanceof NoDocument) {
return new DocumentSnapshot(_this._firestore, ref._key, null,
/* fromCache= */ false,
/* hasPendingWrites= */ false, ref._converter);
}
else if (doc instanceof Document) {
return new DocumentSnapshot(_this._firestore, ref._key, doc,
/* fromCache= */ false,
/* hasPendingWrites= */ false, ref._converter);
}
else {
throw fail("BatchGetDocumentsRequest returned unexpected document type: " + doc.constructor.name);
}
});
};
Transaction$1.prototype.set = function (documentRef, value, options) {
validateBetweenNumberOfArgs('Transaction.set', arguments, 2, 3);
var ref = validateReference('Transaction.set', documentRef, this._firestore);
options = validateSetOptions('Transaction.set', options);
var _e = applyFirestoreDataConverter(ref._converter, value, 'Transaction.set'), convertedValue = _e[0], functionName = _e[1];
var parsed = options.merge || options.mergeFields
? this._firestore._dataReader.parseMergeData(functionName, convertedValue, options.mergeFields)
: this._firestore._dataReader.parseSetData(functionName, convertedValue);
this._transaction.set(ref._key, parsed);
return this;
};
Transaction$1.prototype.update = function (documentRef, fieldOrUpdateData, value) {
var moreFieldsAndValues = [];
for (var _i = 3; _i < arguments.length; _i++) {
moreFieldsAndValues[_i - 3] = arguments[_i];
}
var ref;
var parsed;
if (typeof fieldOrUpdateData === 'string' ||
fieldOrUpdateData instanceof FieldPath$1) {
validateAtLeastNumberOfArgs('Transaction.update', arguments, 3);
ref = validateReference('Transaction.update', documentRef, this._firestore);
parsed = this._firestore._dataReader.parseUpdateVarargs('Transaction.update', fieldOrUpdateData, value, moreFieldsAndValues);
}
else {
validateExactNumberOfArgs('Transaction.update', arguments, 2);
ref = validateReference('Transaction.update', documentRef, this._firestore);
parsed = this._firestore._dataReader.parseUpdateData('Transaction.update', fieldOrUpdateData);
}
this._transaction.update(ref._key, parsed);
return this;
};
Transaction$1.prototype.delete = function (documentRef) {
validateExactNumberOfArgs('Transaction.delete', arguments, 1);
var ref = validateReference('Transaction.delete', documentRef, this._firestore);
this._transaction.delete(ref._key);
return this;
};
return Transaction$1;
}());
var WriteBatch = /** @class */ (function () {
function WriteBatch(_firestore) {
this._firestore = _firestore;
this._mutations = [];
this._committed = false;
}
WriteBatch.prototype.set = function (documentRef, value, options) {
validateBetweenNumberOfArgs('WriteBatch.set', arguments, 2, 3);
this.verifyNotCommitted();
var ref = validateReference('WriteBatch.set', documentRef, this._firestore);
options = validateSetOptions('WriteBatch.set', options);
var _e = applyFirestoreDataConverter(ref._converter, value, 'WriteBatch.set'), convertedValue = _e[0], functionName = _e[1];
var parsed = options.merge || options.mergeFields
? this._firestore._dataReader.parseMergeData(functionName, convertedValue, options.mergeFields)
: this._firestore._dataReader.parseSetData(functionName, convertedValue);
this._mutations = this._mutations.concat(parsed.toMutations(ref._key, Precondition.none()));
return this;
};
WriteBatch.prototype.update = function (documentRef, fieldOrUpdateData, value) {
var moreFieldsAndValues = [];
for (var _i = 3; _i < arguments.length; _i++) {
moreFieldsAndValues[_i - 3] = arguments[_i];
}
this.verifyNotCommitted();
var ref;
var parsed;
if (typeof fieldOrUpdateData === 'string' ||
fieldOrUpdateData instanceof FieldPath$1) {
validateAtLeastNumberOfArgs('WriteBatch.update', arguments, 3);
ref = validateReference('WriteBatch.update', documentRef, this._firestore);
parsed = this._firestore._dataReader.parseUpdateVarargs('WriteBatch.update', fieldOrUpdateData, value, moreFieldsAndValues);
}
else {
validateExactNumberOfArgs('WriteBatch.update', arguments, 2);
ref = validateReference('WriteBatch.update', documentRef, this._firestore);
parsed = this._firestore._dataReader.parseUpdateData('WriteBatch.update', fieldOrUpdateData);
}
this._mutations = this._mutations.concat(parsed.toMutations(ref._key, Precondition.exists(true)));
return this;
};
WriteBatch.prototype.delete = function (documentRef) {
validateExactNumberOfArgs('WriteBatch.delete', arguments, 1);
this.verifyNotCommitted();
var ref = validateReference('WriteBatch.delete', documentRef, this._firestore);
this._mutations = this._mutations.concat(new DeleteMutation(ref._key, Precondition.none()));
return this;
};
WriteBatch.prototype.commit = function () {
this.verifyNotCommitted();
this._committed = true;
if (this._mutations.length > 0) {
return this._firestore.ensureClientConfigured().write(this._mutations);
}
return Promise.resolve();
};
WriteBatch.prototype.verifyNotCommitted = function () {
if (this._committed) {
throw new FirestoreError(Code.FAILED_PRECONDITION, 'A write batch can no longer be used after commit() ' +
'has been called.');
}
};
return WriteBatch;
}());
/**
* A reference to a particular document in a collection in the database.
*/
var DocumentReference = /** @class */ (function () {
function DocumentReference(_key, firestore, _converter) {
this._key = _key;
this.firestore = firestore;
this._converter = _converter;
this._firestoreClient = this.firestore.ensureClientConfigured();
}
DocumentReference.forPath = function (path, firestore, converter) {
if (path.length % 2 !== 0) {
throw new FirestoreError(Code.INVALID_ARGUMENT, 'Invalid document reference. Document ' +
'references must have an even number of segments, but ' +
(path.canonicalString() + " has " + path.length));
}
return new DocumentReference(new DocumentKey(path), firestore, converter);
};
Object.defineProperty(DocumentReference.prototype, "id", {
get: function () {
return this._key.path.lastSegment();
},
enumerable: true,
configurable: true
});
Object.defineProperty(DocumentReference.prototype, "parent", {
get: function () {
return new CollectionReference(this._key.path.popLast(), this.firestore, this._converter);
},
enumerable: true,
configurable: true
});
Object.defineProperty(DocumentReference.prototype, "path", {
get: function () {
return this._key.path.canonicalString();
},
enumerable: true,
configurable: true
});
DocumentReference.prototype.collection = function (pathString) {
validateExactNumberOfArgs('DocumentReference.collection', arguments, 1);
validateArgType('DocumentReference.collection', 'non-empty string', 1, pathString);
if (!pathString) {
throw new FirestoreError(Code.INVALID_ARGUMENT, 'Must provide a non-empty collection name to collection()');
}
var path = ResourcePath.fromString(pathString);
return new CollectionReference(this._key.path.child(path), this.firestore);
};
DocumentReference.prototype.isEqual = function (other) {
if (!(other instanceof DocumentReference)) {
throw invalidClassError('isEqual', 'DocumentReference', 1, other);
}
return (this.firestore === other.firestore &&
this._key.isEqual(other._key) &&
this._converter === other._converter);
};
DocumentReference.prototype.set = function (value, options) {
validateBetweenNumberOfArgs('DocumentReference.set', arguments, 1, 2);
options = validateSetOptions('DocumentReference.set', options);
var _e = applyFirestoreDataConverter(this._converter, value, 'DocumentReference.set'), convertedValue = _e[0], functionName = _e[1];
var parsed = options.merge || options.mergeFields
? this.firestore._dataReader.parseMergeData(functionName, convertedValue, options.mergeFields)
: this.firestore._dataReader.parseSetData(functionName, convertedValue);
return this._firestoreClient.write(parsed.toMutations(this._key, Precondition.none()));
};
DocumentReference.prototype.update = function (fieldOrUpdateData, value) {
var moreFieldsAndValues = [];
for (var _i = 2; _i < arguments.length; _i++) {
moreFieldsAndValues[_i - 2] = arguments[_i];
}
var parsed;
if (typeof fieldOrUpdateData === 'string' ||
fieldOrUpdateData instanceof FieldPath$1) {
validateAtLeastNumberOfArgs('DocumentReference.update', arguments, 2);
parsed = this.firestore._dataReader.parseUpdateVarargs('DocumentReference.update', fieldOrUpdateData, value, moreFieldsAndValues);
}
else {
validateExactNumberOfArgs('DocumentReference.update', arguments, 1);
parsed = this.firestore._dataReader.parseUpdateData('DocumentReference.update', fieldOrUpdateData);
}
return this._firestoreClient.write(parsed.toMutations(this._key, Precondition.exists(true)));
};
DocumentReference.prototype.delete = function () {
validateExactNumberOfArgs('DocumentReference.delete', arguments, 0);
return this._firestoreClient.write([
new DeleteMutation(this._key, Precondition.none())
]);
};
DocumentReference.prototype.onSnapshot = function () {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
validateBetweenNumberOfArgs('DocumentReference.onSnapshot', arguments, 1, 4);
var options = {
includeMetadataChanges: false
};
var observer;
var currArg = 0;
if (typeof args[currArg] === 'object' &&
!isPartialObserver(args[currArg])) {
options = args[currArg];
validateOptionNames('DocumentReference.onSnapshot', options, [
'includeMetadataChanges'
]);
validateNamedOptionalType('DocumentReference.onSnapshot', 'boolean', 'includeMetadataChanges', options.includeMetadataChanges);
currArg++;
}
var internalOptions = {
includeMetadataChanges: options.includeMetadataChanges
};
if (isPartialObserver(args[currArg])) {
observer = args[currArg];
}
else {
validateArgType('DocumentReference.onSnapshot', 'function', currArg, args[currArg]);
validateOptionalArgType('DocumentReference.onSnapshot', 'function', currArg + 1, args[currArg + 1]);
validateOptionalArgType('DocumentReference.onSnapshot', 'function', currArg + 2, args[currArg + 2]);
observer = {
next: args[currArg],
error: args[currArg + 1],
complete: args[currArg + 2]
};
}
return this.onSnapshotInternal(internalOptions, observer);
};
DocumentReference.prototype.onSnapshotInternal = function (options, observer) {
var _this = this;
var errHandler = function (err) {
console.error('Uncaught Error in onSnapshot:', err);
};
if (observer.error) {
errHandler = observer.error.bind(observer);
}
var asyncObserver = new AsyncObserver({
next: function (snapshot) {
if (observer.next) {
debugAssert(snapshot.docs.size <= 1, 'Too many documents returned on a document query');
var doc = snapshot.docs.get(_this._key);
observer.next(new DocumentSnapshot(_this.firestore, _this._key, doc, snapshot.fromCache, snapshot.hasPendingWrites, _this._converter));
}
},
error: errHandler
});
var internalListener = this._firestoreClient.listen(Query.atPath(this._key.path), asyncObserver, options);
return function () {
asyncObserver.mute();
_this._firestoreClient.unlisten(internalListener);
};
};
DocumentReference.prototype.get = function (options) {
var _this = this;
validateBetweenNumberOfArgs('DocumentReference.get', arguments, 0, 1);
validateGetOptions('DocumentReference.get', options);
return new Promise(function (resolve, reject) {
if (options && options.source === 'cache') {
_this.firestore
.ensureClientConfigured()
.getDocumentFromLocalCache(_this._key)
.then(function (doc) {
resolve(new DocumentSnapshot(_this.firestore, _this._key, doc,
/*fromCache=*/ true, doc instanceof Document ? doc.hasLocalMutations : false, _this._converter));
}, reject);
}
else {
_this.getViaSnapshotListener(resolve, reject, options);
}
});
};
DocumentReference.prototype.getViaSnapshotListener = function (resolve, reject, options) {
var unlisten = this.onSnapshotInternal({
includeMetadataChanges: true,
waitForSyncWhenOnline: true
}, {
next: function (snap) {
// Remove query first before passing event to user to avoid
// user actions affecting the now stale query.
unlisten();
if (!snap.exists && snap.metadata.fromCache) {
// TODO(dimond): If we're online and the document doesn't
// exist then we resolve with a doc.exists set to false. If
// we're offline however, we reject the Promise in this
// case. Two options: 1) Cache the negative response from
// the server so we can deliver that even when you're
// offline 2) Actually reject the Promise in the online case
// if the document doesn't exist.
reject(new FirestoreError(Code.UNAVAILABLE, 'Failed to get document because the client is ' + 'offline.'));
}
else if (snap.exists &&
snap.metadata.fromCache &&
options &&
options.source === 'server') {
reject(new FirestoreError(Code.UNAVAILABLE, 'Failed to get document from server. (However, this ' +
'document does exist in the local cache. Run again ' +
'without setting source to "server" to ' +
'retrieve the cached document.)'));
}
else {
resolve(snap);
}
},
error: reject
});
};
DocumentReference.prototype.withConverter = function (converter) {
return new DocumentReference(this._key, this.firestore, converter);
};
return DocumentReference;
}());
var SnapshotMetadata = /** @class */ (function () {
function SnapshotMetadata(hasPendingWrites, fromCache) {
this.hasPendingWrites = hasPendingWrites;
this.fromCache = fromCache;
}
SnapshotMetadata.prototype.isEqual = function (other) {
return (this.hasPendingWrites === other.hasPendingWrites &&
this.fromCache === other.fromCache);
};
return SnapshotMetadata;
}());
var DocumentSnapshot = /** @class */ (function () {
function DocumentSnapshot(_firestore, _key, _document, _fromCache, _hasPendingWrites, _converter) {
this._firestore = _firestore;
this._key = _key;
this._document = _document;
this._fromCache = _fromCache;
this._hasPendingWrites = _hasPendingWrites;
this._converter = _converter;
}
DocumentSnapshot.prototype.data = function (options) {
validateBetweenNumberOfArgs('DocumentSnapshot.data', arguments, 0, 1);
options = validateSnapshotOptions('DocumentSnapshot.data', options);
if (!this._document) {
return undefined;
}
else {
// We only want to use the converter and create a new DocumentSnapshot
// if a converter has been provided.
if (this._converter) {
var snapshot = new QueryDocumentSnapshot(this._firestore, this._key, this._document, this._fromCache, this._hasPendingWrites);
return this._converter.fromFirestore(snapshot, options);
}
else {
var userDataWriter = new UserDataWriter(this._firestore, this._firestore._areTimestampsInSnapshotsEnabled(), options.serverTimestamps,
/* converter= */ undefined);
return userDataWriter.convertValue(this._document.toProto());
}
}
};
DocumentSnapshot.prototype.get = function (fieldPath, options) {
validateBetweenNumberOfArgs('DocumentSnapshot.get', arguments, 1, 2);
options = validateSnapshotOptions('DocumentSnapshot.get', options);
if (this._document) {
var value = this._document
.data()
.field(fieldPathFromArgument('DocumentSnapshot.get', fieldPath));
if (value !== null) {
var userDataWriter = new UserDataWriter(this._firestore, this._firestore._areTimestampsInSnapshotsEnabled(), options.serverTimestamps, this._converter);
return userDataWriter.convertValue(value);
}
}
return undefined;
};
Object.defineProperty(DocumentSnapshot.prototype, "id", {
get: function () {
return this._key.path.lastSegment();
},
enumerable: true,
configurable: true
});
Object.defineProperty(DocumentSnapshot.prototype, "ref", {
get: function () {
return new DocumentReference(this._key, this._firestore, this._converter);
},
enumerable: true,
configurable: true
});
Object.defineProperty(DocumentSnapshot.prototype, "exists", {
get: function () {
return this._document !== null;
},
enumerable: true,
configurable: true
});
Object.defineProperty(DocumentSnapshot.prototype, "metadata", {
get: function () {
return new SnapshotMetadata(this._hasPendingWrites, this._fromCache);
},
enumerable: true,
configurable: true
});
DocumentSnapshot.prototype.isEqual = function (other) {
if (!(other instanceof DocumentSnapshot)) {
throw invalidClassError('isEqual', 'DocumentSnapshot', 1, other);
}
return (this._firestore === other._firestore &&
this._fromCache === other._fromCache &&
this._key.isEqual(other._key) &&
(this._document === null
? other._document === null
: this._document.isEqual(other._document)) &&
this._converter === other._converter);
};
return DocumentSnapshot;
}());
var QueryDocumentSnapshot = /** @class */ (function (_super) {
tslib.__extends(QueryDocumentSnapshot, _super);
function QueryDocumentSnapshot() {
return _super !== null && _super.apply(this, arguments) || this;
}
QueryDocumentSnapshot.prototype.data = function (options) {
var data = _super.prototype.data.call(this, options);
debugAssert(data !== undefined, 'Document in a QueryDocumentSnapshot should exist');
return data;
};
return QueryDocumentSnapshot;
}(DocumentSnapshot));
var Query$1 = /** @class */ (function () {
function Query$1(_query, firestore, _converter) {
this._query = _query;
this.firestore = firestore;
this._converter = _converter;
}
Query$1.prototype.where = function (field, opStr, value) {
validateExactNumberOfArgs('Query.where', arguments, 3);
validateDefined('Query.where', 3, value);
// Enumerated from the WhereFilterOp type in index.d.ts.
var whereFilterOpEnums = [
"<" /* LESS_THAN */,
"<=" /* LESS_THAN_OR_EQUAL */,
"==" /* EQUAL */,
">=" /* GREATER_THAN_OR_EQUAL */,
">" /* GREATER_THAN */,
"array-contains" /* ARRAY_CONTAINS */,
"in" /* IN */,
"array-contains-any" /* ARRAY_CONTAINS_ANY */
];
var op = validateStringEnum('Query.where', whereFilterOpEnums, 2, opStr);
var fieldValue;
var fieldPath = fieldPathFromArgument('Query.where', field);
if (fieldPath.isKeyField()) {
if (op === "array-contains" /* ARRAY_CONTAINS */ ||
op === "array-contains-any" /* ARRAY_CONTAINS_ANY */) {
throw new FirestoreError(Code.INVALID_ARGUMENT, "Invalid Query. You can't perform '" + op + "' " +
'queries on FieldPath.documentId().');
}
else if (op === "in" /* IN */) {
this.validateDisjunctiveFilterElements(value, op);
var referenceList = [];
for (var _i = 0, value_2 = value; _i < value_2.length; _i++) {
var arrayValue = value_2[_i];
referenceList.push(this.parseDocumentIdValue(arrayValue));
}
fieldValue = { arrayValue: { values: referenceList } };
}
else {
fieldValue = this.parseDocumentIdValue(value);
}
}
else {
if (op === "in" /* IN */ || op === "array-contains-any" /* ARRAY_CONTAINS_ANY */) {
this.validateDisjunctiveFilterElements(value, op);
}
fieldValue = this.firestore._dataReader.parseQueryValue('Query.where', value,
// We only allow nested arrays for IN queries.
/** allowArrays = */ op === "in" /* IN */);
}
var filter = FieldFilter.create(fieldPath, op, fieldValue);
this.validateNewFilter(filter);
return new Query$1(this._query.addFilter(filter), this.firestore, this._converter);
};
Query$1.prototype.orderBy = function (field, directionStr) {
validateBetweenNumberOfArgs('Query.orderBy', arguments, 1, 2);
validateOptionalArgType('Query.orderBy', 'non-empty string', 2, directionStr);
var direction;
if (directionStr === undefined || directionStr === 'asc') {
direction = "asc" /* ASCENDING */;
}
else if (directionStr === 'desc') {
direction = "desc" /* DESCENDING */;
}
else {
throw new FirestoreError(Code.INVALID_ARGUMENT, "Function Query.orderBy() has unknown direction '" + directionStr + "', " +
"expected 'asc' or 'desc'.");
}
if (this._query.startAt !== null) {
throw new FirestoreError(Code.INVALID_ARGUMENT, 'Invalid query. You must not call Query.startAt() or ' +
'Query.startAfter() before calling Query.orderBy().');
}
if (this._query.endAt !== null) {
throw new FirestoreError(Code.INVALID_ARGUMENT, 'Invalid query. You must not call Query.endAt() or ' +
'Query.endBefore() before calling Query.orderBy().');
}
var fieldPath = fieldPathFromArgument('Query.orderBy', field);
var orderBy = new OrderBy(fieldPath, direction);
this.validateNewOrderBy(orderBy);
return new Query$1(this._query.addOrderBy(orderBy), this.firestore, this._converter);
};
Query$1.prototype.limit = function (n) {
validateExactNumberOfArgs('Query.limit', arguments, 1);
validateArgType('Query.limit', 'number', 1, n);
validatePositiveNumber('Query.limit', 1, n);
return new Query$1(this._query.withLimitToFirst(n), this.firestore, this._converter);
};
Query$1.prototype.limitToLast = function (n) {
validateExactNumberOfArgs('Query.limitToLast', arguments, 1);
validateArgType('Query.limitToLast', 'number', 1, n);
validatePositiveNumber('Query.limitToLast', 1, n);
return new Query$1(this._query.withLimitToLast(n), this.firestore, this._converter);
};
Query$1.prototype.startAt = function (docOrField) {
var fields = [];
for (var _i = 1; _i < arguments.length; _i++) {
fields[_i - 1] = arguments[_i];
}
validateAtLeastNumberOfArgs('Query.startAt', arguments, 1);
var bound = this.boundFromDocOrFields('Query.startAt', docOrField, fields,
/*before=*/ true);
return new Query$1(this._query.withStartAt(bound), this.firestore, this._converter);
};
Query$1.prototype.startAfter = function (docOrField) {
var fields = [];
for (var _i = 1; _i < arguments.length; _i++) {
fields[_i - 1] = arguments[_i];
}
validateAtLeastNumberOfArgs('Query.startAfter', arguments, 1);
var bound = this.boundFromDocOrFields('Query.startAfter', docOrField, fields,
/*before=*/ false);
return new Query$1(this._query.withStartAt(bound), this.firestore, this._converter);
};
Query$1.prototype.endBefore = function (docOrField) {
var fields = [];
for (var _i = 1; _i < arguments.length; _i++) {
fields[_i - 1] = arguments[_i];
}
validateAtLeastNumberOfArgs('Query.endBefore', arguments, 1);
var bound = this.boundFromDocOrFields('Query.endBefore', docOrField, fields,
/*before=*/ true);
return new Query$1(this._query.withEndAt(bound), this.firestore, this._converter);
};
Query$1.prototype.endAt = function (docOrField) {
var fields = [];
for (var _i = 1; _i < arguments.length; _i++) {
fields[_i - 1] = arguments[_i];
}
validateAtLeastNumberOfArgs('Query.endAt', arguments, 1);
var bound = this.boundFromDocOrFields('Query.endAt', docOrField, fields,
/*before=*/ false);
return new Query$1(this._query.withEndAt(bound), this.firestore, this._converter);
};
Query$1.prototype.isEqual = function (other) {
if (!(other instanceof Query$1)) {
throw invalidClassError('isEqual', 'Query', 1, other);
}
return (this.firestore === other.firestore && this._query.isEqual(other._query));
};
Query$1.prototype.withConverter = function (converter) {
return new Query$1(this._query, this.firestore, converter);
};
/** Helper function to create a bound from a document or fields */
Query$1.prototype.boundFromDocOrFields = function (methodName, docOrField, fields, before) {
validateDefined(methodName, 1, docOrField);
if (docOrField instanceof DocumentSnapshot) {
if (fields.length > 0) {
throw new FirestoreError(Code.INVALID_ARGUMENT, "Too many arguments provided to " + methodName + "().");
}
var snap = docOrField;
if (!snap.exists) {
throw new FirestoreError(Code.NOT_FOUND, "Can't use a DocumentSnapshot that doesn't exist for " +
(methodName + "()."));
}
return this.boundFromDocument(snap._document, before);
}
else {
var allFields = [docOrField].concat(fields);
return this.boundFromFields(methodName, allFields, before);
}
};
/**
* Create a Bound from a query and a document.
*
* Note that the Bound will always include the key of the document
* and so only the provided document will compare equal to the returned
* position.
*
* Will throw if the document does not contain all fields of the order by
* of the query or if any of the fields in the order by are an uncommitted
* server timestamp.
*/
Query$1.prototype.boundFromDocument = function (doc, before) {
var components = [];
// Because people expect to continue/end a query at the exact document
// provided, we need to use the implicit sort order rather than the explicit
// sort order, because it's guaranteed to contain the document key. That way
// the position becomes unambiguous and the query continues/ends exactly at
// the provided document. Without the key (by using the explicit sort
// orders), multiple documents could match the position, yielding duplicate
// results.
for (var _i = 0, _e = this._query.orderBy; _i < _e.length; _i++) {
var orderBy = _e[_i];
if (orderBy.field.isKeyField()) {
components.push(refValue(this.firestore._databaseId, doc.key));
}
else {
var value = doc.field(orderBy.field);
if (isServerTimestamp(value)) {
throw new FirestoreError(Code.INVALID_ARGUMENT, 'Invalid query. You are trying to start or end a query using a ' +
'document for which the field "' +
orderBy.field +
'" is an uncommitted server timestamp. (Since the value of ' +
'this field is unknown, you cannot start/end a query with it.)');
}
else if (value !== null) {
components.push(value);
}
else {
var field = orderBy.field.canonicalString();
throw new FirestoreError(Code.INVALID_ARGUMENT, "Invalid query. You are trying to start or end a query using a " +
("document for which the field '" + field + "' (used as the ") +
"orderBy) does not exist.");
}
}
}
return new Bound(components, before);
};
/**
* Converts a list of field values to a Bound for the given query.
*/
Query$1.prototype.boundFromFields = function (methodName, values, before) {
// Use explicit order by's because it has to match the query the user made
var orderBy = this._query.explicitOrderBy;
if (values.length > orderBy.length) {
throw new FirestoreError(Code.INVALID_ARGUMENT, "Too many arguments provided to " + methodName + "(). " +
"The number of arguments must be less than or equal to the " +
"number of Query.orderBy() clauses");
}
var components = [];
for (var i = 0; i < values.length; i++) {
var rawValue = values[i];
var orderByComponent = orderBy[i];
if (orderByComponent.field.isKeyField()) {
if (typeof rawValue !== 'string') {
throw new FirestoreError(Code.INVALID_ARGUMENT, "Invalid query. Expected a string for document ID in " +
(methodName + "(), but got a " + typeof rawValue));
}
if (!this._query.isCollectionGroupQuery() &&
rawValue.indexOf('/') !== -1) {
throw new FirestoreError(Code.INVALID_ARGUMENT, "Invalid query. When querying a collection and ordering by FieldPath.documentId(), " +
("the value passed to " + methodName + "() must be a plain document ID, but ") +
("'" + rawValue + "' contains a slash."));
}
var path = this._query.path.child(ResourcePath.fromString(rawValue));
if (!DocumentKey.isDocumentKey(path)) {
throw new FirestoreError(Code.INVALID_ARGUMENT, "Invalid query. When querying a collection group and ordering by " +
("FieldPath.documentId(), the value passed to " + methodName + "() must result in a ") +
("valid document path, but '" + path + "' is not because it contains an odd number ") +
"of segments.");
}
var key = new DocumentKey(path);
components.push(refValue(this.firestore._databaseId, key));
}
else {
var wrapped = this.firestore._dataReader.parseQueryValue(methodName, rawValue);
components.push(wrapped);
}
}
return new Bound(components, before);
};
Query$1.prototype.onSnapshot = function () {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
validateBetweenNumberOfArgs('Query.onSnapshot', arguments, 1, 4);
var options = {};
var observer;
var currArg = 0;
if (typeof args[currArg] === 'object' &&
!isPartialObserver(args[currArg])) {
options = args[currArg];
validateOptionNames('Query.onSnapshot', options, [
'includeMetadataChanges'
]);
validateNamedOptionalType('Query.onSnapshot', 'boolean', 'includeMetadataChanges', options.includeMetadataChanges);
currArg++;
}
if (isPartialObserver(args[currArg])) {
observer = args[currArg];
}
else {
validateArgType('Query.onSnapshot', 'function', currArg, args[currArg]);
validateOptionalArgType('Query.onSnapshot', 'function', currArg + 1, args[currArg + 1]);
validateOptionalArgType('Query.onSnapshot', 'function', currArg + 2, args[currArg + 2]);
observer = {
next: args[currArg],
error: args[currArg + 1],
complete: args[currArg + 2]
};
}
this.validateHasExplicitOrderByForLimitToLast(this._query);
return this.onSnapshotInternal(options, observer);
};
Query$1.prototype.onSnapshotInternal = function (options, observer) {
var _this = this;
var errHandler = function (err) {
console.error('Uncaught Error in onSnapshot:', err);
};
if (observer.error) {
errHandler = observer.error.bind(observer);
}
var asyncObserver = new AsyncObserver({
next: function (result) {
if (observer.next) {
observer.next(new QuerySnapshot(_this.firestore, _this._query, result, _this._converter));
}
},
error: errHandler
});
var firestoreClient = this.firestore.ensureClientConfigured();
var internalListener = firestoreClient.listen(this._query, asyncObserver, options);
return function () {
asyncObserver.mute();
firestoreClient.unlisten(internalListener);
};
};
Query$1.prototype.validateHasExplicitOrderByForLimitToLast = function (query) {
if (query.hasLimitToLast() && query.explicitOrderBy.length === 0) {
throw new FirestoreError(Code.UNIMPLEMENTED, 'limitToLast() queries require specifying at least one orderBy() clause');
}
};
Query$1.prototype.get = function (options) {
var _this = this;
validateBetweenNumberOfArgs('Query.get', arguments, 0, 1);
validateGetOptions('Query.get', options);
this.validateHasExplicitOrderByForLimitToLast(this._query);
return new Promise(function (resolve, reject) {
if (options && options.source === 'cache') {
_this.firestore
.ensureClientConfigured()
.getDocumentsFromLocalCache(_this._query)
.then(function (viewSnap) {
resolve(new QuerySnapshot(_this.firestore, _this._query, viewSnap, _this._converter));
}, reject);
}
else {
_this.getViaSnapshotListener(resolve, reject, options);
}
});
};
Query$1.prototype.getViaSnapshotListener = function (resolve, reject, options) {
var unlisten = this.onSnapshotInternal({
includeMetadataChanges: true,
waitForSyncWhenOnline: true
}, {
next: function (result) {
// Remove query first before passing event to user to avoid
// user actions affecting the now stale query.
unlisten();
if (result.metadata.fromCache &&
options &&
options.source === 'server') {
reject(new FirestoreError(Code.UNAVAILABLE, 'Failed to get documents from server. (However, these ' +
'documents may exist in the local cache. Run again ' +
'without setting source to "server" to ' +
'retrieve the cached documents.)'));
}
else {
resolve(result);
}
},
error: reject
});
};
/**
* Parses the given documentIdValue into a ReferenceValue, throwing
* appropriate errors if the value is anything other than a DocumentReference
* or String, or if the string is malformed.
*/
Query$1.prototype.parseDocumentIdValue = function (documentIdValue) {
if (typeof documentIdValue === 'string') {
if (documentIdValue === '') {
throw new FirestoreError(Code.INVALID_ARGUMENT, 'Invalid query. When querying with FieldPath.documentId(), you ' +
'must provide a valid document ID, but it was an empty string.');
}
if (!this._query.isCollectionGroupQuery() &&
documentIdValue.indexOf('/') !== -1) {
throw new FirestoreError(Code.INVALID_ARGUMENT, "Invalid query. When querying a collection by " +
"FieldPath.documentId(), you must provide a plain document ID, but " +
("'" + documentIdValue + "' contains a '/' character."));
}
var path = this._query.path.child(ResourcePath.fromString(documentIdValue));
if (!DocumentKey.isDocumentKey(path)) {
throw new FirestoreError(Code.INVALID_ARGUMENT, "Invalid query. When querying a collection group by " +
"FieldPath.documentId(), the value provided must result in a valid document path, " +
("but '" + path + "' is not because it has an odd number of segments (" + path.length + ")."));
}
return refValue(this.firestore._databaseId, new DocumentKey(path));
}
else if (documentIdValue instanceof DocumentReference) {
var ref = documentIdValue;
return refValue(this.firestore._databaseId, ref._key);
}
else {
throw new FirestoreError(Code.INVALID_ARGUMENT, "Invalid query. When querying with FieldPath.documentId(), you must provide a valid " +
"string or a DocumentReference, but it was: " +
(valueDescription(documentIdValue) + "."));
}
};
/**
* Validates that the value passed into a disjunctrive filter satisfies all
* array requirements.
*/
Query$1.prototype.validateDisjunctiveFilterElements = function (value, operator) {
if (!Array.isArray(value) || value.length === 0) {
throw new FirestoreError(Code.INVALID_ARGUMENT, 'Invalid Query. A non-empty array is required for ' +
("'" + operator.toString() + "' filters."));
}
if (value.length > 10) {
throw new FirestoreError(Code.INVALID_ARGUMENT, "Invalid Query. '" + operator.toString() + "' filters support a " +
'maximum of 10 elements in the value array.');
}
if (value.indexOf(null) >= 0) {
throw new FirestoreError(Code.INVALID_ARGUMENT, "Invalid Query. '" + operator.toString() + "' filters cannot contain 'null' " +
'in the value array.');
}
if (value.filter(function (element) { return Number.isNaN(element); }).length > 0) {
throw new FirestoreError(Code.INVALID_ARGUMENT, "Invalid Query. '" + operator.toString() + "' filters cannot contain 'NaN' " +
'in the value array.');
}
};
Query$1.prototype.validateNewFilter = function (filter) {
if (filter instanceof FieldFilter) {
var arrayOps = ["array-contains" /* ARRAY_CONTAINS */, "array-contains-any" /* ARRAY_CONTAINS_ANY */];
var disjunctiveOps = ["in" /* IN */, "array-contains-any" /* ARRAY_CONTAINS_ANY */];
var isArrayOp = arrayOps.indexOf(filter.op) >= 0;
var isDisjunctiveOp = disjunctiveOps.indexOf(filter.op) >= 0;
if (filter.isInequality()) {
var existingField = this._query.getInequalityFilterField();
if (existingField !== null && !existingField.isEqual(filter.field)) {
throw new FirestoreError(Code.INVALID_ARGUMENT, 'Invalid query. All where filters with an inequality' +
' (<, <=, >, or >=) must be on the same field. But you have' +
(" inequality filters on '" + existingField.toString() + "'") +
(" and '" + filter.field.toString() + "'"));
}
var firstOrderByField = this._query.getFirstOrderByField();
if (firstOrderByField !== null) {
this.validateOrderByAndInequalityMatch(filter.field, firstOrderByField);
}
}
else if (isDisjunctiveOp || isArrayOp) {
// You can have at most 1 disjunctive filter and 1 array filter. Check if
// the new filter conflicts with an existing one.
var conflictingOp = null;
if (isDisjunctiveOp) {
conflictingOp = this._query.findFilterOperator(disjunctiveOps);
}
if (conflictingOp === null && isArrayOp) {
conflictingOp = this._query.findFilterOperator(arrayOps);
}
if (conflictingOp != null) {
// We special case when it's a duplicate op to give a slightly clearer error message.
if (conflictingOp === filter.op) {
throw new FirestoreError(Code.INVALID_ARGUMENT, 'Invalid query. You cannot use more than one ' +
("'" + filter.op.toString() + "' filter."));
}
else {
throw new FirestoreError(Code.INVALID_ARGUMENT, "Invalid query. You cannot use '" + filter.op.toString() + "' filters " +
("with '" + conflictingOp.toString() + "' filters."));
}
}
}
}
};
Query$1.prototype.validateNewOrderBy = function (orderBy) {
if (this._query.getFirstOrderByField() === null) {
// This is the first order by. It must match any inequality.
var inequalityField = this._query.getInequalityFilterField();
if (inequalityField !== null) {
this.validateOrderByAndInequalityMatch(inequalityField, orderBy.field);
}
}
};
Query$1.prototype.validateOrderByAndInequalityMatch = function (inequality, orderBy) {
if (!orderBy.isEqual(inequality)) {
throw new FirestoreError(Code.INVALID_ARGUMENT, "Invalid query. You have a where filter with an inequality " +
("(<, <=, >, or >=) on field '" + inequality.toString() + "' ") +
("and so you must also use '" + inequality.toString() + "' ") +
"as your first Query.orderBy(), but your first Query.orderBy() " +
("is on field '" + orderBy.toString() + "' instead."));
}
};
return Query$1;
}());
var QuerySnapshot = /** @class */ (function () {
function QuerySnapshot(_firestore, _originalQuery, _snapshot, _converter) {
this._firestore = _firestore;
this._originalQuery = _originalQuery;
this._snapshot = _snapshot;
this._converter = _converter;
this._cachedChanges = null;
this._cachedChangesIncludeMetadataChanges = null;
this.metadata = new SnapshotMetadata(_snapshot.hasPendingWrites, _snapshot.fromCache);
}
Object.defineProperty(QuerySnapshot.prototype, "docs", {
get: function () {
var result = [];
this.forEach(function (doc) { return result.push(doc); });
return result;
},
enumerable: true,
configurable: true
});
Object.defineProperty(QuerySnapshot.prototype, "empty", {
get: function () {
return this._snapshot.docs.isEmpty();
},
enumerable: true,
configurable: true
});
Object.defineProperty(QuerySnapshot.prototype, "size", {
get: function () {
return this._snapshot.docs.size;
},
enumerable: true,
configurable: true
});
QuerySnapshot.prototype.forEach = function (callback, thisArg) {
var _this = this;
validateBetweenNumberOfArgs('QuerySnapshot.forEach', arguments, 1, 2);
validateArgType('QuerySnapshot.forEach', 'function', 1, callback);
this._snapshot.docs.forEach(function (doc) {
callback.call(thisArg, _this.convertToDocumentImpl(doc));
});
};
Object.defineProperty(QuerySnapshot.prototype, "query", {
get: function () {
return new Query$1(this._originalQuery, this._firestore, this._converter);
},
enumerable: true,
configurable: true
});
QuerySnapshot.prototype.docChanges = function (options) {
if (options) {
validateOptionNames('QuerySnapshot.docChanges', options, [
'includeMetadataChanges'
]);
validateNamedOptionalType('QuerySnapshot.docChanges', 'boolean', 'includeMetadataChanges', options.includeMetadataChanges);
}
var includeMetadataChanges = !!(options && options.includeMetadataChanges);
if (includeMetadataChanges && this._snapshot.excludesMetadataChanges) {
throw new FirestoreError(Code.INVALID_ARGUMENT, 'To include metadata changes with your document changes, you must ' +
'also pass { includeMetadataChanges:true } to onSnapshot().');
}
if (!this._cachedChanges ||
this._cachedChangesIncludeMetadataChanges !== includeMetadataChanges) {
this._cachedChanges = changesFromSnapshot(this._firestore, includeMetadataChanges, this._snapshot, this._converter);
this._cachedChangesIncludeMetadataChanges = includeMetadataChanges;
}
return this._cachedChanges;
};
/** Check the equality. The call can be very expensive. */
QuerySnapshot.prototype.isEqual = function (other) {
if (!(other instanceof QuerySnapshot)) {
throw invalidClassError('isEqual', 'QuerySnapshot', 1, other);
}
return (this._firestore === other._firestore &&
this._originalQuery.isEqual(other._originalQuery) &&
this._snapshot.isEqual(other._snapshot) &&
this._converter === other._converter);
};
QuerySnapshot.prototype.convertToDocumentImpl = function (doc) {
return new QueryDocumentSnapshot(this._firestore, doc.key, doc, this.metadata.fromCache, this._snapshot.mutatedKeys.has(doc.key), this._converter);
};
return QuerySnapshot;
}());
var CollectionReference = /** @class */ (function (_super) {
tslib.__extends(CollectionReference, _super);
function CollectionReference(_path, firestore, _converter) {
var _this = _super.call(this, Query.atPath(_path), firestore, _converter) || this;
_this._path = _path;
if (_path.length % 2 !== 1) {
throw new FirestoreError(Code.INVALID_ARGUMENT, 'Invalid collection reference. Collection ' +
'references must have an odd number of segments, but ' +
(_path.canonicalString() + " has " + _path.length));
}
return _this;
}
Object.defineProperty(CollectionReference.prototype, "id", {
get: function () {
return this._query.path.lastSegment();
},
enumerable: true,
configurable: true
});
Object.defineProperty(CollectionReference.prototype, "parent", {
get: function () {
var parentPath = this._query.path.popLast();
if (parentPath.isEmpty()) {
return null;
}
else {
return new DocumentReference(new DocumentKey(parentPath), this.firestore);
}
},
enumerable: true,
configurable: true
});
Object.defineProperty(CollectionReference.prototype, "path", {
get: function () {
return this._query.path.canonicalString();
},
enumerable: true,
configurable: true
});
CollectionReference.prototype.doc = function (pathString) {
validateBetweenNumberOfArgs('CollectionReference.doc', arguments, 0, 1);
// We allow omission of 'pathString' but explicitly prohibit passing in both
// 'undefined' and 'null'.
if (arguments.length === 0) {
pathString = AutoId.newId();
}
validateArgType('CollectionReference.doc', 'non-empty string', 1, pathString);
if (pathString === '') {
throw new FirestoreError(Code.INVALID_ARGUMENT, 'Document path must be a non-empty string');
}
var path = ResourcePath.fromString(pathString);
return DocumentReference.forPath(this._query.path.child(path), this.firestore, this._converter);
};
CollectionReference.prototype.add = function (value) {
validateExactNumberOfArgs('CollectionReference.add', arguments, 1);
var convertedValue = this._converter
? this._converter.toFirestore(value)
: value;
validateArgType('CollectionReference.add', 'object', 1, convertedValue);
var docRef = this.doc();
return docRef.set(value).then(function () { return docRef; });
};
CollectionReference.prototype.withConverter = function (converter) {
return new CollectionReference(this._path, this.firestore, converter);
};
return CollectionReference;
}(Query$1));
function validateSetOptions(methodName, options) {
if (options === undefined) {
return {
merge: false
};
}
validateOptionNames(methodName, options, ['merge', 'mergeFields']);
validateNamedOptionalType(methodName, 'boolean', 'merge', options.merge);
validateOptionalArrayElements(methodName, 'mergeFields', 'a string or a FieldPath', options.mergeFields, function (element) { return typeof element === 'string' || element instanceof FieldPath$1; });
if (options.mergeFields !== undefined && options.merge !== undefined) {
throw new FirestoreError(Code.INVALID_ARGUMENT, "Invalid options passed to function " + methodName + "(): You cannot specify both \"merge\" " +
"and \"mergeFields\".");
}
return options;
}
function validateSnapshotOptions(methodName, options) {
if (options === undefined) {
return {};
}
validateOptionNames(methodName, options, ['serverTimestamps']);
validateNamedOptionalPropertyEquals(methodName, 'options', 'serverTimestamps', options.serverTimestamps, ['estimate', 'previous', 'none']);
return options;
}
function validateGetOptions(methodName, options) {
validateOptionalArgType(methodName, 'object', 1, options);
if (options) {
validateOptionNames(methodName, options, ['source']);
validateNamedOptionalPropertyEquals(methodName, 'options', 'source', options.source, ['default', 'server', 'cache']);
}
}
function validateReference(methodName, documentRef, firestore) {
if (!(documentRef instanceof DocumentReference)) {
throw invalidClassError(methodName, 'DocumentReference', 1, documentRef);
}
else if (documentRef.firestore !== firestore) {
throw new FirestoreError(Code.INVALID_ARGUMENT, 'Provided document reference is from a different Firestore instance.');
}
else {
return documentRef;
}
}
/**
* Calculates the array of firestore.DocumentChange's for a given ViewSnapshot.
*
* Exported for testing.
*/
function changesFromSnapshot(firestore, includeMetadataChanges, snapshot, converter) {
if (snapshot.oldDocs.isEmpty()) {
// Special case the first snapshot because index calculation is easy and
// fast
var lastDoc_1;
var index_1 = 0;
return snapshot.docChanges.map(function (change) {
var doc = new QueryDocumentSnapshot(firestore, change.doc.key, change.doc, snapshot.fromCache, snapshot.mutatedKeys.has(change.doc.key), converter);
debugAssert(change.type === 0 /* Added */, 'Invalid event type for first snapshot');
debugAssert(!lastDoc_1 || snapshot.query.docComparator(lastDoc_1, change.doc) < 0, 'Got added events in wrong order');
lastDoc_1 = change.doc;
return {
type: 'added',
doc: doc,
oldIndex: -1,
newIndex: index_1++
};
});
}
else {
// A DocumentSet that is updated incrementally as changes are applied to use
// to lookup the index of a document.
var indexTracker_1 = snapshot.oldDocs;
return snapshot.docChanges
.filter(function (change) { return includeMetadataChanges || change.type !== 3; } /* Metadata */)
.map(function (change) {
var doc = new QueryDocumentSnapshot(firestore, change.doc.key, change.doc, snapshot.fromCache, snapshot.mutatedKeys.has(change.doc.key), converter);
var oldIndex = -1;
var newIndex = -1;
if (change.type !== 0 /* Added */) {
oldIndex = indexTracker_1.indexOf(change.doc.key);
debugAssert(oldIndex >= 0, 'Index for document not found');
indexTracker_1 = indexTracker_1.delete(change.doc.key);
}
if (change.type !== 1 /* Removed */) {
indexTracker_1 = indexTracker_1.add(change.doc);
newIndex = indexTracker_1.indexOf(change.doc.key);
}
return { type: resultChangeType(change.type), doc: doc, oldIndex: oldIndex, newIndex: newIndex };
});
}
}
function resultChangeType(type) {
switch (type) {
case 0 /* Added */:
return 'added';
case 2 /* Modified */:
case 3 /* Metadata */:
return 'modified';
case 1 /* Removed */:
return 'removed';
default:
return fail('Unknown change type: ' + type);
}
}
/**
* Converts custom model object of type T into DocumentData by applying the
* converter if it exists.
*
* This function is used when converting user objects to DocumentData
* because we want to provide the user with a more specific error message if
* their set() or fails due to invalid data originating from a toFirestore()
* call.
*/
function applyFirestoreDataConverter(converter, value, functionName) {
var convertedValue;
if (converter) {
convertedValue = converter.toFirestore(value);
functionName = 'toFirestore() in ' + functionName;
}
else {
convertedValue = value;
}
return [convertedValue, functionName];
}
function contains(obj, key) {
return Object.prototype.hasOwnProperty.call(obj, 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.
*/
/**
* Helper function to prevent instantiation through the constructor.
*
* This method creates a new constructor that throws when it's invoked.
* The prototype of that constructor is then set to the prototype of the hidden
* "class" to expose all the prototype methods and allow for instanceof
* checks.
*
* To also make all the static methods available, all properties of the
* original constructor are copied to the new constructor.
*/
function makeConstructorPrivate(cls, optionalMessage) {
function PublicConstructor() {
var error = 'This constructor is private.';
if (optionalMessage) {
error += ' ';
error += optionalMessage;
}
throw new FirestoreError(Code.INVALID_ARGUMENT, error);
}
// Make sure instanceof checks work and all methods are exposed on the public
// constructor
PublicConstructor.prototype = cls.prototype;
// Copy any static methods/members
Object.assign(PublicConstructor, cls);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return PublicConstructor;
}
/**
* @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.
*/
// Public instance that disallows construction at runtime. Note that this still
// allows instanceof checks.
var PublicFirestore = makeConstructorPrivate(Firestore, 'Use firebase.firestore() instead.');
var PublicTransaction = makeConstructorPrivate(Transaction$1, 'Use firebase.firestore().runTransaction() instead.');
var PublicWriteBatch = makeConstructorPrivate(WriteBatch, 'Use firebase.firestore().batch() instead.');
var PublicDocumentReference = makeConstructorPrivate(DocumentReference, 'Use firebase.firestore().doc() instead.');
var PublicDocumentSnapshot = makeConstructorPrivate(DocumentSnapshot);
var PublicQueryDocumentSnapshot = makeConstructorPrivate(QueryDocumentSnapshot);
var PublicQuery = makeConstructorPrivate(Query$1);
var PublicQuerySnapshot = makeConstructorPrivate(QuerySnapshot);
var PublicCollectionReference = makeConstructorPrivate(CollectionReference, 'Use firebase.firestore().collection() instead.');
var PublicFieldValue = makeConstructorPrivate(FieldValue, 'Use FieldValue.<field>() instead.');
var PublicBlob = makeConstructorPrivate(Blob, 'Use Blob.fromUint8Array() or Blob.fromBase64String() instead.');
var firestoreNamespace = {
Firestore: PublicFirestore,
GeoPoint: GeoPoint,
Timestamp: Timestamp,
Blob: PublicBlob,
Transaction: PublicTransaction,
WriteBatch: PublicWriteBatch,
DocumentReference: PublicDocumentReference,
DocumentSnapshot: PublicDocumentSnapshot,
Query: PublicQuery,
QueryDocumentSnapshot: PublicQueryDocumentSnapshot,
QuerySnapshot: PublicQuerySnapshot,
CollectionReference: PublicCollectionReference,
FieldPath: FieldPath$1,
FieldValue: PublicFieldValue,
setLogLevel: Firestore.setLogLevel,
CACHE_SIZE_UNLIMITED: CACHE_SIZE_UNLIMITED
};
/**
* Configures Firestore as part of the Firebase SDK by calling registerService.
*
* @param firebase The FirebaseNamespace to register Firestore with
* @param firestoreFactory A factory function that returns a new Firestore
* instance.
*/
function configureForFirebase(firebase, firestoreFactory) {
firebase.INTERNAL.registerComponent(new component.Component('firestore', function (container) {
var app = container.getProvider('app').getImmediate();
return firestoreFactory(app, container.getProvider('auth-internal'));
}, "PUBLIC" /* PUBLIC */).setServiceProps(Object.assign({}, firestoreNamespace)));
}
/**
* @license
* Copyright 2019 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.
*/
var NoopConnectivityMonitor = /** @class */ (function () {
function NoopConnectivityMonitor() {
}
NoopConnectivityMonitor.prototype.addCallback = function (callback) {
// No-op.
};
NoopConnectivityMonitor.prototype.shutdown = function () {
// No-op.
};
return NoopConnectivityMonitor;
}());
/**
* @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.
*/
/**
* Provides a simple helper class that implements the Stream interface to
* bridge to other implementations that are streams but do not implement the
* interface. The stream callbacks are invoked with the callOn... methods.
*/
var StreamBridge = /** @class */ (function () {
function StreamBridge(args) {
this.sendFn = args.sendFn;
this.closeFn = args.closeFn;
}
StreamBridge.prototype.onOpen = function (callback) {
debugAssert(!this.wrappedOnOpen, 'Called onOpen on stream twice!');
this.wrappedOnOpen = callback;
};
StreamBridge.prototype.onClose = function (callback) {
debugAssert(!this.wrappedOnClose, 'Called onClose on stream twice!');
this.wrappedOnClose = callback;
};
StreamBridge.prototype.onMessage = function (callback) {
debugAssert(!this.wrappedOnMessage, 'Called onMessage on stream twice!');
this.wrappedOnMessage = callback;
};
StreamBridge.prototype.close = function () {
this.closeFn();
};
StreamBridge.prototype.send = function (msg) {
this.sendFn(msg);
};
StreamBridge.prototype.callOnOpen = function () {
debugAssert(this.wrappedOnOpen !== undefined, 'Cannot call onOpen because no callback was set');
this.wrappedOnOpen();
};
StreamBridge.prototype.callOnClose = function (err) {
debugAssert(this.wrappedOnClose !== undefined, 'Cannot call onClose because no callback was set');
this.wrappedOnClose(err);
};
StreamBridge.prototype.callOnMessage = function (msg) {
debugAssert(this.wrappedOnMessage !== undefined, 'Cannot call onMessage because no callback was set');
this.wrappedOnMessage(msg);
};
return StreamBridge;
}());
/**
* @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.
*/
/*
* Utilities for dealing with node.js-style APIs. See nodePromise for more
* details.
*/
/**
* Creates a node-style callback that resolves or rejects a new Promise. The
* callback is passed to the given action which can then use the callback as
* a parameter to a node-style function.
*
* The intent is to directly bridge a node-style function (which takes a
* callback) into a Promise without manually converting between the node-style
* callback and the promise at each call.
*
* In effect it allows you to convert:
*
* @example
* new Promise((resolve: (value?: fs.Stats) => void,
* reject: (error?: any) => void) => {
* fs.stat(path, (error?: any, stat?: fs.Stats) => {
* if (error) {
* reject(error);
* } else {
* resolve(stat);
* }
* });
* });
*
* Into
* @example
* nodePromise((callback: NodeCallback<fs.Stats>) => {
* fs.stat(path, callback);
* });
*
* @param action a function that takes a node-style callback as an argument and
* then uses that callback to invoke some node-style API.
* @return a new Promise which will be rejected if the callback is given the
* first Error parameter or will resolve to the value given otherwise.
*/
function nodePromise(action) {
return new Promise(function (resolve, reject) {
action(function (error, value) {
if (error) {
reject(error);
}
else {
resolve(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.
*/
var SDK_VERSION$1 = firebase.SDK_VERSION;
var grpcVersion = package_json.version;
var LOG_TAG$e = 'Connection';
// TODO(b/38203344): The SDK_VERSION is set independently from Firebase because
// we are doing out-of-band releases. Once we release as part of Firebase, we
// should use the Firebase version instead.
var X_GOOG_API_CLIENT_VALUE = "gl-node/" + process.versions.node + " fire/" + SDK_VERSION$1 + " grpc/" + grpcVersion;
function createMetadata(databaseInfo, token) {
hardAssert(token === null || token.type === 'OAuth', 'If provided, token must be OAuth');
var metadata = new grpcJs.Metadata();
if (token) {
for (var header in token.authHeaders) {
if (token.authHeaders.hasOwnProperty(header)) {
metadata.set(header, token.authHeaders[header]);
}
}
}
metadata.set('x-goog-api-client', X_GOOG_API_CLIENT_VALUE);
// This header is used to improve routing and project isolation by the
// backend.
metadata.set('google-cloud-resource-prefix', "projects/" + databaseInfo.databaseId.projectId + "/" +
("databases/" + databaseInfo.databaseId.database));
return metadata;
}
/**
* A Connection implemented by GRPC-Node.
*/
var GrpcConnection = /** @class */ (function () {
function GrpcConnection(protos, databaseInfo) {
this.databaseInfo = databaseInfo;
// We cache stubs for the most-recently-used token.
this.cachedStub = null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.firestore = protos['google']['firestore']['v1'];
}
GrpcConnection.prototype.ensureActiveStub = function () {
if (!this.cachedStub) {
logDebug(LOG_TAG$e, 'Creating Firestore stub.');
var credentials$1 = this.databaseInfo.ssl
? grpcJs.credentials.createSsl()
: grpcJs.credentials.createInsecure();
this.cachedStub = new this.firestore.Firestore(this.databaseInfo.host, credentials$1);
}
return this.cachedStub;
};
GrpcConnection.prototype.invokeRPC = function (rpcName, request, token) {
var stub = this.ensureActiveStub();
var metadata = createMetadata(this.databaseInfo, token);
return nodePromise(function (callback) {
logDebug(LOG_TAG$e, "RPC '" + rpcName + "' invoked with request:", request);
return stub[rpcName](request, metadata, function (grpcError, value) {
if (grpcError) {
logDebug(LOG_TAG$e, "RPC '" + rpcName + "' failed with error:", grpcError);
callback(new FirestoreError(mapCodeFromRpcCode(grpcError.code), grpcError.message));
}
else {
logDebug(LOG_TAG$e, "RPC '" + rpcName + "' completed with response:", value);
callback(undefined, value);
}
});
});
};
GrpcConnection.prototype.invokeStreamingRPC = function (rpcName, request, token) {
var results = [];
var responseDeferred = new Deferred();
logDebug(LOG_TAG$e, "RPC '" + rpcName + "' invoked (streaming) with request:", request);
var stub = this.ensureActiveStub();
var metadata = createMetadata(this.databaseInfo, token);
var stream = stub[rpcName](request, metadata);
stream.on('data', function (response) {
logDebug(LOG_TAG$e, "RPC " + rpcName + " received result:", response);
results.push(response);
});
stream.on('end', function () {
logDebug(LOG_TAG$e, "RPC '" + rpcName + "' completed.");
responseDeferred.resolve(results);
});
stream.on('error', function (grpcError) {
logDebug(LOG_TAG$e, "RPC '" + rpcName + "' failed with error:", grpcError);
var code = mapCodeFromRpcCode(grpcError.code);
responseDeferred.reject(new FirestoreError(code, grpcError.message));
});
return responseDeferred.promise;
};
// TODO(mikelehen): This "method" is a monster. Should be refactored.
GrpcConnection.prototype.openStream = function (rpcName, token) {
var stub = this.ensureActiveStub();
var metadata = createMetadata(this.databaseInfo, token);
var grpcStream = stub[rpcName](metadata);
var closed = false;
var close = function (err) {
if (!closed) {
closed = true;
stream.callOnClose(err);
grpcStream.end();
}
};
var stream = new StreamBridge({
sendFn: function (msg) {
if (!closed) {
logDebug(LOG_TAG$e, 'GRPC stream sending:', msg);
try {
grpcStream.write(msg);
}
catch (e) {
// This probably means we didn't conform to the proto. Make sure to
// log the message we sent.
logError('Failure sending:', msg);
logError('Error:', e);
throw e;
}
}
else {
logDebug(LOG_TAG$e, 'Not sending because gRPC stream is closed:', msg);
}
},
closeFn: function () {
logDebug(LOG_TAG$e, 'GRPC stream closed locally via close().');
close();
}
});
grpcStream.on('data', function (msg) {
if (!closed) {
logDebug(LOG_TAG$e, 'GRPC stream received:', msg);
stream.callOnMessage(msg);
}
});
grpcStream.on('end', function () {
logDebug(LOG_TAG$e, 'GRPC stream ended.');
close();
});
grpcStream.on('error', function (grpcError) {
logDebug(LOG_TAG$e, 'GRPC stream error. Code:', grpcError.code, 'Message:', grpcError.message);
var code = mapCodeFromRpcCode(grpcError.code);
close(new FirestoreError(code, grpcError.message));
});
logDebug(LOG_TAG$e, 'Opening GRPC stream');
// TODO(dimond): Since grpc has no explicit open status (or does it?) we
// simulate an onOpen in the next loop after the stream had it's listeners
// registered
setTimeout(function () {
stream.callOnOpen();
}, 0);
return stream;
};
return GrpcConnection;
}());
/**
* @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.
*/
/** Used by tests so we can match @grpc/proto-loader behavior. */
var protoLoaderOptions = {
longs: String,
enums: String,
defaults: true,
oneofs: false
};
/**
* Loads the protocol buffer definitions for Firestore.
*
* @returns The GrpcObject representing our protos.
*/
function loadProtos() {
var root = path.resolve(__dirname, "src/protos");
var firestoreProtoFile = path.join(root, 'google/firestore/v1/firestore.proto');
var packageDefinition = protoLoader.loadSync(firestoreProtoFile, Object.assign(Object.assign({}, protoLoaderOptions), { includeDirs: [root] }));
return grpcJs.loadPackageDefinition(packageDefinition);
}
/**
* @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.
*/
var NodePlatform = /** @class */ (function () {
function NodePlatform() {
this.base64Available = true;
this.document = null;
}
Object.defineProperty(NodePlatform.prototype, "window", {
get: function () {
if (process.env.USE_MOCK_PERSISTENCE === 'YES') {
// eslint-disable-next-line no-restricted-globals
return window;
}
return null;
},
enumerable: true,
configurable: true
});
NodePlatform.prototype.loadConnection = function (databaseInfo) {
var protos = loadProtos();
return Promise.resolve(new GrpcConnection(protos, databaseInfo));
};
NodePlatform.prototype.newConnectivityMonitor = function () {
return new NoopConnectivityMonitor();
};
NodePlatform.prototype.newSerializer = function (partitionId) {
return new JsonProtoSerializer(partitionId, { useProto3Json: false });
};
NodePlatform.prototype.formatJSON = function (value) {
// util.inspect() results in much more readable output than JSON.stringify()
return util$1.inspect(value, { depth: 100 });
};
NodePlatform.prototype.atob = function (encoded) {
// Node actually doesn't validate base64 strings.
// A quick sanity check that is not a fool-proof validation
if (/[^-A-Za-z0-9+/=]/.test(encoded)) {
throw new FirestoreError(Code.INVALID_ARGUMENT, 'Not a valid Base64 string: ' + encoded);
}
return new Buffer(encoded, 'base64').toString('binary');
};
NodePlatform.prototype.btoa = function (raw) {
return new Buffer(raw, 'binary').toString('base64');
};
NodePlatform.prototype.randomBytes = function (nBytes) {
debugAssert(nBytes >= 0, "Expecting non-negative nBytes, got: " + nBytes);
return crypto.randomBytes(nBytes);
};
return NodePlatform;
}());
/**
* @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.
*/
/**
* This code needs to run before Firestore is used. This can be achieved in
* several ways:
* 1) Through the JSCompiler compiling this code and then (automatically)
* executing it before exporting the Firestore symbols.
* 2) Through importing this module first in a Firestore main module
*/
PlatformSupport.setPlatform(new NodePlatform());
var name = "@firebase/firestore";
var version = "1.14.6";
/**
* @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.
*/
/**
* Registers the main Firestore Node build with the components framework.
* Persistence can be enabled via `firebase.firestore().enablePersistence()`.
*/
function registerFirestore(instance) {
configureForFirebase(instance, function (app, auth) { return new Firestore(app, auth, new IndexedDbComponentProvider()); });
instance.registerVersion(name, version, 'node');
}
registerFirestore(firebase);
exports.registerFirestore = registerFirestore;
//# sourceMappingURL=index.node.cjs.js.map