UNPKG

563 kBJavaScriptView Raw
1import Websocket from 'faye-websocket';
2import { stringify, jsonEval, contains, assert, isNodeSdk, base64, stringToByteArray, Sha1, deepCopy, base64Encode, isMobileCordova, stringLength, Deferred, safeGet, isAdmin, isValidFormat, isEmpty, isReactNative, assertionError, map, querystring, errorPrefix, getModularInstance, createMockUserToken } from '@firebase/util';
3import { Logger, LogLevel } from '@firebase/logger';
4import { getApp, _getProvider, SDK_VERSION as SDK_VERSION$1, _registerComponent, registerVersion } from '@firebase/app';
5import { Component } from '@firebase/component';
6
7/**
8 * @license
9 * Copyright 2017 Google LLC
10 *
11 * Licensed under the Apache License, Version 2.0 (the "License");
12 * you may not use this file except in compliance with the License.
13 * You may obtain a copy of the License at
14 *
15 * http://www.apache.org/licenses/LICENSE-2.0
16 *
17 * Unless required by applicable law or agreed to in writing, software
18 * distributed under the License is distributed on an "AS IS" BASIS,
19 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20 * See the License for the specific language governing permissions and
21 * limitations under the License.
22 */
23const PROTOCOL_VERSION = '5';
24const VERSION_PARAM = 'v';
25const TRANSPORT_SESSION_PARAM = 's';
26const REFERER_PARAM = 'r';
27const FORGE_REF = 'f';
28// Matches console.firebase.google.com, firebase-console-*.corp.google.com and
29// firebase.corp.google.com
30const FORGE_DOMAIN_RE = /(console\.firebase|firebase-console-\w+\.corp|firebase\.corp)\.google\.com/;
31const LAST_SESSION_PARAM = 'ls';
32const APPLICATION_ID_PARAM = 'p';
33const APP_CHECK_TOKEN_PARAM = 'ac';
34const WEBSOCKET = 'websocket';
35const LONG_POLLING = 'long_polling';
36
37/**
38 * @license
39 * Copyright 2017 Google LLC
40 *
41 * Licensed under the Apache License, Version 2.0 (the "License");
42 * you may not use this file except in compliance with the License.
43 * You may obtain a copy of the License at
44 *
45 * http://www.apache.org/licenses/LICENSE-2.0
46 *
47 * Unless required by applicable law or agreed to in writing, software
48 * distributed under the License is distributed on an "AS IS" BASIS,
49 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
50 * See the License for the specific language governing permissions and
51 * limitations under the License.
52 */
53/**
54 * Wraps a DOM Storage object and:
55 * - automatically encode objects as JSON strings before storing them to allow us to store arbitrary types.
56 * - prefixes names with "firebase:" to avoid collisions with app data.
57 *
58 * We automatically (see storage.js) create two such wrappers, one for sessionStorage,
59 * and one for localStorage.
60 *
61 */
62class DOMStorageWrapper {
63 /**
64 * @param domStorage_ - The underlying storage object (e.g. localStorage or sessionStorage)
65 */
66 constructor(domStorage_) {
67 this.domStorage_ = domStorage_;
68 // Use a prefix to avoid collisions with other stuff saved by the app.
69 this.prefix_ = 'firebase:';
70 }
71 /**
72 * @param key - The key to save the value under
73 * @param value - The value being stored, or null to remove the key.
74 */
75 set(key, value) {
76 if (value == null) {
77 this.domStorage_.removeItem(this.prefixedName_(key));
78 }
79 else {
80 this.domStorage_.setItem(this.prefixedName_(key), stringify(value));
81 }
82 }
83 /**
84 * @returns The value that was stored under this key, or null
85 */
86 get(key) {
87 const storedVal = this.domStorage_.getItem(this.prefixedName_(key));
88 if (storedVal == null) {
89 return null;
90 }
91 else {
92 return jsonEval(storedVal);
93 }
94 }
95 remove(key) {
96 this.domStorage_.removeItem(this.prefixedName_(key));
97 }
98 prefixedName_(name) {
99 return this.prefix_ + name;
100 }
101 toString() {
102 return this.domStorage_.toString();
103 }
104}
105
106/**
107 * @license
108 * Copyright 2017 Google LLC
109 *
110 * Licensed under the Apache License, Version 2.0 (the "License");
111 * you may not use this file except in compliance with the License.
112 * You may obtain a copy of the License at
113 *
114 * http://www.apache.org/licenses/LICENSE-2.0
115 *
116 * Unless required by applicable law or agreed to in writing, software
117 * distributed under the License is distributed on an "AS IS" BASIS,
118 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
119 * See the License for the specific language governing permissions and
120 * limitations under the License.
121 */
122/**
123 * An in-memory storage implementation that matches the API of DOMStorageWrapper
124 * (TODO: create interface for both to implement).
125 */
126class MemoryStorage {
127 constructor() {
128 this.cache_ = {};
129 this.isInMemoryStorage = true;
130 }
131 set(key, value) {
132 if (value == null) {
133 delete this.cache_[key];
134 }
135 else {
136 this.cache_[key] = value;
137 }
138 }
139 get(key) {
140 if (contains(this.cache_, key)) {
141 return this.cache_[key];
142 }
143 return null;
144 }
145 remove(key) {
146 delete this.cache_[key];
147 }
148}
149
150/**
151 * @license
152 * Copyright 2017 Google LLC
153 *
154 * Licensed under the Apache License, Version 2.0 (the "License");
155 * you may not use this file except in compliance with the License.
156 * You may obtain a copy of the License at
157 *
158 * http://www.apache.org/licenses/LICENSE-2.0
159 *
160 * Unless required by applicable law or agreed to in writing, software
161 * distributed under the License is distributed on an "AS IS" BASIS,
162 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
163 * See the License for the specific language governing permissions and
164 * limitations under the License.
165 */
166/**
167 * Helper to create a DOMStorageWrapper or else fall back to MemoryStorage.
168 * TODO: Once MemoryStorage and DOMStorageWrapper have a shared interface this method annotation should change
169 * to reflect this type
170 *
171 * @param domStorageName - Name of the underlying storage object
172 * (e.g. 'localStorage' or 'sessionStorage').
173 * @returns Turning off type information until a common interface is defined.
174 */
175const createStoragefor = function (domStorageName) {
176 try {
177 // NOTE: just accessing "localStorage" or "window['localStorage']" may throw a security exception,
178 // so it must be inside the try/catch.
179 if (typeof window !== 'undefined' &&
180 typeof window[domStorageName] !== 'undefined') {
181 // Need to test cache. Just because it's here doesn't mean it works
182 const domStorage = window[domStorageName];
183 domStorage.setItem('firebase:sentinel', 'cache');
184 domStorage.removeItem('firebase:sentinel');
185 return new DOMStorageWrapper(domStorage);
186 }
187 }
188 catch (e) { }
189 // Failed to create wrapper. Just return in-memory storage.
190 // TODO: log?
191 return new MemoryStorage();
192};
193/** A storage object that lasts across sessions */
194const PersistentStorage = createStoragefor('localStorage');
195/** A storage object that only lasts one session */
196const SessionStorage = createStoragefor('sessionStorage');
197
198/**
199 * @license
200 * Copyright 2017 Google LLC
201 *
202 * Licensed under the Apache License, Version 2.0 (the "License");
203 * you may not use this file except in compliance with the License.
204 * You may obtain a copy of the License at
205 *
206 * http://www.apache.org/licenses/LICENSE-2.0
207 *
208 * Unless required by applicable law or agreed to in writing, software
209 * distributed under the License is distributed on an "AS IS" BASIS,
210 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
211 * See the License for the specific language governing permissions and
212 * limitations under the License.
213 */
214const logClient = new Logger('@firebase/database');
215/**
216 * Returns a locally-unique ID (generated by just incrementing up from 0 each time its called).
217 */
218const LUIDGenerator = (function () {
219 let id = 1;
220 return function () {
221 return id++;
222 };
223})();
224/**
225 * Sha1 hash of the input string
226 * @param str - The string to hash
227 * @returns {!string} The resulting hash
228 */
229const sha1 = function (str) {
230 const utf8Bytes = stringToByteArray(str);
231 const sha1 = new Sha1();
232 sha1.update(utf8Bytes);
233 const sha1Bytes = sha1.digest();
234 return base64.encodeByteArray(sha1Bytes);
235};
236const buildLogMessage_ = function (...varArgs) {
237 let message = '';
238 for (let i = 0; i < varArgs.length; i++) {
239 const arg = varArgs[i];
240 if (Array.isArray(arg) ||
241 (arg &&
242 typeof arg === 'object' &&
243 // eslint-disable-next-line @typescript-eslint/no-explicit-any
244 typeof arg.length === 'number')) {
245 message += buildLogMessage_.apply(null, arg);
246 }
247 else if (typeof arg === 'object') {
248 message += stringify(arg);
249 }
250 else {
251 message += arg;
252 }
253 message += ' ';
254 }
255 return message;
256};
257/**
258 * Use this for all debug messages in Firebase.
259 */
260let logger = null;
261/**
262 * Flag to check for log availability on first log message
263 */
264let firstLog_ = true;
265/**
266 * The implementation of Firebase.enableLogging (defined here to break dependencies)
267 * @param logger_ - A flag to turn on logging, or a custom logger
268 * @param persistent - Whether or not to persist logging settings across refreshes
269 */
270const enableLogging$1 = function (logger_, persistent) {
271 assert(!persistent || logger_ === true || logger_ === false, "Can't turn on custom loggers persistently.");
272 if (logger_ === true) {
273 logClient.logLevel = LogLevel.VERBOSE;
274 logger = logClient.log.bind(logClient);
275 if (persistent) {
276 SessionStorage.set('logging_enabled', true);
277 }
278 }
279 else if (typeof logger_ === 'function') {
280 logger = logger_;
281 }
282 else {
283 logger = null;
284 SessionStorage.remove('logging_enabled');
285 }
286};
287const log = function (...varArgs) {
288 if (firstLog_ === true) {
289 firstLog_ = false;
290 if (logger === null && SessionStorage.get('logging_enabled') === true) {
291 enableLogging$1(true);
292 }
293 }
294 if (logger) {
295 const message = buildLogMessage_.apply(null, varArgs);
296 logger(message);
297 }
298};
299const logWrapper = function (prefix) {
300 return function (...varArgs) {
301 log(prefix, ...varArgs);
302 };
303};
304const error = function (...varArgs) {
305 const message = 'FIREBASE INTERNAL ERROR: ' + buildLogMessage_(...varArgs);
306 logClient.error(message);
307};
308const fatal = function (...varArgs) {
309 const message = `FIREBASE FATAL ERROR: ${buildLogMessage_(...varArgs)}`;
310 logClient.error(message);
311 throw new Error(message);
312};
313const warn = function (...varArgs) {
314 const message = 'FIREBASE WARNING: ' + buildLogMessage_(...varArgs);
315 logClient.warn(message);
316};
317/**
318 * Logs a warning if the containing page uses https. Called when a call to new Firebase
319 * does not use https.
320 */
321const warnIfPageIsSecure = function () {
322 // Be very careful accessing browser globals. Who knows what may or may not exist.
323 if (typeof window !== 'undefined' &&
324 window.location &&
325 window.location.protocol &&
326 window.location.protocol.indexOf('https:') !== -1) {
327 warn('Insecure Firebase access from a secure page. ' +
328 'Please use https in calls to new Firebase().');
329 }
330};
331/**
332 * Returns true if data is NaN, or +/- Infinity.
333 */
334const isInvalidJSONNumber = function (data) {
335 return (typeof data === 'number' &&
336 (data !== data || // NaN
337 data === Number.POSITIVE_INFINITY ||
338 data === Number.NEGATIVE_INFINITY));
339};
340const executeWhenDOMReady = function (fn) {
341 if (isNodeSdk() || document.readyState === 'complete') {
342 fn();
343 }
344 else {
345 // Modeled after jQuery. Try DOMContentLoaded and onreadystatechange (which
346 // fire before onload), but fall back to onload.
347 let called = false;
348 const wrappedFn = function () {
349 if (!document.body) {
350 setTimeout(wrappedFn, Math.floor(10));
351 return;
352 }
353 if (!called) {
354 called = true;
355 fn();
356 }
357 };
358 if (document.addEventListener) {
359 document.addEventListener('DOMContentLoaded', wrappedFn, false);
360 // fallback to onload.
361 window.addEventListener('load', wrappedFn, false);
362 // eslint-disable-next-line @typescript-eslint/no-explicit-any
363 }
364 else if (document.attachEvent) {
365 // IE.
366 // eslint-disable-next-line @typescript-eslint/no-explicit-any
367 document.attachEvent('onreadystatechange', () => {
368 if (document.readyState === 'complete') {
369 wrappedFn();
370 }
371 });
372 // fallback to onload.
373 // eslint-disable-next-line @typescript-eslint/no-explicit-any
374 window.attachEvent('onload', wrappedFn);
375 // jQuery has an extra hack for IE that we could employ (based on
376 // http://javascript.nwbox.com/IEContentLoaded/) But it looks really old.
377 // I'm hoping we don't need it.
378 }
379 }
380};
381/**
382 * Minimum key name. Invalid for actual data, used as a marker to sort before any valid names
383 */
384const MIN_NAME = '[MIN_NAME]';
385/**
386 * Maximum key name. Invalid for actual data, used as a marker to sort above any valid names
387 */
388const MAX_NAME = '[MAX_NAME]';
389/**
390 * Compares valid Firebase key names, plus min and max name
391 */
392const nameCompare = function (a, b) {
393 if (a === b) {
394 return 0;
395 }
396 else if (a === MIN_NAME || b === MAX_NAME) {
397 return -1;
398 }
399 else if (b === MIN_NAME || a === MAX_NAME) {
400 return 1;
401 }
402 else {
403 const aAsInt = tryParseInt(a), bAsInt = tryParseInt(b);
404 if (aAsInt !== null) {
405 if (bAsInt !== null) {
406 return aAsInt - bAsInt === 0 ? a.length - b.length : aAsInt - bAsInt;
407 }
408 else {
409 return -1;
410 }
411 }
412 else if (bAsInt !== null) {
413 return 1;
414 }
415 else {
416 return a < b ? -1 : 1;
417 }
418 }
419};
420/**
421 * @returns {!number} comparison result.
422 */
423const stringCompare = function (a, b) {
424 if (a === b) {
425 return 0;
426 }
427 else if (a < b) {
428 return -1;
429 }
430 else {
431 return 1;
432 }
433};
434const requireKey = function (key, obj) {
435 if (obj && key in obj) {
436 return obj[key];
437 }
438 else {
439 throw new Error('Missing required key (' + key + ') in object: ' + stringify(obj));
440 }
441};
442const ObjectToUniqueKey = function (obj) {
443 if (typeof obj !== 'object' || obj === null) {
444 return stringify(obj);
445 }
446 const keys = [];
447 // eslint-disable-next-line guard-for-in
448 for (const k in obj) {
449 keys.push(k);
450 }
451 // Export as json, but with the keys sorted.
452 keys.sort();
453 let key = '{';
454 for (let i = 0; i < keys.length; i++) {
455 if (i !== 0) {
456 key += ',';
457 }
458 key += stringify(keys[i]);
459 key += ':';
460 key += ObjectToUniqueKey(obj[keys[i]]);
461 }
462 key += '}';
463 return key;
464};
465/**
466 * Splits a string into a number of smaller segments of maximum size
467 * @param str - The string
468 * @param segsize - The maximum number of chars in the string.
469 * @returns The string, split into appropriately-sized chunks
470 */
471const splitStringBySize = function (str, segsize) {
472 const len = str.length;
473 if (len <= segsize) {
474 return [str];
475 }
476 const dataSegs = [];
477 for (let c = 0; c < len; c += segsize) {
478 if (c + segsize > len) {
479 dataSegs.push(str.substring(c, len));
480 }
481 else {
482 dataSegs.push(str.substring(c, c + segsize));
483 }
484 }
485 return dataSegs;
486};
487/**
488 * Apply a function to each (key, value) pair in an object or
489 * apply a function to each (index, value) pair in an array
490 * @param obj - The object or array to iterate over
491 * @param fn - The function to apply
492 */
493function each(obj, fn) {
494 for (const key in obj) {
495 if (obj.hasOwnProperty(key)) {
496 fn(key, obj[key]);
497 }
498 }
499}
500/**
501 * Borrowed from http://hg.secondlife.com/llsd/src/tip/js/typedarray.js (MIT License)
502 * I made one modification at the end and removed the NaN / Infinity
503 * handling (since it seemed broken [caused an overflow] and we don't need it). See MJL comments.
504 * @param v - A double
505 *
506 */
507const doubleToIEEE754String = function (v) {
508 assert(!isInvalidJSONNumber(v), 'Invalid JSON number'); // MJL
509 const ebits = 11, fbits = 52;
510 const bias = (1 << (ebits - 1)) - 1;
511 let s, e, f, ln, i;
512 // Compute sign, exponent, fraction
513 // Skip NaN / Infinity handling --MJL.
514 if (v === 0) {
515 e = 0;
516 f = 0;
517 s = 1 / v === -Infinity ? 1 : 0;
518 }
519 else {
520 s = v < 0;
521 v = Math.abs(v);
522 if (v >= Math.pow(2, 1 - bias)) {
523 // Normalized
524 ln = Math.min(Math.floor(Math.log(v) / Math.LN2), bias);
525 e = ln + bias;
526 f = Math.round(v * Math.pow(2, fbits - ln) - Math.pow(2, fbits));
527 }
528 else {
529 // Denormalized
530 e = 0;
531 f = Math.round(v / Math.pow(2, 1 - bias - fbits));
532 }
533 }
534 // Pack sign, exponent, fraction
535 const bits = [];
536 for (i = fbits; i; i -= 1) {
537 bits.push(f % 2 ? 1 : 0);
538 f = Math.floor(f / 2);
539 }
540 for (i = ebits; i; i -= 1) {
541 bits.push(e % 2 ? 1 : 0);
542 e = Math.floor(e / 2);
543 }
544 bits.push(s ? 1 : 0);
545 bits.reverse();
546 const str = bits.join('');
547 // Return the data as a hex string. --MJL
548 let hexByteString = '';
549 for (i = 0; i < 64; i += 8) {
550 let hexByte = parseInt(str.substr(i, 8), 2).toString(16);
551 if (hexByte.length === 1) {
552 hexByte = '0' + hexByte;
553 }
554 hexByteString = hexByteString + hexByte;
555 }
556 return hexByteString.toLowerCase();
557};
558/**
559 * Used to detect if we're in a Chrome content script (which executes in an
560 * isolated environment where long-polling doesn't work).
561 */
562const isChromeExtensionContentScript = function () {
563 return !!(typeof window === 'object' &&
564 window['chrome'] &&
565 window['chrome']['extension'] &&
566 !/^chrome/.test(window.location.href));
567};
568/**
569 * Used to detect if we're in a Windows 8 Store app.
570 */
571const isWindowsStoreApp = function () {
572 // Check for the presence of a couple WinRT globals
573 return typeof Windows === 'object' && typeof Windows.UI === 'object';
574};
575/**
576 * Converts a server error code to a Javascript Error
577 */
578function errorForServerCode(code, query) {
579 let reason = 'Unknown Error';
580 if (code === 'too_big') {
581 reason =
582 'The data requested exceeds the maximum size ' +
583 'that can be accessed with a single request.';
584 }
585 else if (code === 'permission_denied') {
586 reason = "Client doesn't have permission to access the desired data.";
587 }
588 else if (code === 'unavailable') {
589 reason = 'The service is unavailable';
590 }
591 const error = new Error(code + ' at ' + query._path.toString() + ': ' + reason);
592 // eslint-disable-next-line @typescript-eslint/no-explicit-any
593 error.code = code.toUpperCase();
594 return error;
595}
596/**
597 * Used to test for integer-looking strings
598 */
599const INTEGER_REGEXP_ = new RegExp('^-?(0*)\\d{1,10}$');
600/**
601 * For use in keys, the minimum possible 32-bit integer.
602 */
603const INTEGER_32_MIN = -2147483648;
604/**
605 * For use in kyes, the maximum possible 32-bit integer.
606 */
607const INTEGER_32_MAX = 2147483647;
608/**
609 * If the string contains a 32-bit integer, return it. Else return null.
610 */
611const tryParseInt = function (str) {
612 if (INTEGER_REGEXP_.test(str)) {
613 const intVal = Number(str);
614 if (intVal >= INTEGER_32_MIN && intVal <= INTEGER_32_MAX) {
615 return intVal;
616 }
617 }
618 return null;
619};
620/**
621 * Helper to run some code but catch any exceptions and re-throw them later.
622 * Useful for preventing user callbacks from breaking internal code.
623 *
624 * Re-throwing the exception from a setTimeout is a little evil, but it's very
625 * convenient (we don't have to try to figure out when is a safe point to
626 * re-throw it), and the behavior seems reasonable:
627 *
628 * * If you aren't pausing on exceptions, you get an error in the console with
629 * the correct stack trace.
630 * * If you're pausing on all exceptions, the debugger will pause on your
631 * exception and then again when we rethrow it.
632 * * If you're only pausing on uncaught exceptions, the debugger will only pause
633 * on us re-throwing it.
634 *
635 * @param fn - The code to guard.
636 */
637const exceptionGuard = function (fn) {
638 try {
639 fn();
640 }
641 catch (e) {
642 // Re-throw exception when it's safe.
643 setTimeout(() => {
644 // It used to be that "throw e" would result in a good console error with
645 // relevant context, but as of Chrome 39, you just get the firebase.js
646 // file/line number where we re-throw it, which is useless. So we log
647 // e.stack explicitly.
648 const stack = e.stack || '';
649 warn('Exception was thrown by user callback.', stack);
650 throw e;
651 }, Math.floor(0));
652 }
653};
654/**
655 * @returns {boolean} true if we think we're currently being crawled.
656 */
657const beingCrawled = function () {
658 const userAgent = (typeof window === 'object' &&
659 window['navigator'] &&
660 window['navigator']['userAgent']) ||
661 '';
662 // For now we whitelist the most popular crawlers. We should refine this to be the set of crawlers we
663 // believe to support JavaScript/AJAX rendering.
664 // NOTE: Google Webmaster Tools doesn't really belong, but their "This is how a visitor to your website
665 // would have seen the page" is flaky if we don't treat it as a crawler.
666 return (userAgent.search(/googlebot|google webmaster tools|bingbot|yahoo! slurp|baiduspider|yandexbot|duckduckbot/i) >= 0);
667};
668/**
669 * Same as setTimeout() except on Node.JS it will /not/ prevent the process from exiting.
670 *
671 * It is removed with clearTimeout() as normal.
672 *
673 * @param fn - Function to run.
674 * @param time - Milliseconds to wait before running.
675 * @returns The setTimeout() return value.
676 */
677const setTimeoutNonBlocking = function (fn, time) {
678 const timeout = setTimeout(fn, time);
679 // eslint-disable-next-line @typescript-eslint/no-explicit-any
680 if (typeof timeout === 'object' && timeout['unref']) {
681 // eslint-disable-next-line @typescript-eslint/no-explicit-any
682 timeout['unref']();
683 }
684 return timeout;
685};
686
687/**
688 * @license
689 * Copyright 2017 Google LLC
690 *
691 * Licensed under the Apache License, Version 2.0 (the "License");
692 * you may not use this file except in compliance with the License.
693 * You may obtain a copy of the License at
694 *
695 * http://www.apache.org/licenses/LICENSE-2.0
696 *
697 * Unless required by applicable law or agreed to in writing, software
698 * distributed under the License is distributed on an "AS IS" BASIS,
699 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
700 * See the License for the specific language governing permissions and
701 * limitations under the License.
702 */
703/**
704 * A class that holds metadata about a Repo object
705 */
706class RepoInfo {
707 /**
708 * @param host - Hostname portion of the url for the repo
709 * @param secure - Whether or not this repo is accessed over ssl
710 * @param namespace - The namespace represented by the repo
711 * @param webSocketOnly - Whether to prefer websockets over all other transports (used by Nest).
712 * @param nodeAdmin - Whether this instance uses Admin SDK credentials
713 * @param persistenceKey - Override the default session persistence storage key
714 */
715 constructor(host, secure, namespace, webSocketOnly, nodeAdmin = false, persistenceKey = '', includeNamespaceInQueryParams = false) {
716 this.secure = secure;
717 this.namespace = namespace;
718 this.webSocketOnly = webSocketOnly;
719 this.nodeAdmin = nodeAdmin;
720 this.persistenceKey = persistenceKey;
721 this.includeNamespaceInQueryParams = includeNamespaceInQueryParams;
722 this._host = host.toLowerCase();
723 this._domain = this._host.substr(this._host.indexOf('.') + 1);
724 this.internalHost =
725 PersistentStorage.get('host:' + host) || this._host;
726 }
727 isCacheableHost() {
728 return this.internalHost.substr(0, 2) === 's-';
729 }
730 isCustomHost() {
731 return (this._domain !== 'firebaseio.com' &&
732 this._domain !== 'firebaseio-demo.com');
733 }
734 get host() {
735 return this._host;
736 }
737 set host(newHost) {
738 if (newHost !== this.internalHost) {
739 this.internalHost = newHost;
740 if (this.isCacheableHost()) {
741 PersistentStorage.set('host:' + this._host, this.internalHost);
742 }
743 }
744 }
745 toString() {
746 let str = this.toURLString();
747 if (this.persistenceKey) {
748 str += '<' + this.persistenceKey + '>';
749 }
750 return str;
751 }
752 toURLString() {
753 const protocol = this.secure ? 'https://' : 'http://';
754 const query = this.includeNamespaceInQueryParams
755 ? `?ns=${this.namespace}`
756 : '';
757 return `${protocol}${this.host}/${query}`;
758 }
759}
760function repoInfoNeedsQueryParam(repoInfo) {
761 return (repoInfo.host !== repoInfo.internalHost ||
762 repoInfo.isCustomHost() ||
763 repoInfo.includeNamespaceInQueryParams);
764}
765/**
766 * Returns the websocket URL for this repo
767 * @param repoInfo - RepoInfo object
768 * @param type - of connection
769 * @param params - list
770 * @returns The URL for this repo
771 */
772function repoInfoConnectionURL(repoInfo, type, params) {
773 assert(typeof type === 'string', 'typeof type must == string');
774 assert(typeof params === 'object', 'typeof params must == object');
775 let connURL;
776 if (type === WEBSOCKET) {
777 connURL =
778 (repoInfo.secure ? 'wss://' : 'ws://') + repoInfo.internalHost + '/.ws?';
779 }
780 else if (type === LONG_POLLING) {
781 connURL =
782 (repoInfo.secure ? 'https://' : 'http://') +
783 repoInfo.internalHost +
784 '/.lp?';
785 }
786 else {
787 throw new Error('Unknown connection type: ' + type);
788 }
789 if (repoInfoNeedsQueryParam(repoInfo)) {
790 params['ns'] = repoInfo.namespace;
791 }
792 const pairs = [];
793 each(params, (key, value) => {
794 pairs.push(key + '=' + value);
795 });
796 return connURL + pairs.join('&');
797}
798
799/**
800 * @license
801 * Copyright 2017 Google LLC
802 *
803 * Licensed under the Apache License, Version 2.0 (the "License");
804 * you may not use this file except in compliance with the License.
805 * You may obtain a copy of the License at
806 *
807 * http://www.apache.org/licenses/LICENSE-2.0
808 *
809 * Unless required by applicable law or agreed to in writing, software
810 * distributed under the License is distributed on an "AS IS" BASIS,
811 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
812 * See the License for the specific language governing permissions and
813 * limitations under the License.
814 */
815/**
816 * Tracks a collection of stats.
817 */
818class StatsCollection {
819 constructor() {
820 this.counters_ = {};
821 }
822 incrementCounter(name, amount = 1) {
823 if (!contains(this.counters_, name)) {
824 this.counters_[name] = 0;
825 }
826 this.counters_[name] += amount;
827 }
828 get() {
829 return deepCopy(this.counters_);
830 }
831}
832
833/**
834 * @license
835 * Copyright 2017 Google LLC
836 *
837 * Licensed under the Apache License, Version 2.0 (the "License");
838 * you may not use this file except in compliance with the License.
839 * You may obtain a copy of the License at
840 *
841 * http://www.apache.org/licenses/LICENSE-2.0
842 *
843 * Unless required by applicable law or agreed to in writing, software
844 * distributed under the License is distributed on an "AS IS" BASIS,
845 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
846 * See the License for the specific language governing permissions and
847 * limitations under the License.
848 */
849const collections = {};
850const reporters = {};
851function statsManagerGetCollection(repoInfo) {
852 const hashString = repoInfo.toString();
853 if (!collections[hashString]) {
854 collections[hashString] = new StatsCollection();
855 }
856 return collections[hashString];
857}
858function statsManagerGetOrCreateReporter(repoInfo, creatorFunction) {
859 const hashString = repoInfo.toString();
860 if (!reporters[hashString]) {
861 reporters[hashString] = creatorFunction();
862 }
863 return reporters[hashString];
864}
865
866/**
867 * @license
868 * Copyright 2019 Google LLC
869 *
870 * Licensed under the Apache License, Version 2.0 (the "License");
871 * you may not use this file except in compliance with the License.
872 * You may obtain a copy of the License at
873 *
874 * http://www.apache.org/licenses/LICENSE-2.0
875 *
876 * Unless required by applicable law or agreed to in writing, software
877 * distributed under the License is distributed on an "AS IS" BASIS,
878 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
879 * See the License for the specific language governing permissions and
880 * limitations under the License.
881 */
882/** The semver (www.semver.org) version of the SDK. */
883let SDK_VERSION = '';
884/**
885 * SDK_VERSION should be set before any database instance is created
886 * @internal
887 */
888function setSDKVersion(version) {
889 SDK_VERSION = version;
890}
891
892/**
893 * @license
894 * Copyright 2017 Google LLC
895 *
896 * Licensed under the Apache License, Version 2.0 (the "License");
897 * you may not use this file except in compliance with the License.
898 * You may obtain a copy of the License at
899 *
900 * http://www.apache.org/licenses/LICENSE-2.0
901 *
902 * Unless required by applicable law or agreed to in writing, software
903 * distributed under the License is distributed on an "AS IS" BASIS,
904 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
905 * See the License for the specific language governing permissions and
906 * limitations under the License.
907 */
908const WEBSOCKET_MAX_FRAME_SIZE = 16384;
909const WEBSOCKET_KEEPALIVE_INTERVAL = 45000;
910let WebSocketImpl = null;
911if (typeof MozWebSocket !== 'undefined') {
912 WebSocketImpl = MozWebSocket;
913}
914else if (typeof WebSocket !== 'undefined') {
915 WebSocketImpl = WebSocket;
916}
917function setWebSocketImpl(impl) {
918 WebSocketImpl = impl;
919}
920/**
921 * Create a new websocket connection with the given callbacks.
922 */
923class WebSocketConnection {
924 /**
925 * @param connId identifier for this transport
926 * @param repoInfo The info for the websocket endpoint.
927 * @param applicationId The Firebase App ID for this project.
928 * @param appCheckToken The App Check Token for this client.
929 * @param authToken The Auth Token for this client.
930 * @param transportSessionId Optional transportSessionId if this is connecting
931 * to an existing transport session
932 * @param lastSessionId Optional lastSessionId if there was a previous
933 * connection
934 */
935 constructor(connId, repoInfo, applicationId, appCheckToken, authToken, transportSessionId, lastSessionId) {
936 this.connId = connId;
937 this.applicationId = applicationId;
938 this.appCheckToken = appCheckToken;
939 this.authToken = authToken;
940 this.keepaliveTimer = null;
941 this.frames = null;
942 this.totalFrames = 0;
943 this.bytesSent = 0;
944 this.bytesReceived = 0;
945 this.log_ = logWrapper(this.connId);
946 this.stats_ = statsManagerGetCollection(repoInfo);
947 this.connURL = WebSocketConnection.connectionURL_(repoInfo, transportSessionId, lastSessionId, appCheckToken, applicationId);
948 this.nodeAdmin = repoInfo.nodeAdmin;
949 }
950 /**
951 * @param repoInfo - The info for the websocket endpoint.
952 * @param transportSessionId - Optional transportSessionId if this is connecting to an existing transport
953 * session
954 * @param lastSessionId - Optional lastSessionId if there was a previous connection
955 * @returns connection url
956 */
957 static connectionURL_(repoInfo, transportSessionId, lastSessionId, appCheckToken, applicationId) {
958 const urlParams = {};
959 urlParams[VERSION_PARAM] = PROTOCOL_VERSION;
960 if (!isNodeSdk() &&
961 typeof location !== 'undefined' &&
962 location.hostname &&
963 FORGE_DOMAIN_RE.test(location.hostname)) {
964 urlParams[REFERER_PARAM] = FORGE_REF;
965 }
966 if (transportSessionId) {
967 urlParams[TRANSPORT_SESSION_PARAM] = transportSessionId;
968 }
969 if (lastSessionId) {
970 urlParams[LAST_SESSION_PARAM] = lastSessionId;
971 }
972 if (appCheckToken) {
973 urlParams[APP_CHECK_TOKEN_PARAM] = appCheckToken;
974 }
975 if (applicationId) {
976 urlParams[APPLICATION_ID_PARAM] = applicationId;
977 }
978 return repoInfoConnectionURL(repoInfo, WEBSOCKET, urlParams);
979 }
980 /**
981 * @param onMessage - Callback when messages arrive
982 * @param onDisconnect - Callback with connection lost.
983 */
984 open(onMessage, onDisconnect) {
985 this.onDisconnect = onDisconnect;
986 this.onMessage = onMessage;
987 this.log_('Websocket connecting to ' + this.connURL);
988 this.everConnected_ = false;
989 // Assume failure until proven otherwise.
990 PersistentStorage.set('previous_websocket_failure', true);
991 try {
992 let options;
993 if (isNodeSdk()) {
994 const device = this.nodeAdmin ? 'AdminNode' : 'Node';
995 // UA Format: Firebase/<wire_protocol>/<sdk_version>/<platform>/<device>
996 const options = {
997 headers: {
998 'User-Agent': `Firebase/${PROTOCOL_VERSION}/${SDK_VERSION}/${process.platform}/${device}`,
999 'X-Firebase-GMPID': this.applicationId || ''
1000 }
1001 };
1002 // If using Node with admin creds, AppCheck-related checks are unnecessary.
1003 // Note that we send the credentials here even if they aren't admin credentials, which is
1004 // not a problem.
1005 // Note that this header is just used to bypass appcheck, and the token should still be sent
1006 // through the websocket connection once it is established.
1007 if (this.authToken) {
1008 options.headers['Authorization'] = `Bearer ${this.authToken}`;
1009 }
1010 if (this.appCheckToken) {
1011 options.headers['X-Firebase-AppCheck'] = this.appCheckToken;
1012 }
1013 // Plumb appropriate http_proxy environment variable into faye-websocket if it exists.
1014 const env = process['env'];
1015 const proxy = this.connURL.indexOf('wss://') === 0
1016 ? env['HTTPS_PROXY'] || env['https_proxy']
1017 : env['HTTP_PROXY'] || env['http_proxy'];
1018 if (proxy) {
1019 options['proxy'] = { origin: proxy };
1020 }
1021 }
1022 this.mySock = new WebSocketImpl(this.connURL, [], options);
1023 }
1024 catch (e) {
1025 this.log_('Error instantiating WebSocket.');
1026 const error = e.message || e.data;
1027 if (error) {
1028 this.log_(error);
1029 }
1030 this.onClosed_();
1031 return;
1032 }
1033 this.mySock.onopen = () => {
1034 this.log_('Websocket connected.');
1035 this.everConnected_ = true;
1036 };
1037 this.mySock.onclose = () => {
1038 this.log_('Websocket connection was disconnected.');
1039 this.mySock = null;
1040 this.onClosed_();
1041 };
1042 this.mySock.onmessage = m => {
1043 this.handleIncomingFrame(m);
1044 };
1045 this.mySock.onerror = e => {
1046 this.log_('WebSocket error. Closing connection.');
1047 // eslint-disable-next-line @typescript-eslint/no-explicit-any
1048 const error = e.message || e.data;
1049 if (error) {
1050 this.log_(error);
1051 }
1052 this.onClosed_();
1053 };
1054 }
1055 /**
1056 * No-op for websockets, we don't need to do anything once the connection is confirmed as open
1057 */
1058 start() { }
1059 static forceDisallow() {
1060 WebSocketConnection.forceDisallow_ = true;
1061 }
1062 static isAvailable() {
1063 let isOldAndroid = false;
1064 if (typeof navigator !== 'undefined' && navigator.userAgent) {
1065 const oldAndroidRegex = /Android ([0-9]{0,}\.[0-9]{0,})/;
1066 const oldAndroidMatch = navigator.userAgent.match(oldAndroidRegex);
1067 if (oldAndroidMatch && oldAndroidMatch.length > 1) {
1068 if (parseFloat(oldAndroidMatch[1]) < 4.4) {
1069 isOldAndroid = true;
1070 }
1071 }
1072 }
1073 return (!isOldAndroid &&
1074 WebSocketImpl !== null &&
1075 !WebSocketConnection.forceDisallow_);
1076 }
1077 /**
1078 * Returns true if we previously failed to connect with this transport.
1079 */
1080 static previouslyFailed() {
1081 // If our persistent storage is actually only in-memory storage,
1082 // we default to assuming that it previously failed to be safe.
1083 return (PersistentStorage.isInMemoryStorage ||
1084 PersistentStorage.get('previous_websocket_failure') === true);
1085 }
1086 markConnectionHealthy() {
1087 PersistentStorage.remove('previous_websocket_failure');
1088 }
1089 appendFrame_(data) {
1090 this.frames.push(data);
1091 if (this.frames.length === this.totalFrames) {
1092 const fullMess = this.frames.join('');
1093 this.frames = null;
1094 const jsonMess = jsonEval(fullMess);
1095 //handle the message
1096 this.onMessage(jsonMess);
1097 }
1098 }
1099 /**
1100 * @param frameCount - The number of frames we are expecting from the server
1101 */
1102 handleNewFrameCount_(frameCount) {
1103 this.totalFrames = frameCount;
1104 this.frames = [];
1105 }
1106 /**
1107 * Attempts to parse a frame count out of some text. If it can't, assumes a value of 1
1108 * @returns Any remaining data to be process, or null if there is none
1109 */
1110 extractFrameCount_(data) {
1111 assert(this.frames === null, 'We already have a frame buffer');
1112 // TODO: The server is only supposed to send up to 9999 frames (i.e. length <= 4), but that isn't being enforced
1113 // currently. So allowing larger frame counts (length <= 6). See https://app.asana.com/0/search/8688598998380/8237608042508
1114 if (data.length <= 6) {
1115 const frameCount = Number(data);
1116 if (!isNaN(frameCount)) {
1117 this.handleNewFrameCount_(frameCount);
1118 return null;
1119 }
1120 }
1121 this.handleNewFrameCount_(1);
1122 return data;
1123 }
1124 /**
1125 * Process a websocket frame that has arrived from the server.
1126 * @param mess - The frame data
1127 */
1128 handleIncomingFrame(mess) {
1129 if (this.mySock === null) {
1130 return; // Chrome apparently delivers incoming packets even after we .close() the connection sometimes.
1131 }
1132 const data = mess['data'];
1133 this.bytesReceived += data.length;
1134 this.stats_.incrementCounter('bytes_received', data.length);
1135 this.resetKeepAlive();
1136 if (this.frames !== null) {
1137 // we're buffering
1138 this.appendFrame_(data);
1139 }
1140 else {
1141 // try to parse out a frame count, otherwise, assume 1 and process it
1142 const remainingData = this.extractFrameCount_(data);
1143 if (remainingData !== null) {
1144 this.appendFrame_(remainingData);
1145 }
1146 }
1147 }
1148 /**
1149 * Send a message to the server
1150 * @param data - The JSON object to transmit
1151 */
1152 send(data) {
1153 this.resetKeepAlive();
1154 const dataStr = stringify(data);
1155 this.bytesSent += dataStr.length;
1156 this.stats_.incrementCounter('bytes_sent', dataStr.length);
1157 //We can only fit a certain amount in each websocket frame, so we need to split this request
1158 //up into multiple pieces if it doesn't fit in one request.
1159 const dataSegs = splitStringBySize(dataStr, WEBSOCKET_MAX_FRAME_SIZE);
1160 //Send the length header
1161 if (dataSegs.length > 1) {
1162 this.sendString_(String(dataSegs.length));
1163 }
1164 //Send the actual data in segments.
1165 for (let i = 0; i < dataSegs.length; i++) {
1166 this.sendString_(dataSegs[i]);
1167 }
1168 }
1169 shutdown_() {
1170 this.isClosed_ = true;
1171 if (this.keepaliveTimer) {
1172 clearInterval(this.keepaliveTimer);
1173 this.keepaliveTimer = null;
1174 }
1175 if (this.mySock) {
1176 this.mySock.close();
1177 this.mySock = null;
1178 }
1179 }
1180 onClosed_() {
1181 if (!this.isClosed_) {
1182 this.log_('WebSocket is closing itself');
1183 this.shutdown_();
1184 // since this is an internal close, trigger the close listener
1185 if (this.onDisconnect) {
1186 this.onDisconnect(this.everConnected_);
1187 this.onDisconnect = null;
1188 }
1189 }
1190 }
1191 /**
1192 * External-facing close handler.
1193 * Close the websocket and kill the connection.
1194 */
1195 close() {
1196 if (!this.isClosed_) {
1197 this.log_('WebSocket is being closed');
1198 this.shutdown_();
1199 }
1200 }
1201 /**
1202 * Kill the current keepalive timer and start a new one, to ensure that it always fires N seconds after
1203 * the last activity.
1204 */
1205 resetKeepAlive() {
1206 clearInterval(this.keepaliveTimer);
1207 this.keepaliveTimer = setInterval(() => {
1208 //If there has been no websocket activity for a while, send a no-op
1209 if (this.mySock) {
1210 this.sendString_('0');
1211 }
1212 this.resetKeepAlive();
1213 // eslint-disable-next-line @typescript-eslint/no-explicit-any
1214 }, Math.floor(WEBSOCKET_KEEPALIVE_INTERVAL));
1215 }
1216 /**
1217 * Send a string over the websocket.
1218 *
1219 * @param str - String to send.
1220 */
1221 sendString_(str) {
1222 // Firefox seems to sometimes throw exceptions (NS_ERROR_UNEXPECTED) from websocket .send()
1223 // calls for some unknown reason. We treat these as an error and disconnect.
1224 // See https://app.asana.com/0/58926111402292/68021340250410
1225 try {
1226 this.mySock.send(str);
1227 }
1228 catch (e) {
1229 this.log_('Exception thrown from WebSocket.send():', e.message || e.data, 'Closing connection.');
1230 setTimeout(this.onClosed_.bind(this), 0);
1231 }
1232 }
1233}
1234/**
1235 * Number of response before we consider the connection "healthy."
1236 */
1237WebSocketConnection.responsesRequiredToBeHealthy = 2;
1238/**
1239 * Time to wait for the connection te become healthy before giving up.
1240 */
1241WebSocketConnection.healthyTimeout = 30000;
1242
1243const name = "@firebase/database";
1244const version = "0.13.2";
1245
1246/**
1247 * @license
1248 * Copyright 2021 Google LLC
1249 *
1250 * Licensed under the Apache License, Version 2.0 (the "License");
1251 * you may not use this file except in compliance with the License.
1252 * You may obtain a copy of the License at
1253 *
1254 * http://www.apache.org/licenses/LICENSE-2.0
1255 *
1256 * Unless required by applicable law or agreed to in writing, software
1257 * distributed under the License is distributed on an "AS IS" BASIS,
1258 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1259 * See the License for the specific language governing permissions and
1260 * limitations under the License.
1261 */
1262/**
1263 * Abstraction around AppCheck's token fetching capabilities.
1264 */
1265class AppCheckTokenProvider {
1266 constructor(appName_, appCheckProvider) {
1267 this.appName_ = appName_;
1268 this.appCheckProvider = appCheckProvider;
1269 this.appCheck = appCheckProvider === null || appCheckProvider === void 0 ? void 0 : appCheckProvider.getImmediate({ optional: true });
1270 if (!this.appCheck) {
1271 appCheckProvider === null || appCheckProvider === void 0 ? void 0 : appCheckProvider.get().then(appCheck => (this.appCheck = appCheck));
1272 }
1273 }
1274 getToken(forceRefresh) {
1275 if (!this.appCheck) {
1276 return new Promise((resolve, reject) => {
1277 // Support delayed initialization of FirebaseAppCheck. This allows our
1278 // customers to initialize the RTDB SDK before initializing Firebase
1279 // AppCheck and ensures that all requests are authenticated if a token
1280 // becomes available before the timoeout below expires.
1281 setTimeout(() => {
1282 if (this.appCheck) {
1283 this.getToken(forceRefresh).then(resolve, reject);
1284 }
1285 else {
1286 resolve(null);
1287 }
1288 }, 0);
1289 });
1290 }
1291 return this.appCheck.getToken(forceRefresh);
1292 }
1293 addTokenChangeListener(listener) {
1294 var _a;
1295 (_a = this.appCheckProvider) === null || _a === void 0 ? void 0 : _a.get().then(appCheck => appCheck.addTokenListener(listener));
1296 }
1297 notifyForInvalidToken() {
1298 warn(`Provided AppCheck credentials for the app named "${this.appName_}" ` +
1299 'are invalid. This usually indicates your app was not initialized correctly.');
1300 }
1301}
1302
1303/**
1304 * @license
1305 * Copyright 2017 Google LLC
1306 *
1307 * Licensed under the Apache License, Version 2.0 (the "License");
1308 * you may not use this file except in compliance with the License.
1309 * You may obtain a copy of the License at
1310 *
1311 * http://www.apache.org/licenses/LICENSE-2.0
1312 *
1313 * Unless required by applicable law or agreed to in writing, software
1314 * distributed under the License is distributed on an "AS IS" BASIS,
1315 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1316 * See the License for the specific language governing permissions and
1317 * limitations under the License.
1318 */
1319/**
1320 * Abstraction around FirebaseApp's token fetching capabilities.
1321 */
1322class FirebaseAuthTokenProvider {
1323 constructor(appName_, firebaseOptions_, authProvider_) {
1324 this.appName_ = appName_;
1325 this.firebaseOptions_ = firebaseOptions_;
1326 this.authProvider_ = authProvider_;
1327 this.auth_ = null;
1328 this.auth_ = authProvider_.getImmediate({ optional: true });
1329 if (!this.auth_) {
1330 authProvider_.onInit(auth => (this.auth_ = auth));
1331 }
1332 }
1333 getToken(forceRefresh) {
1334 if (!this.auth_) {
1335 return new Promise((resolve, reject) => {
1336 // Support delayed initialization of FirebaseAuth. This allows our
1337 // customers to initialize the RTDB SDK before initializing Firebase
1338 // Auth and ensures that all requests are authenticated if a token
1339 // becomes available before the timoeout below expires.
1340 setTimeout(() => {
1341 if (this.auth_) {
1342 this.getToken(forceRefresh).then(resolve, reject);
1343 }
1344 else {
1345 resolve(null);
1346 }
1347 }, 0);
1348 });
1349 }
1350 return this.auth_.getToken(forceRefresh).catch(error => {
1351 // TODO: Need to figure out all the cases this is raised and whether
1352 // this makes sense.
1353 if (error && error.code === 'auth/token-not-initialized') {
1354 log('Got auth/token-not-initialized error. Treating as null token.');
1355 return null;
1356 }
1357 else {
1358 return Promise.reject(error);
1359 }
1360 });
1361 }
1362 addTokenChangeListener(listener) {
1363 // TODO: We might want to wrap the listener and call it with no args to
1364 // avoid a leaky abstraction, but that makes removing the listener harder.
1365 if (this.auth_) {
1366 this.auth_.addAuthTokenListener(listener);
1367 }
1368 else {
1369 this.authProvider_
1370 .get()
1371 .then(auth => auth.addAuthTokenListener(listener));
1372 }
1373 }
1374 removeTokenChangeListener(listener) {
1375 this.authProvider_
1376 .get()
1377 .then(auth => auth.removeAuthTokenListener(listener));
1378 }
1379 notifyForInvalidToken() {
1380 let errorMessage = 'Provided authentication credentials for the app named "' +
1381 this.appName_ +
1382 '" are invalid. This usually indicates your app was not ' +
1383 'initialized correctly. ';
1384 if ('credential' in this.firebaseOptions_) {
1385 errorMessage +=
1386 'Make sure the "credential" property provided to initializeApp() ' +
1387 'is authorized to access the specified "databaseURL" and is from the correct ' +
1388 'project.';
1389 }
1390 else if ('serviceAccount' in this.firebaseOptions_) {
1391 errorMessage +=
1392 'Make sure the "serviceAccount" property provided to initializeApp() ' +
1393 'is authorized to access the specified "databaseURL" and is from the correct ' +
1394 'project.';
1395 }
1396 else {
1397 errorMessage +=
1398 'Make sure the "apiKey" and "databaseURL" properties provided to ' +
1399 'initializeApp() match the values provided for your app at ' +
1400 'https://console.firebase.google.com/.';
1401 }
1402 warn(errorMessage);
1403 }
1404}
1405/* AuthTokenProvider that supplies a constant token. Used by Admin SDK or mockUserToken with emulators. */
1406class EmulatorTokenProvider {
1407 constructor(accessToken) {
1408 this.accessToken = accessToken;
1409 }
1410 getToken(forceRefresh) {
1411 return Promise.resolve({
1412 accessToken: this.accessToken
1413 });
1414 }
1415 addTokenChangeListener(listener) {
1416 // Invoke the listener immediately to match the behavior in Firebase Auth
1417 // (see packages/auth/src/auth.js#L1807)
1418 listener(this.accessToken);
1419 }
1420 removeTokenChangeListener(listener) { }
1421 notifyForInvalidToken() { }
1422}
1423/** A string that is treated as an admin access token by the RTDB emulator. Used by Admin SDK. */
1424EmulatorTokenProvider.OWNER = 'owner';
1425
1426/**
1427 * @license
1428 * Copyright 2017 Google LLC
1429 *
1430 * Licensed under the Apache License, Version 2.0 (the "License");
1431 * you may not use this file except in compliance with the License.
1432 * You may obtain a copy of the License at
1433 *
1434 * http://www.apache.org/licenses/LICENSE-2.0
1435 *
1436 * Unless required by applicable law or agreed to in writing, software
1437 * distributed under the License is distributed on an "AS IS" BASIS,
1438 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1439 * See the License for the specific language governing permissions and
1440 * limitations under the License.
1441 */
1442/**
1443 * This class ensures the packets from the server arrive in order
1444 * This class takes data from the server and ensures it gets passed into the callbacks in order.
1445 */
1446class PacketReceiver {
1447 /**
1448 * @param onMessage_
1449 */
1450 constructor(onMessage_) {
1451 this.onMessage_ = onMessage_;
1452 this.pendingResponses = [];
1453 this.currentResponseNum = 0;
1454 this.closeAfterResponse = -1;
1455 this.onClose = null;
1456 }
1457 closeAfter(responseNum, callback) {
1458 this.closeAfterResponse = responseNum;
1459 this.onClose = callback;
1460 if (this.closeAfterResponse < this.currentResponseNum) {
1461 this.onClose();
1462 this.onClose = null;
1463 }
1464 }
1465 /**
1466 * Each message from the server comes with a response number, and an array of data. The responseNumber
1467 * allows us to ensure that we process them in the right order, since we can't be guaranteed that all
1468 * browsers will respond in the same order as the requests we sent
1469 */
1470 handleResponse(requestNum, data) {
1471 this.pendingResponses[requestNum] = data;
1472 while (this.pendingResponses[this.currentResponseNum]) {
1473 const toProcess = this.pendingResponses[this.currentResponseNum];
1474 delete this.pendingResponses[this.currentResponseNum];
1475 for (let i = 0; i < toProcess.length; ++i) {
1476 if (toProcess[i]) {
1477 exceptionGuard(() => {
1478 this.onMessage_(toProcess[i]);
1479 });
1480 }
1481 }
1482 if (this.currentResponseNum === this.closeAfterResponse) {
1483 if (this.onClose) {
1484 this.onClose();
1485 this.onClose = null;
1486 }
1487 break;
1488 }
1489 this.currentResponseNum++;
1490 }
1491 }
1492}
1493
1494/**
1495 * @license
1496 * Copyright 2017 Google LLC
1497 *
1498 * Licensed under the Apache License, Version 2.0 (the "License");
1499 * you may not use this file except in compliance with the License.
1500 * You may obtain a copy of the License at
1501 *
1502 * http://www.apache.org/licenses/LICENSE-2.0
1503 *
1504 * Unless required by applicable law or agreed to in writing, software
1505 * distributed under the License is distributed on an "AS IS" BASIS,
1506 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1507 * See the License for the specific language governing permissions and
1508 * limitations under the License.
1509 */
1510// URL query parameters associated with longpolling
1511const FIREBASE_LONGPOLL_START_PARAM = 'start';
1512const FIREBASE_LONGPOLL_CLOSE_COMMAND = 'close';
1513const FIREBASE_LONGPOLL_COMMAND_CB_NAME = 'pLPCommand';
1514const FIREBASE_LONGPOLL_DATA_CB_NAME = 'pRTLPCB';
1515const FIREBASE_LONGPOLL_ID_PARAM = 'id';
1516const FIREBASE_LONGPOLL_PW_PARAM = 'pw';
1517const FIREBASE_LONGPOLL_SERIAL_PARAM = 'ser';
1518const FIREBASE_LONGPOLL_CALLBACK_ID_PARAM = 'cb';
1519const FIREBASE_LONGPOLL_SEGMENT_NUM_PARAM = 'seg';
1520const FIREBASE_LONGPOLL_SEGMENTS_IN_PACKET = 'ts';
1521const FIREBASE_LONGPOLL_DATA_PARAM = 'd';
1522const FIREBASE_LONGPOLL_DISCONN_FRAME_REQUEST_PARAM = 'dframe';
1523//Data size constants.
1524//TODO: Perf: the maximum length actually differs from browser to browser.
1525// We should check what browser we're on and set accordingly.
1526const MAX_URL_DATA_SIZE = 1870;
1527const SEG_HEADER_SIZE = 30; //ie: &seg=8299234&ts=982389123&d=
1528const MAX_PAYLOAD_SIZE = MAX_URL_DATA_SIZE - SEG_HEADER_SIZE;
1529/**
1530 * Keepalive period
1531 * send a fresh request at minimum every 25 seconds. Opera has a maximum request
1532 * length of 30 seconds that we can't exceed.
1533 */
1534const KEEPALIVE_REQUEST_INTERVAL = 25000;
1535/**
1536 * How long to wait before aborting a long-polling connection attempt.
1537 */
1538const LP_CONNECT_TIMEOUT = 30000;
1539/**
1540 * This class manages a single long-polling connection.
1541 */
1542class BrowserPollConnection {
1543 /**
1544 * @param connId An identifier for this connection, used for logging
1545 * @param repoInfo The info for the endpoint to send data to.
1546 * @param applicationId The Firebase App ID for this project.
1547 * @param appCheckToken The AppCheck token for this client.
1548 * @param authToken The AuthToken to use for this connection.
1549 * @param transportSessionId Optional transportSessionid if we are
1550 * reconnecting for an existing transport session
1551 * @param lastSessionId Optional lastSessionId if the PersistentConnection has
1552 * already created a connection previously
1553 */
1554 constructor(connId, repoInfo, applicationId, appCheckToken, authToken, transportSessionId, lastSessionId) {
1555 this.connId = connId;
1556 this.repoInfo = repoInfo;
1557 this.applicationId = applicationId;
1558 this.appCheckToken = appCheckToken;
1559 this.authToken = authToken;
1560 this.transportSessionId = transportSessionId;
1561 this.lastSessionId = lastSessionId;
1562 this.bytesSent = 0;
1563 this.bytesReceived = 0;
1564 this.everConnected_ = false;
1565 this.log_ = logWrapper(connId);
1566 this.stats_ = statsManagerGetCollection(repoInfo);
1567 this.urlFn = (params) => {
1568 // Always add the token if we have one.
1569 if (this.appCheckToken) {
1570 params[APP_CHECK_TOKEN_PARAM] = this.appCheckToken;
1571 }
1572 return repoInfoConnectionURL(repoInfo, LONG_POLLING, params);
1573 };
1574 }
1575 /**
1576 * @param onMessage - Callback when messages arrive
1577 * @param onDisconnect - Callback with connection lost.
1578 */
1579 open(onMessage, onDisconnect) {
1580 this.curSegmentNum = 0;
1581 this.onDisconnect_ = onDisconnect;
1582 this.myPacketOrderer = new PacketReceiver(onMessage);
1583 this.isClosed_ = false;
1584 this.connectTimeoutTimer_ = setTimeout(() => {
1585 this.log_('Timed out trying to connect.');
1586 // Make sure we clear the host cache
1587 this.onClosed_();
1588 this.connectTimeoutTimer_ = null;
1589 // eslint-disable-next-line @typescript-eslint/no-explicit-any
1590 }, Math.floor(LP_CONNECT_TIMEOUT));
1591 // Ensure we delay the creation of the iframe until the DOM is loaded.
1592 executeWhenDOMReady(() => {
1593 if (this.isClosed_) {
1594 return;
1595 }
1596 //Set up a callback that gets triggered once a connection is set up.
1597 this.scriptTagHolder = new FirebaseIFrameScriptHolder((...args) => {
1598 const [command, arg1, arg2, arg3, arg4] = args;
1599 this.incrementIncomingBytes_(args);
1600 if (!this.scriptTagHolder) {
1601 return; // we closed the connection.
1602 }
1603 if (this.connectTimeoutTimer_) {
1604 clearTimeout(this.connectTimeoutTimer_);
1605 this.connectTimeoutTimer_ = null;
1606 }
1607 this.everConnected_ = true;
1608 if (command === FIREBASE_LONGPOLL_START_PARAM) {
1609 this.id = arg1;
1610 this.password = arg2;
1611 }
1612 else if (command === FIREBASE_LONGPOLL_CLOSE_COMMAND) {
1613 // Don't clear the host cache. We got a response from the server, so we know it's reachable
1614 if (arg1) {
1615 // We aren't expecting any more data (other than what the server's already in the process of sending us
1616 // through our already open polls), so don't send any more.
1617 this.scriptTagHolder.sendNewPolls = false;
1618 // arg1 in this case is the last response number sent by the server. We should try to receive
1619 // all of the responses up to this one before closing
1620 this.myPacketOrderer.closeAfter(arg1, () => {
1621 this.onClosed_();
1622 });
1623 }
1624 else {
1625 this.onClosed_();
1626 }
1627 }
1628 else {
1629 throw new Error('Unrecognized command received: ' + command);
1630 }
1631 }, (...args) => {
1632 const [pN, data] = args;
1633 this.incrementIncomingBytes_(args);
1634 this.myPacketOrderer.handleResponse(pN, data);
1635 }, () => {
1636 this.onClosed_();
1637 }, this.urlFn);
1638 //Send the initial request to connect. The serial number is simply to keep the browser from pulling previous results
1639 //from cache.
1640 const urlParams = {};
1641 urlParams[FIREBASE_LONGPOLL_START_PARAM] = 't';
1642 urlParams[FIREBASE_LONGPOLL_SERIAL_PARAM] = Math.floor(Math.random() * 100000000);
1643 if (this.scriptTagHolder.uniqueCallbackIdentifier) {
1644 urlParams[FIREBASE_LONGPOLL_CALLBACK_ID_PARAM] =
1645 this.scriptTagHolder.uniqueCallbackIdentifier;
1646 }
1647 urlParams[VERSION_PARAM] = PROTOCOL_VERSION;
1648 if (this.transportSessionId) {
1649 urlParams[TRANSPORT_SESSION_PARAM] = this.transportSessionId;
1650 }
1651 if (this.lastSessionId) {
1652 urlParams[LAST_SESSION_PARAM] = this.lastSessionId;
1653 }
1654 if (this.applicationId) {
1655 urlParams[APPLICATION_ID_PARAM] = this.applicationId;
1656 }
1657 if (this.appCheckToken) {
1658 urlParams[APP_CHECK_TOKEN_PARAM] = this.appCheckToken;
1659 }
1660 if (typeof location !== 'undefined' &&
1661 location.hostname &&
1662 FORGE_DOMAIN_RE.test(location.hostname)) {
1663 urlParams[REFERER_PARAM] = FORGE_REF;
1664 }
1665 const connectURL = this.urlFn(urlParams);
1666 this.log_('Connecting via long-poll to ' + connectURL);
1667 this.scriptTagHolder.addTag(connectURL, () => {
1668 /* do nothing */
1669 });
1670 });
1671 }
1672 /**
1673 * Call this when a handshake has completed successfully and we want to consider the connection established
1674 */
1675 start() {
1676 this.scriptTagHolder.startLongPoll(this.id, this.password);
1677 this.addDisconnectPingFrame(this.id, this.password);
1678 }
1679 /**
1680 * Forces long polling to be considered as a potential transport
1681 */
1682 static forceAllow() {
1683 BrowserPollConnection.forceAllow_ = true;
1684 }
1685 /**
1686 * Forces longpolling to not be considered as a potential transport
1687 */
1688 static forceDisallow() {
1689 BrowserPollConnection.forceDisallow_ = true;
1690 }
1691 // Static method, use string literal so it can be accessed in a generic way
1692 static isAvailable() {
1693 if (isNodeSdk()) {
1694 return false;
1695 }
1696 else if (BrowserPollConnection.forceAllow_) {
1697 return true;
1698 }
1699 else {
1700 // NOTE: In React-Native there's normally no 'document', but if you debug a React-Native app in
1701 // the Chrome debugger, 'document' is defined, but document.createElement is null (2015/06/08).
1702 return (!BrowserPollConnection.forceDisallow_ &&
1703 typeof document !== 'undefined' &&
1704 document.createElement != null &&
1705 !isChromeExtensionContentScript() &&
1706 !isWindowsStoreApp());
1707 }
1708 }
1709 /**
1710 * No-op for polling
1711 */
1712 markConnectionHealthy() { }
1713 /**
1714 * Stops polling and cleans up the iframe
1715 */
1716 shutdown_() {
1717 this.isClosed_ = true;
1718 if (this.scriptTagHolder) {
1719 this.scriptTagHolder.close();
1720 this.scriptTagHolder = null;
1721 }
1722 //remove the disconnect frame, which will trigger an XHR call to the server to tell it we're leaving.
1723 if (this.myDisconnFrame) {
1724 document.body.removeChild(this.myDisconnFrame);
1725 this.myDisconnFrame = null;
1726 }
1727 if (this.connectTimeoutTimer_) {
1728 clearTimeout(this.connectTimeoutTimer_);
1729 this.connectTimeoutTimer_ = null;
1730 }
1731 }
1732 /**
1733 * Triggered when this transport is closed
1734 */
1735 onClosed_() {
1736 if (!this.isClosed_) {
1737 this.log_('Longpoll is closing itself');
1738 this.shutdown_();
1739 if (this.onDisconnect_) {
1740 this.onDisconnect_(this.everConnected_);
1741 this.onDisconnect_ = null;
1742 }
1743 }
1744 }
1745 /**
1746 * External-facing close handler. RealTime has requested we shut down. Kill our connection and tell the server
1747 * that we've left.
1748 */
1749 close() {
1750 if (!this.isClosed_) {
1751 this.log_('Longpoll is being closed.');
1752 this.shutdown_();
1753 }
1754 }
1755 /**
1756 * Send the JSON object down to the server. It will need to be stringified, base64 encoded, and then
1757 * broken into chunks (since URLs have a small maximum length).
1758 * @param data - The JSON data to transmit.
1759 */
1760 send(data) {
1761 const dataStr = stringify(data);
1762 this.bytesSent += dataStr.length;
1763 this.stats_.incrementCounter('bytes_sent', dataStr.length);
1764 //first, lets get the base64-encoded data
1765 const base64data = base64Encode(dataStr);
1766 //We can only fit a certain amount in each URL, so we need to split this request
1767 //up into multiple pieces if it doesn't fit in one request.
1768 const dataSegs = splitStringBySize(base64data, MAX_PAYLOAD_SIZE);
1769 //Enqueue each segment for transmission. We assign each chunk a sequential ID and a total number
1770 //of segments so that we can reassemble the packet on the server.
1771 for (let i = 0; i < dataSegs.length; i++) {
1772 this.scriptTagHolder.enqueueSegment(this.curSegmentNum, dataSegs.length, dataSegs[i]);
1773 this.curSegmentNum++;
1774 }
1775 }
1776 /**
1777 * This is how we notify the server that we're leaving.
1778 * We aren't able to send requests with DHTML on a window close event, but we can
1779 * trigger XHR requests in some browsers (everything but Opera basically).
1780 */
1781 addDisconnectPingFrame(id, pw) {
1782 if (isNodeSdk()) {
1783 return;
1784 }
1785 this.myDisconnFrame = document.createElement('iframe');
1786 const urlParams = {};
1787 urlParams[FIREBASE_LONGPOLL_DISCONN_FRAME_REQUEST_PARAM] = 't';
1788 urlParams[FIREBASE_LONGPOLL_ID_PARAM] = id;
1789 urlParams[FIREBASE_LONGPOLL_PW_PARAM] = pw;
1790 this.myDisconnFrame.src = this.urlFn(urlParams);
1791 this.myDisconnFrame.style.display = 'none';
1792 document.body.appendChild(this.myDisconnFrame);
1793 }
1794 /**
1795 * Used to track the bytes received by this client
1796 */
1797 incrementIncomingBytes_(args) {
1798 // TODO: This is an annoying perf hit just to track the number of incoming bytes. Maybe it should be opt-in.
1799 const bytesReceived = stringify(args).length;
1800 this.bytesReceived += bytesReceived;
1801 this.stats_.incrementCounter('bytes_received', bytesReceived);
1802 }
1803}
1804/*********************************************************************************************
1805 * A wrapper around an iframe that is used as a long-polling script holder.
1806 *********************************************************************************************/
1807class FirebaseIFrameScriptHolder {
1808 /**
1809 * @param commandCB - The callback to be called when control commands are recevied from the server.
1810 * @param onMessageCB - The callback to be triggered when responses arrive from the server.
1811 * @param onDisconnect - The callback to be triggered when this tag holder is closed
1812 * @param urlFn - A function that provides the URL of the endpoint to send data to.
1813 */
1814 constructor(commandCB, onMessageCB, onDisconnect, urlFn) {
1815 this.onDisconnect = onDisconnect;
1816 this.urlFn = urlFn;
1817 //We maintain a count of all of the outstanding requests, because if we have too many active at once it can cause
1818 //problems in some browsers.
1819 this.outstandingRequests = new Set();
1820 //A queue of the pending segments waiting for transmission to the server.
1821 this.pendingSegs = [];
1822 //A serial number. We use this for two things:
1823 // 1) A way to ensure the browser doesn't cache responses to polls
1824 // 2) A way to make the server aware when long-polls arrive in a different order than we started them. The
1825 // server needs to release both polls in this case or it will cause problems in Opera since Opera can only execute
1826 // JSONP code in the order it was added to the iframe.
1827 this.currentSerial = Math.floor(Math.random() * 100000000);
1828 // This gets set to false when we're "closing down" the connection (e.g. we're switching transports but there's still
1829 // incoming data from the server that we're waiting for).
1830 this.sendNewPolls = true;
1831 if (!isNodeSdk()) {
1832 //Each script holder registers a couple of uniquely named callbacks with the window. These are called from the
1833 //iframes where we put the long-polling script tags. We have two callbacks:
1834 // 1) Command Callback - Triggered for control issues, like starting a connection.
1835 // 2) Message Callback - Triggered when new data arrives.
1836 this.uniqueCallbackIdentifier = LUIDGenerator();
1837 window[FIREBASE_LONGPOLL_COMMAND_CB_NAME + this.uniqueCallbackIdentifier] = commandCB;
1838 window[FIREBASE_LONGPOLL_DATA_CB_NAME + this.uniqueCallbackIdentifier] =
1839 onMessageCB;
1840 //Create an iframe for us to add script tags to.
1841 this.myIFrame = FirebaseIFrameScriptHolder.createIFrame_();
1842 // Set the iframe's contents.
1843 let script = '';
1844 // if we set a javascript url, it's IE and we need to set the document domain. The javascript url is sufficient
1845 // for ie9, but ie8 needs to do it again in the document itself.
1846 if (this.myIFrame.src &&
1847 this.myIFrame.src.substr(0, 'javascript:'.length) === 'javascript:') {
1848 const currentDomain = document.domain;
1849 script = '<script>document.domain="' + currentDomain + '";</script>';
1850 }
1851 const iframeContents = '<html><body>' + script + '</body></html>';
1852 try {
1853 this.myIFrame.doc.open();
1854 this.myIFrame.doc.write(iframeContents);
1855 this.myIFrame.doc.close();
1856 }
1857 catch (e) {
1858 log('frame writing exception');
1859 if (e.stack) {
1860 log(e.stack);
1861 }
1862 log(e);
1863 }
1864 }
1865 else {
1866 this.commandCB = commandCB;
1867 this.onMessageCB = onMessageCB;
1868 }
1869 }
1870 /**
1871 * Each browser has its own funny way to handle iframes. Here we mush them all together into one object that I can
1872 * actually use.
1873 */
1874 static createIFrame_() {
1875 const iframe = document.createElement('iframe');
1876 iframe.style.display = 'none';
1877 // This is necessary in order to initialize the document inside the iframe
1878 if (document.body) {
1879 document.body.appendChild(iframe);
1880 try {
1881 // If document.domain has been modified in IE, this will throw an error, and we need to set the
1882 // domain of the iframe's document manually. We can do this via a javascript: url as the src attribute
1883 // Also note that we must do this *after* the iframe has been appended to the page. Otherwise it doesn't work.
1884 const a = iframe.contentWindow.document;
1885 if (!a) {
1886 // Apologies for the log-spam, I need to do something to keep closure from optimizing out the assignment above.
1887 log('No IE domain setting required');
1888 }
1889 }
1890 catch (e) {
1891 const domain = document.domain;
1892 iframe.src =
1893 "javascript:void((function(){document.open();document.domain='" +
1894 domain +
1895 "';document.close();})())";
1896 }
1897 }
1898 else {
1899 // LongPollConnection attempts to delay initialization until the document is ready, so hopefully this
1900 // never gets hit.
1901 throw 'Document body has not initialized. Wait to initialize Firebase until after the document is ready.';
1902 }
1903 // Get the document of the iframe in a browser-specific way.
1904 if (iframe.contentDocument) {
1905 iframe.doc = iframe.contentDocument; // Firefox, Opera, Safari
1906 }
1907 else if (iframe.contentWindow) {
1908 iframe.doc = iframe.contentWindow.document; // Internet Explorer
1909 // eslint-disable-next-line @typescript-eslint/no-explicit-any
1910 }
1911 else if (iframe.document) {
1912 // eslint-disable-next-line @typescript-eslint/no-explicit-any
1913 iframe.doc = iframe.document; //others?
1914 }
1915 return iframe;
1916 }
1917 /**
1918 * Cancel all outstanding queries and remove the frame.
1919 */
1920 close() {
1921 //Mark this iframe as dead, so no new requests are sent.
1922 this.alive = false;
1923 if (this.myIFrame) {
1924 //We have to actually remove all of the html inside this iframe before removing it from the
1925 //window, or IE will continue loading and executing the script tags we've already added, which
1926 //can lead to some errors being thrown. Setting innerHTML seems to be the easiest way to do this.
1927 this.myIFrame.doc.body.innerHTML = '';
1928 setTimeout(() => {
1929 if (this.myIFrame !== null) {
1930 document.body.removeChild(this.myIFrame);
1931 this.myIFrame = null;
1932 }
1933 }, Math.floor(0));
1934 }
1935 // Protect from being called recursively.
1936 const onDisconnect = this.onDisconnect;
1937 if (onDisconnect) {
1938 this.onDisconnect = null;
1939 onDisconnect();
1940 }
1941 }
1942 /**
1943 * Actually start the long-polling session by adding the first script tag(s) to the iframe.
1944 * @param id - The ID of this connection
1945 * @param pw - The password for this connection
1946 */
1947 startLongPoll(id, pw) {
1948 this.myID = id;
1949 this.myPW = pw;
1950 this.alive = true;
1951 //send the initial request. If there are requests queued, make sure that we transmit as many as we are currently able to.
1952 while (this.newRequest_()) { }
1953 }
1954 /**
1955 * This is called any time someone might want a script tag to be added. It adds a script tag when there aren't
1956 * too many outstanding requests and we are still alive.
1957 *
1958 * If there are outstanding packet segments to send, it sends one. If there aren't, it sends a long-poll anyways if
1959 * needed.
1960 */
1961 newRequest_() {
1962 // We keep one outstanding request open all the time to receive data, but if we need to send data
1963 // (pendingSegs.length > 0) then we create a new request to send the data. The server will automatically
1964 // close the old request.
1965 if (this.alive &&
1966 this.sendNewPolls &&
1967 this.outstandingRequests.size < (this.pendingSegs.length > 0 ? 2 : 1)) {
1968 //construct our url
1969 this.currentSerial++;
1970 const urlParams = {};
1971 urlParams[FIREBASE_LONGPOLL_ID_PARAM] = this.myID;
1972 urlParams[FIREBASE_LONGPOLL_PW_PARAM] = this.myPW;
1973 urlParams[FIREBASE_LONGPOLL_SERIAL_PARAM] = this.currentSerial;
1974 let theURL = this.urlFn(urlParams);
1975 //Now add as much data as we can.
1976 let curDataString = '';
1977 let i = 0;
1978 while (this.pendingSegs.length > 0) {
1979 //first, lets see if the next segment will fit.
1980 const nextSeg = this.pendingSegs[0];
1981 if (nextSeg.d.length +
1982 SEG_HEADER_SIZE +
1983 curDataString.length <=
1984 MAX_URL_DATA_SIZE) {
1985 //great, the segment will fit. Lets append it.
1986 const theSeg = this.pendingSegs.shift();
1987 curDataString =
1988 curDataString +
1989 '&' +
1990 FIREBASE_LONGPOLL_SEGMENT_NUM_PARAM +
1991 i +
1992 '=' +
1993 theSeg.seg +
1994 '&' +
1995 FIREBASE_LONGPOLL_SEGMENTS_IN_PACKET +
1996 i +
1997 '=' +
1998 theSeg.ts +
1999 '&' +
2000 FIREBASE_LONGPOLL_DATA_PARAM +
2001 i +
2002 '=' +
2003 theSeg.d;
2004 i++;
2005 }
2006 else {
2007 break;
2008 }
2009 }
2010 theURL = theURL + curDataString;
2011 this.addLongPollTag_(theURL, this.currentSerial);
2012 return true;
2013 }
2014 else {
2015 return false;
2016 }
2017 }
2018 /**
2019 * Queue a packet for transmission to the server.
2020 * @param segnum - A sequential id for this packet segment used for reassembly
2021 * @param totalsegs - The total number of segments in this packet
2022 * @param data - The data for this segment.
2023 */
2024 enqueueSegment(segnum, totalsegs, data) {
2025 //add this to the queue of segments to send.
2026 this.pendingSegs.push({ seg: segnum, ts: totalsegs, d: data });
2027 //send the data immediately if there isn't already data being transmitted, unless
2028 //startLongPoll hasn't been called yet.
2029 if (this.alive) {
2030 this.newRequest_();
2031 }
2032 }
2033 /**
2034 * Add a script tag for a regular long-poll request.
2035 * @param url - The URL of the script tag.
2036 * @param serial - The serial number of the request.
2037 */
2038 addLongPollTag_(url, serial) {
2039 //remember that we sent this request.
2040 this.outstandingRequests.add(serial);
2041 const doNewRequest = () => {
2042 this.outstandingRequests.delete(serial);
2043 this.newRequest_();
2044 };
2045 // If this request doesn't return on its own accord (by the server sending us some data), we'll
2046 // create a new one after the KEEPALIVE interval to make sure we always keep a fresh request open.
2047 const keepaliveTimeout = setTimeout(doNewRequest, Math.floor(KEEPALIVE_REQUEST_INTERVAL));
2048 const readyStateCB = () => {
2049 // Request completed. Cancel the keepalive.
2050 clearTimeout(keepaliveTimeout);
2051 // Trigger a new request so we can continue receiving data.
2052 doNewRequest();
2053 };
2054 this.addTag(url, readyStateCB);
2055 }
2056 /**
2057 * Add an arbitrary script tag to the iframe.
2058 * @param url - The URL for the script tag source.
2059 * @param loadCB - A callback to be triggered once the script has loaded.
2060 */
2061 addTag(url, loadCB) {
2062 if (isNodeSdk()) {
2063 // eslint-disable-next-line @typescript-eslint/no-explicit-any
2064 this.doNodeLongPoll(url, loadCB);
2065 }
2066 else {
2067 setTimeout(() => {
2068 try {
2069 // if we're already closed, don't add this poll
2070 if (!this.sendNewPolls) {
2071 return;
2072 }
2073 const newScript = this.myIFrame.doc.createElement('script');
2074 newScript.type = 'text/javascript';
2075 newScript.async = true;
2076 newScript.src = url;
2077 // eslint-disable-next-line @typescript-eslint/no-explicit-any
2078 newScript.onload = newScript.onreadystatechange =
2079 function () {
2080 // eslint-disable-next-line @typescript-eslint/no-explicit-any
2081 const rstate = newScript.readyState;
2082 if (!rstate || rstate === 'loaded' || rstate === 'complete') {
2083 // eslint-disable-next-line @typescript-eslint/no-explicit-any
2084 newScript.onload = newScript.onreadystatechange = null;
2085 if (newScript.parentNode) {
2086 newScript.parentNode.removeChild(newScript);
2087 }
2088 loadCB();
2089 }
2090 };
2091 newScript.onerror = () => {
2092 log('Long-poll script failed to load: ' + url);
2093 this.sendNewPolls = false;
2094 this.close();
2095 };
2096 this.myIFrame.doc.body.appendChild(newScript);
2097 }
2098 catch (e) {
2099 // TODO: we should make this error visible somehow
2100 }
2101 }, Math.floor(1));
2102 }
2103 }
2104}
2105
2106/**
2107 * @license
2108 * Copyright 2017 Google LLC
2109 *
2110 * Licensed under the Apache License, Version 2.0 (the "License");
2111 * you may not use this file except in compliance with the License.
2112 * You may obtain a copy of the License at
2113 *
2114 * http://www.apache.org/licenses/LICENSE-2.0
2115 *
2116 * Unless required by applicable law or agreed to in writing, software
2117 * distributed under the License is distributed on an "AS IS" BASIS,
2118 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2119 * See the License for the specific language governing permissions and
2120 * limitations under the License.
2121 */
2122/**
2123 * Currently simplistic, this class manages what transport a Connection should use at various stages of its
2124 * lifecycle.
2125 *
2126 * It starts with longpolling in a browser, and httppolling on node. It then upgrades to websockets if
2127 * they are available.
2128 */
2129class TransportManager {
2130 /**
2131 * @param repoInfo - Metadata around the namespace we're connecting to
2132 */
2133 constructor(repoInfo) {
2134 this.initTransports_(repoInfo);
2135 }
2136 static get ALL_TRANSPORTS() {
2137 return [BrowserPollConnection, WebSocketConnection];
2138 }
2139 /**
2140 * Returns whether transport has been selected to ensure WebSocketConnection or BrowserPollConnection are not called after
2141 * TransportManager has already set up transports_
2142 */
2143 static get IS_TRANSPORT_INITIALIZED() {
2144 return this.globalTransportInitialized_;
2145 }
2146 initTransports_(repoInfo) {
2147 const isWebSocketsAvailable = WebSocketConnection && WebSocketConnection['isAvailable']();
2148 let isSkipPollConnection = isWebSocketsAvailable && !WebSocketConnection.previouslyFailed();
2149 if (repoInfo.webSocketOnly) {
2150 if (!isWebSocketsAvailable) {
2151 warn("wss:// URL used, but browser isn't known to support websockets. Trying anyway.");
2152 }
2153 isSkipPollConnection = true;
2154 }
2155 if (isSkipPollConnection) {
2156 this.transports_ = [WebSocketConnection];
2157 }
2158 else {
2159 const transports = (this.transports_ = []);
2160 for (const transport of TransportManager.ALL_TRANSPORTS) {
2161 if (transport && transport['isAvailable']()) {
2162 transports.push(transport);
2163 }
2164 }
2165 TransportManager.globalTransportInitialized_ = true;
2166 }
2167 }
2168 /**
2169 * @returns The constructor for the initial transport to use
2170 */
2171 initialTransport() {
2172 if (this.transports_.length > 0) {
2173 return this.transports_[0];
2174 }
2175 else {
2176 throw new Error('No transports available');
2177 }
2178 }
2179 /**
2180 * @returns The constructor for the next transport, or null
2181 */
2182 upgradeTransport() {
2183 if (this.transports_.length > 1) {
2184 return this.transports_[1];
2185 }
2186 else {
2187 return null;
2188 }
2189 }
2190}
2191// Keeps track of whether the TransportManager has already chosen a transport to use
2192TransportManager.globalTransportInitialized_ = false;
2193
2194/**
2195 * @license
2196 * Copyright 2017 Google LLC
2197 *
2198 * Licensed under the Apache License, Version 2.0 (the "License");
2199 * you may not use this file except in compliance with the License.
2200 * You may obtain a copy of the License at
2201 *
2202 * http://www.apache.org/licenses/LICENSE-2.0
2203 *
2204 * Unless required by applicable law or agreed to in writing, software
2205 * distributed under the License is distributed on an "AS IS" BASIS,
2206 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2207 * See the License for the specific language governing permissions and
2208 * limitations under the License.
2209 */
2210// Abort upgrade attempt if it takes longer than 60s.
2211const UPGRADE_TIMEOUT = 60000;
2212// For some transports (WebSockets), we need to "validate" the transport by exchanging a few requests and responses.
2213// If we haven't sent enough requests within 5s, we'll start sending noop ping requests.
2214const DELAY_BEFORE_SENDING_EXTRA_REQUESTS = 5000;
2215// If the initial data sent triggers a lot of bandwidth (i.e. it's a large put or a listen for a large amount of data)
2216// then we may not be able to exchange our ping/pong requests within the healthy timeout. So if we reach the timeout
2217// but we've sent/received enough bytes, we don't cancel the connection.
2218const BYTES_SENT_HEALTHY_OVERRIDE = 10 * 1024;
2219const BYTES_RECEIVED_HEALTHY_OVERRIDE = 100 * 1024;
2220const MESSAGE_TYPE = 't';
2221const MESSAGE_DATA = 'd';
2222const CONTROL_SHUTDOWN = 's';
2223const CONTROL_RESET = 'r';
2224const CONTROL_ERROR = 'e';
2225const CONTROL_PONG = 'o';
2226const SWITCH_ACK = 'a';
2227const END_TRANSMISSION = 'n';
2228const PING = 'p';
2229const SERVER_HELLO = 'h';
2230/**
2231 * Creates a new real-time connection to the server using whichever method works
2232 * best in the current browser.
2233 */
2234class Connection {
2235 /**
2236 * @param id - an id for this connection
2237 * @param repoInfo_ - the info for the endpoint to connect to
2238 * @param applicationId_ - the Firebase App ID for this project
2239 * @param appCheckToken_ - The App Check Token for this device.
2240 * @param authToken_ - The auth token for this session.
2241 * @param onMessage_ - the callback to be triggered when a server-push message arrives
2242 * @param onReady_ - the callback to be triggered when this connection is ready to send messages.
2243 * @param onDisconnect_ - the callback to be triggered when a connection was lost
2244 * @param onKill_ - the callback to be triggered when this connection has permanently shut down.
2245 * @param lastSessionId - last session id in persistent connection. is used to clean up old session in real-time server
2246 */
2247 constructor(id, repoInfo_, applicationId_, appCheckToken_, authToken_, onMessage_, onReady_, onDisconnect_, onKill_, lastSessionId) {
2248 this.id = id;
2249 this.repoInfo_ = repoInfo_;
2250 this.applicationId_ = applicationId_;
2251 this.appCheckToken_ = appCheckToken_;
2252 this.authToken_ = authToken_;
2253 this.onMessage_ = onMessage_;
2254 this.onReady_ = onReady_;
2255 this.onDisconnect_ = onDisconnect_;
2256 this.onKill_ = onKill_;
2257 this.lastSessionId = lastSessionId;
2258 this.connectionCount = 0;
2259 this.pendingDataMessages = [];
2260 this.state_ = 0 /* CONNECTING */;
2261 this.log_ = logWrapper('c:' + this.id + ':');
2262 this.transportManager_ = new TransportManager(repoInfo_);
2263 this.log_('Connection created');
2264 this.start_();
2265 }
2266 /**
2267 * Starts a connection attempt
2268 */
2269 start_() {
2270 const conn = this.transportManager_.initialTransport();
2271 this.conn_ = new conn(this.nextTransportId_(), this.repoInfo_, this.applicationId_, this.appCheckToken_, this.authToken_, null, this.lastSessionId);
2272 // For certain transports (WebSockets), we need to send and receive several messages back and forth before we
2273 // can consider the transport healthy.
2274 this.primaryResponsesRequired_ = conn['responsesRequiredToBeHealthy'] || 0;
2275 const onMessageReceived = this.connReceiver_(this.conn_);
2276 const onConnectionLost = this.disconnReceiver_(this.conn_);
2277 this.tx_ = this.conn_;
2278 this.rx_ = this.conn_;
2279 this.secondaryConn_ = null;
2280 this.isHealthy_ = false;
2281 /*
2282 * Firefox doesn't like when code from one iframe tries to create another iframe by way of the parent frame.
2283 * This can occur in the case of a redirect, i.e. we guessed wrong on what server to connect to and received a reset.
2284 * Somehow, setTimeout seems to make this ok. That doesn't make sense from a security perspective, since you should
2285 * still have the context of your originating frame.
2286 */
2287 setTimeout(() => {
2288 // this.conn_ gets set to null in some of the tests. Check to make sure it still exists before using it
2289 this.conn_ && this.conn_.open(onMessageReceived, onConnectionLost);
2290 }, Math.floor(0));
2291 const healthyTimeoutMS = conn['healthyTimeout'] || 0;
2292 if (healthyTimeoutMS > 0) {
2293 this.healthyTimeout_ = setTimeoutNonBlocking(() => {
2294 this.healthyTimeout_ = null;
2295 if (!this.isHealthy_) {
2296 if (this.conn_ &&
2297 this.conn_.bytesReceived > BYTES_RECEIVED_HEALTHY_OVERRIDE) {
2298 this.log_('Connection exceeded healthy timeout but has received ' +
2299 this.conn_.bytesReceived +
2300 ' bytes. Marking connection healthy.');
2301 this.isHealthy_ = true;
2302 this.conn_.markConnectionHealthy();
2303 }
2304 else if (this.conn_ &&
2305 this.conn_.bytesSent > BYTES_SENT_HEALTHY_OVERRIDE) {
2306 this.log_('Connection exceeded healthy timeout but has sent ' +
2307 this.conn_.bytesSent +
2308 ' bytes. Leaving connection alive.');
2309 // NOTE: We don't want to mark it healthy, since we have no guarantee that the bytes have made it to
2310 // the server.
2311 }
2312 else {
2313 this.log_('Closing unhealthy connection after timeout.');
2314 this.close();
2315 }
2316 }
2317 // eslint-disable-next-line @typescript-eslint/no-explicit-any
2318 }, Math.floor(healthyTimeoutMS));
2319 }
2320 }
2321 nextTransportId_() {
2322 return 'c:' + this.id + ':' + this.connectionCount++;
2323 }
2324 disconnReceiver_(conn) {
2325 return everConnected => {
2326 if (conn === this.conn_) {
2327 this.onConnectionLost_(everConnected);
2328 }
2329 else if (conn === this.secondaryConn_) {
2330 this.log_('Secondary connection lost.');
2331 this.onSecondaryConnectionLost_();
2332 }
2333 else {
2334 this.log_('closing an old connection');
2335 }
2336 };
2337 }
2338 connReceiver_(conn) {
2339 return (message) => {
2340 if (this.state_ !== 2 /* DISCONNECTED */) {
2341 if (conn === this.rx_) {
2342 this.onPrimaryMessageReceived_(message);
2343 }
2344 else if (conn === this.secondaryConn_) {
2345 this.onSecondaryMessageReceived_(message);
2346 }
2347 else {
2348 this.log_('message on old connection');
2349 }
2350 }
2351 };
2352 }
2353 /**
2354 * @param dataMsg - An arbitrary data message to be sent to the server
2355 */
2356 sendRequest(dataMsg) {
2357 // wrap in a data message envelope and send it on
2358 const msg = { t: 'd', d: dataMsg };
2359 this.sendData_(msg);
2360 }
2361 tryCleanupConnection() {
2362 if (this.tx_ === this.secondaryConn_ && this.rx_ === this.secondaryConn_) {
2363 this.log_('cleaning up and promoting a connection: ' + this.secondaryConn_.connId);
2364 this.conn_ = this.secondaryConn_;
2365 this.secondaryConn_ = null;
2366 // the server will shutdown the old connection
2367 }
2368 }
2369 onSecondaryControl_(controlData) {
2370 if (MESSAGE_TYPE in controlData) {
2371 const cmd = controlData[MESSAGE_TYPE];
2372 if (cmd === SWITCH_ACK) {
2373 this.upgradeIfSecondaryHealthy_();
2374 }
2375 else if (cmd === CONTROL_RESET) {
2376 // Most likely the session wasn't valid. Abandon the switch attempt
2377 this.log_('Got a reset on secondary, closing it');
2378 this.secondaryConn_.close();
2379 // If we were already using this connection for something, than we need to fully close
2380 if (this.tx_ === this.secondaryConn_ ||
2381 this.rx_ === this.secondaryConn_) {
2382 this.close();
2383 }
2384 }
2385 else if (cmd === CONTROL_PONG) {
2386 this.log_('got pong on secondary.');
2387 this.secondaryResponsesRequired_--;
2388 this.upgradeIfSecondaryHealthy_();
2389 }
2390 }
2391 }
2392 onSecondaryMessageReceived_(parsedData) {
2393 const layer = requireKey('t', parsedData);
2394 const data = requireKey('d', parsedData);
2395 if (layer === 'c') {
2396 this.onSecondaryControl_(data);
2397 }
2398 else if (layer === 'd') {
2399 // got a data message, but we're still second connection. Need to buffer it up
2400 this.pendingDataMessages.push(data);
2401 }
2402 else {
2403 throw new Error('Unknown protocol layer: ' + layer);
2404 }
2405 }
2406 upgradeIfSecondaryHealthy_() {
2407 if (this.secondaryResponsesRequired_ <= 0) {
2408 this.log_('Secondary connection is healthy.');
2409 this.isHealthy_ = true;
2410 this.secondaryConn_.markConnectionHealthy();
2411 this.proceedWithUpgrade_();
2412 }
2413 else {
2414 // Send a ping to make sure the connection is healthy.
2415 this.log_('sending ping on secondary.');
2416 this.secondaryConn_.send({ t: 'c', d: { t: PING, d: {} } });
2417 }
2418 }
2419 proceedWithUpgrade_() {
2420 // tell this connection to consider itself open
2421 this.secondaryConn_.start();
2422 // send ack
2423 this.log_('sending client ack on secondary');
2424 this.secondaryConn_.send({ t: 'c', d: { t: SWITCH_ACK, d: {} } });
2425 // send end packet on primary transport, switch to sending on this one
2426 // can receive on this one, buffer responses until end received on primary transport
2427 this.log_('Ending transmission on primary');
2428 this.conn_.send({ t: 'c', d: { t: END_TRANSMISSION, d: {} } });
2429 this.tx_ = this.secondaryConn_;
2430 this.tryCleanupConnection();
2431 }
2432 onPrimaryMessageReceived_(parsedData) {
2433 // Must refer to parsedData properties in quotes, so closure doesn't touch them.
2434 const layer = requireKey('t', parsedData);
2435 const data = requireKey('d', parsedData);
2436 if (layer === 'c') {
2437 this.onControl_(data);
2438 }
2439 else if (layer === 'd') {
2440 this.onDataMessage_(data);
2441 }
2442 }
2443 onDataMessage_(message) {
2444 this.onPrimaryResponse_();
2445 // We don't do anything with data messages, just kick them up a level
2446 this.onMessage_(message);
2447 }
2448 onPrimaryResponse_() {
2449 if (!this.isHealthy_) {
2450 this.primaryResponsesRequired_--;
2451 if (this.primaryResponsesRequired_ <= 0) {
2452 this.log_('Primary connection is healthy.');
2453 this.isHealthy_ = true;
2454 this.conn_.markConnectionHealthy();
2455 }
2456 }
2457 }
2458 onControl_(controlData) {
2459 const cmd = requireKey(MESSAGE_TYPE, controlData);
2460 if (MESSAGE_DATA in controlData) {
2461 const payload = controlData[MESSAGE_DATA];
2462 if (cmd === SERVER_HELLO) {
2463 this.onHandshake_(payload);
2464 }
2465 else if (cmd === END_TRANSMISSION) {
2466 this.log_('recvd end transmission on primary');
2467 this.rx_ = this.secondaryConn_;
2468 for (let i = 0; i < this.pendingDataMessages.length; ++i) {
2469 this.onDataMessage_(this.pendingDataMessages[i]);
2470 }
2471 this.pendingDataMessages = [];
2472 this.tryCleanupConnection();
2473 }
2474 else if (cmd === CONTROL_SHUTDOWN) {
2475 // This was previously the 'onKill' callback passed to the lower-level connection
2476 // payload in this case is the reason for the shutdown. Generally a human-readable error
2477 this.onConnectionShutdown_(payload);
2478 }
2479 else if (cmd === CONTROL_RESET) {
2480 // payload in this case is the host we should contact
2481 this.onReset_(payload);
2482 }
2483 else if (cmd === CONTROL_ERROR) {
2484 error('Server Error: ' + payload);
2485 }
2486 else if (cmd === CONTROL_PONG) {
2487 this.log_('got pong on primary.');
2488 this.onPrimaryResponse_();
2489 this.sendPingOnPrimaryIfNecessary_();
2490 }
2491 else {
2492 error('Unknown control packet command: ' + cmd);
2493 }
2494 }
2495 }
2496 /**
2497 * @param handshake - The handshake data returned from the server
2498 */
2499 onHandshake_(handshake) {
2500 const timestamp = handshake.ts;
2501 const version = handshake.v;
2502 const host = handshake.h;
2503 this.sessionId = handshake.s;
2504 this.repoInfo_.host = host;
2505 // if we've already closed the connection, then don't bother trying to progress further
2506 if (this.state_ === 0 /* CONNECTING */) {
2507 this.conn_.start();
2508 this.onConnectionEstablished_(this.conn_, timestamp);
2509 if (PROTOCOL_VERSION !== version) {
2510 warn('Protocol version mismatch detected');
2511 }
2512 // TODO: do we want to upgrade? when? maybe a delay?
2513 this.tryStartUpgrade_();
2514 }
2515 }
2516 tryStartUpgrade_() {
2517 const conn = this.transportManager_.upgradeTransport();
2518 if (conn) {
2519 this.startUpgrade_(conn);
2520 }
2521 }
2522 startUpgrade_(conn) {
2523 this.secondaryConn_ = new conn(this.nextTransportId_(), this.repoInfo_, this.applicationId_, this.appCheckToken_, this.authToken_, this.sessionId);
2524 // For certain transports (WebSockets), we need to send and receive several messages back and forth before we
2525 // can consider the transport healthy.
2526 this.secondaryResponsesRequired_ =
2527 conn['responsesRequiredToBeHealthy'] || 0;
2528 const onMessage = this.connReceiver_(this.secondaryConn_);
2529 const onDisconnect = this.disconnReceiver_(this.secondaryConn_);
2530 this.secondaryConn_.open(onMessage, onDisconnect);
2531 // If we haven't successfully upgraded after UPGRADE_TIMEOUT, give up and kill the secondary.
2532 setTimeoutNonBlocking(() => {
2533 if (this.secondaryConn_) {
2534 this.log_('Timed out trying to upgrade.');
2535 this.secondaryConn_.close();
2536 }
2537 }, Math.floor(UPGRADE_TIMEOUT));
2538 }
2539 onReset_(host) {
2540 this.log_('Reset packet received. New host: ' + host);
2541 this.repoInfo_.host = host;
2542 // TODO: if we're already "connected", we need to trigger a disconnect at the next layer up.
2543 // We don't currently support resets after the connection has already been established
2544 if (this.state_ === 1 /* CONNECTED */) {
2545 this.close();
2546 }
2547 else {
2548 // Close whatever connections we have open and start again.
2549 this.closeConnections_();
2550 this.start_();
2551 }
2552 }
2553 onConnectionEstablished_(conn, timestamp) {
2554 this.log_('Realtime connection established.');
2555 this.conn_ = conn;
2556 this.state_ = 1 /* CONNECTED */;
2557 if (this.onReady_) {
2558 this.onReady_(timestamp, this.sessionId);
2559 this.onReady_ = null;
2560 }
2561 // If after 5 seconds we haven't sent enough requests to the server to get the connection healthy,
2562 // send some pings.
2563 if (this.primaryResponsesRequired_ === 0) {
2564 this.log_('Primary connection is healthy.');
2565 this.isHealthy_ = true;
2566 }
2567 else {
2568 setTimeoutNonBlocking(() => {
2569 this.sendPingOnPrimaryIfNecessary_();
2570 }, Math.floor(DELAY_BEFORE_SENDING_EXTRA_REQUESTS));
2571 }
2572 }
2573 sendPingOnPrimaryIfNecessary_() {
2574 // If the connection isn't considered healthy yet, we'll send a noop ping packet request.
2575 if (!this.isHealthy_ && this.state_ === 1 /* CONNECTED */) {
2576 this.log_('sending ping on primary.');
2577 this.sendData_({ t: 'c', d: { t: PING, d: {} } });
2578 }
2579 }
2580 onSecondaryConnectionLost_() {
2581 const conn = this.secondaryConn_;
2582 this.secondaryConn_ = null;
2583 if (this.tx_ === conn || this.rx_ === conn) {
2584 // we are relying on this connection already in some capacity. Therefore, a failure is real
2585 this.close();
2586 }
2587 }
2588 /**
2589 * @param everConnected - Whether or not the connection ever reached a server. Used to determine if
2590 * we should flush the host cache
2591 */
2592 onConnectionLost_(everConnected) {
2593 this.conn_ = null;
2594 // NOTE: IF you're seeing a Firefox error for this line, I think it might be because it's getting
2595 // called on window close and RealtimeState.CONNECTING is no longer defined. Just a guess.
2596 if (!everConnected && this.state_ === 0 /* CONNECTING */) {
2597 this.log_('Realtime connection failed.');
2598 // Since we failed to connect at all, clear any cached entry for this namespace in case the machine went away
2599 if (this.repoInfo_.isCacheableHost()) {
2600 PersistentStorage.remove('host:' + this.repoInfo_.host);
2601 // reset the internal host to what we would show the user, i.e. <ns>.firebaseio.com
2602 this.repoInfo_.internalHost = this.repoInfo_.host;
2603 }
2604 }
2605 else if (this.state_ === 1 /* CONNECTED */) {
2606 this.log_('Realtime connection lost.');
2607 }
2608 this.close();
2609 }
2610 onConnectionShutdown_(reason) {
2611 this.log_('Connection shutdown command received. Shutting down...');
2612 if (this.onKill_) {
2613 this.onKill_(reason);
2614 this.onKill_ = null;
2615 }
2616 // We intentionally don't want to fire onDisconnect (kill is a different case),
2617 // so clear the callback.
2618 this.onDisconnect_ = null;
2619 this.close();
2620 }
2621 sendData_(data) {
2622 if (this.state_ !== 1 /* CONNECTED */) {
2623 throw 'Connection is not connected';
2624 }
2625 else {
2626 this.tx_.send(data);
2627 }
2628 }
2629 /**
2630 * Cleans up this connection, calling the appropriate callbacks
2631 */
2632 close() {
2633 if (this.state_ !== 2 /* DISCONNECTED */) {
2634 this.log_('Closing realtime connection.');
2635 this.state_ = 2 /* DISCONNECTED */;
2636 this.closeConnections_();
2637 if (this.onDisconnect_) {
2638 this.onDisconnect_();
2639 this.onDisconnect_ = null;
2640 }
2641 }
2642 }
2643 closeConnections_() {
2644 this.log_('Shutting down all connections');
2645 if (this.conn_) {
2646 this.conn_.close();
2647 this.conn_ = null;
2648 }
2649 if (this.secondaryConn_) {
2650 this.secondaryConn_.close();
2651 this.secondaryConn_ = null;
2652 }
2653 if (this.healthyTimeout_) {
2654 clearTimeout(this.healthyTimeout_);
2655 this.healthyTimeout_ = null;
2656 }
2657 }
2658}
2659
2660/**
2661 * @license
2662 * Copyright 2017 Google LLC
2663 *
2664 * Licensed under the Apache License, Version 2.0 (the "License");
2665 * you may not use this file except in compliance with the License.
2666 * You may obtain a copy of the License at
2667 *
2668 * http://www.apache.org/licenses/LICENSE-2.0
2669 *
2670 * Unless required by applicable law or agreed to in writing, software
2671 * distributed under the License is distributed on an "AS IS" BASIS,
2672 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2673 * See the License for the specific language governing permissions and
2674 * limitations under the License.
2675 */
2676/**
2677 * Interface defining the set of actions that can be performed against the Firebase server
2678 * (basically corresponds to our wire protocol).
2679 *
2680 * @interface
2681 */
2682class ServerActions {
2683 put(pathString, data, onComplete, hash) { }
2684 merge(pathString, data, onComplete, hash) { }
2685 /**
2686 * Refreshes the auth token for the current connection.
2687 * @param token - The authentication token
2688 */
2689 refreshAuthToken(token) { }
2690 /**
2691 * Refreshes the app check token for the current connection.
2692 * @param token The app check token
2693 */
2694 refreshAppCheckToken(token) { }
2695 onDisconnectPut(pathString, data, onComplete) { }
2696 onDisconnectMerge(pathString, data, onComplete) { }
2697 onDisconnectCancel(pathString, onComplete) { }
2698 reportStats(stats) { }
2699}
2700
2701/**
2702 * @license
2703 * Copyright 2017 Google LLC
2704 *
2705 * Licensed under the Apache License, Version 2.0 (the "License");
2706 * you may not use this file except in compliance with the License.
2707 * You may obtain a copy of the License at
2708 *
2709 * http://www.apache.org/licenses/LICENSE-2.0
2710 *
2711 * Unless required by applicable law or agreed to in writing, software
2712 * distributed under the License is distributed on an "AS IS" BASIS,
2713 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2714 * See the License for the specific language governing permissions and
2715 * limitations under the License.
2716 */
2717/**
2718 * Base class to be used if you want to emit events. Call the constructor with
2719 * the set of allowed event names.
2720 */
2721class EventEmitter {
2722 constructor(allowedEvents_) {
2723 this.allowedEvents_ = allowedEvents_;
2724 this.listeners_ = {};
2725 assert(Array.isArray(allowedEvents_) && allowedEvents_.length > 0, 'Requires a non-empty array');
2726 }
2727 /**
2728 * To be called by derived classes to trigger events.
2729 */
2730 trigger(eventType, ...varArgs) {
2731 if (Array.isArray(this.listeners_[eventType])) {
2732 // Clone the list, since callbacks could add/remove listeners.
2733 const listeners = [...this.listeners_[eventType]];
2734 for (let i = 0; i < listeners.length; i++) {
2735 listeners[i].callback.apply(listeners[i].context, varArgs);
2736 }
2737 }
2738 }
2739 on(eventType, callback, context) {
2740 this.validateEventType_(eventType);
2741 this.listeners_[eventType] = this.listeners_[eventType] || [];
2742 this.listeners_[eventType].push({ callback, context });
2743 const eventData = this.getInitialEvent(eventType);
2744 if (eventData) {
2745 callback.apply(context, eventData);
2746 }
2747 }
2748 off(eventType, callback, context) {
2749 this.validateEventType_(eventType);
2750 const listeners = this.listeners_[eventType] || [];
2751 for (let i = 0; i < listeners.length; i++) {
2752 if (listeners[i].callback === callback &&
2753 (!context || context === listeners[i].context)) {
2754 listeners.splice(i, 1);
2755 return;
2756 }
2757 }
2758 }
2759 validateEventType_(eventType) {
2760 assert(this.allowedEvents_.find(et => {
2761 return et === eventType;
2762 }), 'Unknown event: ' + eventType);
2763 }
2764}
2765
2766/**
2767 * @license
2768 * Copyright 2017 Google LLC
2769 *
2770 * Licensed under the Apache License, Version 2.0 (the "License");
2771 * you may not use this file except in compliance with the License.
2772 * You may obtain a copy of the License at
2773 *
2774 * http://www.apache.org/licenses/LICENSE-2.0
2775 *
2776 * Unless required by applicable law or agreed to in writing, software
2777 * distributed under the License is distributed on an "AS IS" BASIS,
2778 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2779 * See the License for the specific language governing permissions and
2780 * limitations under the License.
2781 */
2782/**
2783 * Monitors online state (as reported by window.online/offline events).
2784 *
2785 * The expectation is that this could have many false positives (thinks we are online
2786 * when we're not), but no false negatives. So we can safely use it to determine when
2787 * we definitely cannot reach the internet.
2788 */
2789class OnlineMonitor extends EventEmitter {
2790 constructor() {
2791 super(['online']);
2792 this.online_ = true;
2793 // We've had repeated complaints that Cordova apps can get stuck "offline", e.g.
2794 // https://forum.ionicframework.com/t/firebase-connection-is-lost-and-never-come-back/43810
2795 // It would seem that the 'online' event does not always fire consistently. So we disable it
2796 // for Cordova.
2797 if (typeof window !== 'undefined' &&
2798 typeof window.addEventListener !== 'undefined' &&
2799 !isMobileCordova()) {
2800 window.addEventListener('online', () => {
2801 if (!this.online_) {
2802 this.online_ = true;
2803 this.trigger('online', true);
2804 }
2805 }, false);
2806 window.addEventListener('offline', () => {
2807 if (this.online_) {
2808 this.online_ = false;
2809 this.trigger('online', false);
2810 }
2811 }, false);
2812 }
2813 }
2814 static getInstance() {
2815 return new OnlineMonitor();
2816 }
2817 getInitialEvent(eventType) {
2818 assert(eventType === 'online', 'Unknown event type: ' + eventType);
2819 return [this.online_];
2820 }
2821 currentlyOnline() {
2822 return this.online_;
2823 }
2824}
2825
2826/**
2827 * @license
2828 * Copyright 2017 Google LLC
2829 *
2830 * Licensed under the Apache License, Version 2.0 (the "License");
2831 * you may not use this file except in compliance with the License.
2832 * You may obtain a copy of the License at
2833 *
2834 * http://www.apache.org/licenses/LICENSE-2.0
2835 *
2836 * Unless required by applicable law or agreed to in writing, software
2837 * distributed under the License is distributed on an "AS IS" BASIS,
2838 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2839 * See the License for the specific language governing permissions and
2840 * limitations under the License.
2841 */
2842/** Maximum key depth. */
2843const MAX_PATH_DEPTH = 32;
2844/** Maximum number of (UTF8) bytes in a Firebase path. */
2845const MAX_PATH_LENGTH_BYTES = 768;
2846/**
2847 * An immutable object representing a parsed path. It's immutable so that you
2848 * can pass them around to other functions without worrying about them changing
2849 * it.
2850 */
2851class Path {
2852 /**
2853 * @param pathOrString - Path string to parse, or another path, or the raw
2854 * tokens array
2855 */
2856 constructor(pathOrString, pieceNum) {
2857 if (pieceNum === void 0) {
2858 this.pieces_ = pathOrString.split('/');
2859 // Remove empty pieces.
2860 let copyTo = 0;
2861 for (let i = 0; i < this.pieces_.length; i++) {
2862 if (this.pieces_[i].length > 0) {
2863 this.pieces_[copyTo] = this.pieces_[i];
2864 copyTo++;
2865 }
2866 }
2867 this.pieces_.length = copyTo;
2868 this.pieceNum_ = 0;
2869 }
2870 else {
2871 this.pieces_ = pathOrString;
2872 this.pieceNum_ = pieceNum;
2873 }
2874 }
2875 toString() {
2876 let pathString = '';
2877 for (let i = this.pieceNum_; i < this.pieces_.length; i++) {
2878 if (this.pieces_[i] !== '') {
2879 pathString += '/' + this.pieces_[i];
2880 }
2881 }
2882 return pathString || '/';
2883 }
2884}
2885function newEmptyPath() {
2886 return new Path('');
2887}
2888function pathGetFront(path) {
2889 if (path.pieceNum_ >= path.pieces_.length) {
2890 return null;
2891 }
2892 return path.pieces_[path.pieceNum_];
2893}
2894/**
2895 * @returns The number of segments in this path
2896 */
2897function pathGetLength(path) {
2898 return path.pieces_.length - path.pieceNum_;
2899}
2900function pathPopFront(path) {
2901 let pieceNum = path.pieceNum_;
2902 if (pieceNum < path.pieces_.length) {
2903 pieceNum++;
2904 }
2905 return new Path(path.pieces_, pieceNum);
2906}
2907function pathGetBack(path) {
2908 if (path.pieceNum_ < path.pieces_.length) {
2909 return path.pieces_[path.pieces_.length - 1];
2910 }
2911 return null;
2912}
2913function pathToUrlEncodedString(path) {
2914 let pathString = '';
2915 for (let i = path.pieceNum_; i < path.pieces_.length; i++) {
2916 if (path.pieces_[i] !== '') {
2917 pathString += '/' + encodeURIComponent(String(path.pieces_[i]));
2918 }
2919 }
2920 return pathString || '/';
2921}
2922/**
2923 * Shallow copy of the parts of the path.
2924 *
2925 */
2926function pathSlice(path, begin = 0) {
2927 return path.pieces_.slice(path.pieceNum_ + begin);
2928}
2929function pathParent(path) {
2930 if (path.pieceNum_ >= path.pieces_.length) {
2931 return null;
2932 }
2933 const pieces = [];
2934 for (let i = path.pieceNum_; i < path.pieces_.length - 1; i++) {
2935 pieces.push(path.pieces_[i]);
2936 }
2937 return new Path(pieces, 0);
2938}
2939function pathChild(path, childPathObj) {
2940 const pieces = [];
2941 for (let i = path.pieceNum_; i < path.pieces_.length; i++) {
2942 pieces.push(path.pieces_[i]);
2943 }
2944 if (childPathObj instanceof Path) {
2945 for (let i = childPathObj.pieceNum_; i < childPathObj.pieces_.length; i++) {
2946 pieces.push(childPathObj.pieces_[i]);
2947 }
2948 }
2949 else {
2950 const childPieces = childPathObj.split('/');
2951 for (let i = 0; i < childPieces.length; i++) {
2952 if (childPieces[i].length > 0) {
2953 pieces.push(childPieces[i]);
2954 }
2955 }
2956 }
2957 return new Path(pieces, 0);
2958}
2959/**
2960 * @returns True if there are no segments in this path
2961 */
2962function pathIsEmpty(path) {
2963 return path.pieceNum_ >= path.pieces_.length;
2964}
2965/**
2966 * @returns The path from outerPath to innerPath
2967 */
2968function newRelativePath(outerPath, innerPath) {
2969 const outer = pathGetFront(outerPath), inner = pathGetFront(innerPath);
2970 if (outer === null) {
2971 return innerPath;
2972 }
2973 else if (outer === inner) {
2974 return newRelativePath(pathPopFront(outerPath), pathPopFront(innerPath));
2975 }
2976 else {
2977 throw new Error('INTERNAL ERROR: innerPath (' +
2978 innerPath +
2979 ') is not within ' +
2980 'outerPath (' +
2981 outerPath +
2982 ')');
2983 }
2984}
2985/**
2986 * @returns -1, 0, 1 if left is less, equal, or greater than the right.
2987 */
2988function pathCompare(left, right) {
2989 const leftKeys = pathSlice(left, 0);
2990 const rightKeys = pathSlice(right, 0);
2991 for (let i = 0; i < leftKeys.length && i < rightKeys.length; i++) {
2992 const cmp = nameCompare(leftKeys[i], rightKeys[i]);
2993 if (cmp !== 0) {
2994 return cmp;
2995 }
2996 }
2997 if (leftKeys.length === rightKeys.length) {
2998 return 0;
2999 }
3000 return leftKeys.length < rightKeys.length ? -1 : 1;
3001}
3002/**
3003 * @returns true if paths are the same.
3004 */
3005function pathEquals(path, other) {
3006 if (pathGetLength(path) !== pathGetLength(other)) {
3007 return false;
3008 }
3009 for (let i = path.pieceNum_, j = other.pieceNum_; i <= path.pieces_.length; i++, j++) {
3010 if (path.pieces_[i] !== other.pieces_[j]) {
3011 return false;
3012 }
3013 }
3014 return true;
3015}
3016/**
3017 * @returns True if this path is a parent of (or the same as) other
3018 */
3019function pathContains(path, other) {
3020 let i = path.pieceNum_;
3021 let j = other.pieceNum_;
3022 if (pathGetLength(path) > pathGetLength(other)) {
3023 return false;
3024 }
3025 while (i < path.pieces_.length) {
3026 if (path.pieces_[i] !== other.pieces_[j]) {
3027 return false;
3028 }
3029 ++i;
3030 ++j;
3031 }
3032 return true;
3033}
3034/**
3035 * Dynamic (mutable) path used to count path lengths.
3036 *
3037 * This class is used to efficiently check paths for valid
3038 * length (in UTF8 bytes) and depth (used in path validation).
3039 *
3040 * Throws Error exception if path is ever invalid.
3041 *
3042 * The definition of a path always begins with '/'.
3043 */
3044class ValidationPath {
3045 /**
3046 * @param path - Initial Path.
3047 * @param errorPrefix_ - Prefix for any error messages.
3048 */
3049 constructor(path, errorPrefix_) {
3050 this.errorPrefix_ = errorPrefix_;
3051 this.parts_ = pathSlice(path, 0);
3052 /** Initialize to number of '/' chars needed in path. */
3053 this.byteLength_ = Math.max(1, this.parts_.length);
3054 for (let i = 0; i < this.parts_.length; i++) {
3055 this.byteLength_ += stringLength(this.parts_[i]);
3056 }
3057 validationPathCheckValid(this);
3058 }
3059}
3060function validationPathPush(validationPath, child) {
3061 // Count the needed '/'
3062 if (validationPath.parts_.length > 0) {
3063 validationPath.byteLength_ += 1;
3064 }
3065 validationPath.parts_.push(child);
3066 validationPath.byteLength_ += stringLength(child);
3067 validationPathCheckValid(validationPath);
3068}
3069function validationPathPop(validationPath) {
3070 const last = validationPath.parts_.pop();
3071 validationPath.byteLength_ -= stringLength(last);
3072 // Un-count the previous '/'
3073 if (validationPath.parts_.length > 0) {
3074 validationPath.byteLength_ -= 1;
3075 }
3076}
3077function validationPathCheckValid(validationPath) {
3078 if (validationPath.byteLength_ > MAX_PATH_LENGTH_BYTES) {
3079 throw new Error(validationPath.errorPrefix_ +
3080 'has a key path longer than ' +
3081 MAX_PATH_LENGTH_BYTES +
3082 ' bytes (' +
3083 validationPath.byteLength_ +
3084 ').');
3085 }
3086 if (validationPath.parts_.length > MAX_PATH_DEPTH) {
3087 throw new Error(validationPath.errorPrefix_ +
3088 'path specified exceeds the maximum depth that can be written (' +
3089 MAX_PATH_DEPTH +
3090 ') or object contains a cycle ' +
3091 validationPathToErrorString(validationPath));
3092 }
3093}
3094/**
3095 * String for use in error messages - uses '.' notation for path.
3096 */
3097function validationPathToErrorString(validationPath) {
3098 if (validationPath.parts_.length === 0) {
3099 return '';
3100 }
3101 return "in property '" + validationPath.parts_.join('.') + "'";
3102}
3103
3104/**
3105 * @license
3106 * Copyright 2017 Google LLC
3107 *
3108 * Licensed under the Apache License, Version 2.0 (the "License");
3109 * you may not use this file except in compliance with the License.
3110 * You may obtain a copy of the License at
3111 *
3112 * http://www.apache.org/licenses/LICENSE-2.0
3113 *
3114 * Unless required by applicable law or agreed to in writing, software
3115 * distributed under the License is distributed on an "AS IS" BASIS,
3116 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3117 * See the License for the specific language governing permissions and
3118 * limitations under the License.
3119 */
3120class VisibilityMonitor extends EventEmitter {
3121 constructor() {
3122 super(['visible']);
3123 let hidden;
3124 let visibilityChange;
3125 if (typeof document !== 'undefined' &&
3126 typeof document.addEventListener !== 'undefined') {
3127 if (typeof document['hidden'] !== 'undefined') {
3128 // Opera 12.10 and Firefox 18 and later support
3129 visibilityChange = 'visibilitychange';
3130 hidden = 'hidden';
3131 }
3132 else if (typeof document['mozHidden'] !== 'undefined') {
3133 visibilityChange = 'mozvisibilitychange';
3134 hidden = 'mozHidden';
3135 }
3136 else if (typeof document['msHidden'] !== 'undefined') {
3137 visibilityChange = 'msvisibilitychange';
3138 hidden = 'msHidden';
3139 }
3140 else if (typeof document['webkitHidden'] !== 'undefined') {
3141 visibilityChange = 'webkitvisibilitychange';
3142 hidden = 'webkitHidden';
3143 }
3144 }
3145 // Initially, we always assume we are visible. This ensures that in browsers
3146 // without page visibility support or in cases where we are never visible
3147 // (e.g. chrome extension), we act as if we are visible, i.e. don't delay
3148 // reconnects
3149 this.visible_ = true;
3150 if (visibilityChange) {
3151 document.addEventListener(visibilityChange, () => {
3152 const visible = !document[hidden];
3153 if (visible !== this.visible_) {
3154 this.visible_ = visible;
3155 this.trigger('visible', visible);
3156 }
3157 }, false);
3158 }
3159 }
3160 static getInstance() {
3161 return new VisibilityMonitor();
3162 }
3163 getInitialEvent(eventType) {
3164 assert(eventType === 'visible', 'Unknown event type: ' + eventType);
3165 return [this.visible_];
3166 }
3167}
3168
3169/**
3170 * @license
3171 * Copyright 2017 Google LLC
3172 *
3173 * Licensed under the Apache License, Version 2.0 (the "License");
3174 * you may not use this file except in compliance with the License.
3175 * You may obtain a copy of the License at
3176 *
3177 * http://www.apache.org/licenses/LICENSE-2.0
3178 *
3179 * Unless required by applicable law or agreed to in writing, software
3180 * distributed under the License is distributed on an "AS IS" BASIS,
3181 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3182 * See the License for the specific language governing permissions and
3183 * limitations under the License.
3184 */
3185const RECONNECT_MIN_DELAY = 1000;
3186const RECONNECT_MAX_DELAY_DEFAULT = 60 * 5 * 1000; // 5 minutes in milliseconds (Case: 1858)
3187const GET_CONNECT_TIMEOUT = 3 * 1000;
3188const RECONNECT_MAX_DELAY_FOR_ADMINS = 30 * 1000; // 30 seconds for admin clients (likely to be a backend server)
3189const RECONNECT_DELAY_MULTIPLIER = 1.3;
3190const RECONNECT_DELAY_RESET_TIMEOUT = 30000; // Reset delay back to MIN_DELAY after being connected for 30sec.
3191const SERVER_KILL_INTERRUPT_REASON = 'server_kill';
3192// If auth fails repeatedly, we'll assume something is wrong and log a warning / back off.
3193const INVALID_TOKEN_THRESHOLD = 3;
3194/**
3195 * Firebase connection. Abstracts wire protocol and handles reconnecting.
3196 *
3197 * NOTE: All JSON objects sent to the realtime connection must have property names enclosed
3198 * in quotes to make sure the closure compiler does not minify them.
3199 */
3200class PersistentConnection extends ServerActions {
3201 /**
3202 * @param repoInfo_ - Data about the namespace we are connecting to
3203 * @param applicationId_ - The Firebase App ID for this project
3204 * @param onDataUpdate_ - A callback for new data from the server
3205 */
3206 constructor(repoInfo_, applicationId_, onDataUpdate_, onConnectStatus_, onServerInfoUpdate_, authTokenProvider_, appCheckTokenProvider_, authOverride_) {
3207 super();
3208 this.repoInfo_ = repoInfo_;
3209 this.applicationId_ = applicationId_;
3210 this.onDataUpdate_ = onDataUpdate_;
3211 this.onConnectStatus_ = onConnectStatus_;
3212 this.onServerInfoUpdate_ = onServerInfoUpdate_;
3213 this.authTokenProvider_ = authTokenProvider_;
3214 this.appCheckTokenProvider_ = appCheckTokenProvider_;
3215 this.authOverride_ = authOverride_;
3216 // Used for diagnostic logging.
3217 this.id = PersistentConnection.nextPersistentConnectionId_++;
3218 this.log_ = logWrapper('p:' + this.id + ':');
3219 this.interruptReasons_ = {};
3220 this.listens = new Map();
3221 this.outstandingPuts_ = [];
3222 this.outstandingGets_ = [];
3223 this.outstandingPutCount_ = 0;
3224 this.outstandingGetCount_ = 0;
3225 this.onDisconnectRequestQueue_ = [];
3226 this.connected_ = false;
3227 this.reconnectDelay_ = RECONNECT_MIN_DELAY;
3228 this.maxReconnectDelay_ = RECONNECT_MAX_DELAY_DEFAULT;
3229 this.securityDebugCallback_ = null;
3230 this.lastSessionId = null;
3231 this.establishConnectionTimer_ = null;
3232 this.visible_ = false;
3233 // Before we get connected, we keep a queue of pending messages to send.
3234 this.requestCBHash_ = {};
3235 this.requestNumber_ = 0;
3236 this.realtime_ = null;
3237 this.authToken_ = null;
3238 this.appCheckToken_ = null;
3239 this.forceTokenRefresh_ = false;
3240 this.invalidAuthTokenCount_ = 0;
3241 this.invalidAppCheckTokenCount_ = 0;
3242 this.firstConnection_ = true;
3243 this.lastConnectionAttemptTime_ = null;
3244 this.lastConnectionEstablishedTime_ = null;
3245 if (authOverride_ && !isNodeSdk()) {
3246 throw new Error('Auth override specified in options, but not supported on non Node.js platforms');
3247 }
3248 VisibilityMonitor.getInstance().on('visible', this.onVisible_, this);
3249 if (repoInfo_.host.indexOf('fblocal') === -1) {
3250 OnlineMonitor.getInstance().on('online', this.onOnline_, this);
3251 }
3252 }
3253 sendRequest(action, body, onResponse) {
3254 const curReqNum = ++this.requestNumber_;
3255 const msg = { r: curReqNum, a: action, b: body };
3256 this.log_(stringify(msg));
3257 assert(this.connected_, "sendRequest call when we're not connected not allowed.");
3258 this.realtime_.sendRequest(msg);
3259 if (onResponse) {
3260 this.requestCBHash_[curReqNum] = onResponse;
3261 }
3262 }
3263 get(query) {
3264 this.initConnection_();
3265 const deferred = new Deferred();
3266 const request = {
3267 p: query._path.toString(),
3268 q: query._queryObject
3269 };
3270 const outstandingGet = {
3271 action: 'g',
3272 request,
3273 onComplete: (message) => {
3274 const payload = message['d'];
3275 if (message['s'] === 'ok') {
3276 deferred.resolve(payload);
3277 }
3278 else {
3279 deferred.reject(payload);
3280 }
3281 }
3282 };
3283 this.outstandingGets_.push(outstandingGet);
3284 this.outstandingGetCount_++;
3285 const index = this.outstandingGets_.length - 1;
3286 if (!this.connected_) {
3287 setTimeout(() => {
3288 const get = this.outstandingGets_[index];
3289 if (get === undefined || outstandingGet !== get) {
3290 return;
3291 }
3292 delete this.outstandingGets_[index];
3293 this.outstandingGetCount_--;
3294 if (this.outstandingGetCount_ === 0) {
3295 this.outstandingGets_ = [];
3296 }
3297 this.log_('get ' + index + ' timed out on connection');
3298 deferred.reject(new Error('Client is offline.'));
3299 }, GET_CONNECT_TIMEOUT);
3300 }
3301 if (this.connected_) {
3302 this.sendGet_(index);
3303 }
3304 return deferred.promise;
3305 }
3306 listen(query, currentHashFn, tag, onComplete) {
3307 this.initConnection_();
3308 const queryId = query._queryIdentifier;
3309 const pathString = query._path.toString();
3310 this.log_('Listen called for ' + pathString + ' ' + queryId);
3311 if (!this.listens.has(pathString)) {
3312 this.listens.set(pathString, new Map());
3313 }
3314 assert(query._queryParams.isDefault() || !query._queryParams.loadsAllData(), 'listen() called for non-default but complete query');
3315 assert(!this.listens.get(pathString).has(queryId), `listen() called twice for same path/queryId.`);
3316 const listenSpec = {
3317 onComplete,
3318 hashFn: currentHashFn,
3319 query,
3320 tag
3321 };
3322 this.listens.get(pathString).set(queryId, listenSpec);
3323 if (this.connected_) {
3324 this.sendListen_(listenSpec);
3325 }
3326 }
3327 sendGet_(index) {
3328 const get = this.outstandingGets_[index];
3329 this.sendRequest('g', get.request, (message) => {
3330 delete this.outstandingGets_[index];
3331 this.outstandingGetCount_--;
3332 if (this.outstandingGetCount_ === 0) {
3333 this.outstandingGets_ = [];
3334 }
3335 if (get.onComplete) {
3336 get.onComplete(message);
3337 }
3338 });
3339 }
3340 sendListen_(listenSpec) {
3341 const query = listenSpec.query;
3342 const pathString = query._path.toString();
3343 const queryId = query._queryIdentifier;
3344 this.log_('Listen on ' + pathString + ' for ' + queryId);
3345 const req = { /*path*/ p: pathString };
3346 const action = 'q';
3347 // Only bother to send query if it's non-default.
3348 if (listenSpec.tag) {
3349 req['q'] = query._queryObject;
3350 req['t'] = listenSpec.tag;
3351 }
3352 req[ /*hash*/'h'] = listenSpec.hashFn();
3353 this.sendRequest(action, req, (message) => {
3354 const payload = message[ /*data*/'d'];
3355 const status = message[ /*status*/'s'];
3356 // print warnings in any case...
3357 PersistentConnection.warnOnListenWarnings_(payload, query);
3358 const currentListenSpec = this.listens.get(pathString) &&
3359 this.listens.get(pathString).get(queryId);
3360 // only trigger actions if the listen hasn't been removed and readded
3361 if (currentListenSpec === listenSpec) {
3362 this.log_('listen response', message);
3363 if (status !== 'ok') {
3364 this.removeListen_(pathString, queryId);
3365 }
3366 if (listenSpec.onComplete) {
3367 listenSpec.onComplete(status, payload);
3368 }
3369 }
3370 });
3371 }
3372 static warnOnListenWarnings_(payload, query) {
3373 if (payload && typeof payload === 'object' && contains(payload, 'w')) {
3374 // eslint-disable-next-line @typescript-eslint/no-explicit-any
3375 const warnings = safeGet(payload, 'w');
3376 if (Array.isArray(warnings) && ~warnings.indexOf('no_index')) {
3377 const indexSpec = '".indexOn": "' + query._queryParams.getIndex().toString() + '"';
3378 const indexPath = query._path.toString();
3379 warn(`Using an unspecified index. Your data will be downloaded and ` +
3380 `filtered on the client. Consider adding ${indexSpec} at ` +
3381 `${indexPath} to your security rules for better performance.`);
3382 }
3383 }
3384 }
3385 refreshAuthToken(token) {
3386 this.authToken_ = token;
3387 this.log_('Auth token refreshed');
3388 if (this.authToken_) {
3389 this.tryAuth();
3390 }
3391 else {
3392 //If we're connected we want to let the server know to unauthenticate us. If we're not connected, simply delete
3393 //the credential so we dont become authenticated next time we connect.
3394 if (this.connected_) {
3395 this.sendRequest('unauth', {}, () => { });
3396 }
3397 }
3398 this.reduceReconnectDelayIfAdminCredential_(token);
3399 }
3400 reduceReconnectDelayIfAdminCredential_(credential) {
3401 // NOTE: This isn't intended to be bulletproof (a malicious developer can always just modify the client).
3402 // Additionally, we don't bother resetting the max delay back to the default if auth fails / expires.
3403 const isFirebaseSecret = credential && credential.length === 40;
3404 if (isFirebaseSecret || isAdmin(credential)) {
3405 this.log_('Admin auth credential detected. Reducing max reconnect time.');
3406 this.maxReconnectDelay_ = RECONNECT_MAX_DELAY_FOR_ADMINS;
3407 }
3408 }
3409 refreshAppCheckToken(token) {
3410 this.appCheckToken_ = token;
3411 this.log_('App check token refreshed');
3412 if (this.appCheckToken_) {
3413 this.tryAppCheck();
3414 }
3415 else {
3416 //If we're connected we want to let the server know to unauthenticate us.
3417 //If we're not connected, simply delete the credential so we dont become
3418 // authenticated next time we connect.
3419 if (this.connected_) {
3420 this.sendRequest('unappeck', {}, () => { });
3421 }
3422 }
3423 }
3424 /**
3425 * Attempts to authenticate with the given credentials. If the authentication attempt fails, it's triggered like
3426 * a auth revoked (the connection is closed).
3427 */
3428 tryAuth() {
3429 if (this.connected_ && this.authToken_) {
3430 const token = this.authToken_;
3431 const authMethod = isValidFormat(token) ? 'auth' : 'gauth';
3432 const requestData = { cred: token };
3433 if (this.authOverride_ === null) {
3434 requestData['noauth'] = true;
3435 }
3436 else if (typeof this.authOverride_ === 'object') {
3437 requestData['authvar'] = this.authOverride_;
3438 }
3439 this.sendRequest(authMethod, requestData, (res) => {
3440 const status = res[ /*status*/'s'];
3441 const data = res[ /*data*/'d'] || 'error';
3442 if (this.authToken_ === token) {
3443 if (status === 'ok') {
3444 this.invalidAuthTokenCount_ = 0;
3445 }
3446 else {
3447 // Triggers reconnect and force refresh for auth token
3448 this.onAuthRevoked_(status, data);
3449 }
3450 }
3451 });
3452 }
3453 }
3454 /**
3455 * Attempts to authenticate with the given token. If the authentication
3456 * attempt fails, it's triggered like the token was revoked (the connection is
3457 * closed).
3458 */
3459 tryAppCheck() {
3460 if (this.connected_ && this.appCheckToken_) {
3461 this.sendRequest('appcheck', { 'token': this.appCheckToken_ }, (res) => {
3462 const status = res[ /*status*/'s'];
3463 const data = res[ /*data*/'d'] || 'error';
3464 if (status === 'ok') {
3465 this.invalidAppCheckTokenCount_ = 0;
3466 }
3467 else {
3468 this.onAppCheckRevoked_(status, data);
3469 }
3470 });
3471 }
3472 }
3473 /**
3474 * @inheritDoc
3475 */
3476 unlisten(query, tag) {
3477 const pathString = query._path.toString();
3478 const queryId = query._queryIdentifier;
3479 this.log_('Unlisten called for ' + pathString + ' ' + queryId);
3480 assert(query._queryParams.isDefault() || !query._queryParams.loadsAllData(), 'unlisten() called for non-default but complete query');
3481 const listen = this.removeListen_(pathString, queryId);
3482 if (listen && this.connected_) {
3483 this.sendUnlisten_(pathString, queryId, query._queryObject, tag);
3484 }
3485 }
3486 sendUnlisten_(pathString, queryId, queryObj, tag) {
3487 this.log_('Unlisten on ' + pathString + ' for ' + queryId);
3488 const req = { /*path*/ p: pathString };
3489 const action = 'n';
3490 // Only bother sending queryId if it's non-default.
3491 if (tag) {
3492 req['q'] = queryObj;
3493 req['t'] = tag;
3494 }
3495 this.sendRequest(action, req);
3496 }
3497 onDisconnectPut(pathString, data, onComplete) {
3498 this.initConnection_();
3499 if (this.connected_) {
3500 this.sendOnDisconnect_('o', pathString, data, onComplete);
3501 }
3502 else {
3503 this.onDisconnectRequestQueue_.push({
3504 pathString,
3505 action: 'o',
3506 data,
3507 onComplete
3508 });
3509 }
3510 }
3511 onDisconnectMerge(pathString, data, onComplete) {
3512 this.initConnection_();
3513 if (this.connected_) {
3514 this.sendOnDisconnect_('om', pathString, data, onComplete);
3515 }
3516 else {
3517 this.onDisconnectRequestQueue_.push({
3518 pathString,
3519 action: 'om',
3520 data,
3521 onComplete
3522 });
3523 }
3524 }
3525 onDisconnectCancel(pathString, onComplete) {
3526 this.initConnection_();
3527 if (this.connected_) {
3528 this.sendOnDisconnect_('oc', pathString, null, onComplete);
3529 }
3530 else {
3531 this.onDisconnectRequestQueue_.push({
3532 pathString,
3533 action: 'oc',
3534 data: null,
3535 onComplete
3536 });
3537 }
3538 }
3539 sendOnDisconnect_(action, pathString, data, onComplete) {
3540 const request = { /*path*/ p: pathString, /*data*/ d: data };
3541 this.log_('onDisconnect ' + action, request);
3542 this.sendRequest(action, request, (response) => {
3543 if (onComplete) {
3544 setTimeout(() => {
3545 onComplete(response[ /*status*/'s'], response[ /* data */'d']);
3546 }, Math.floor(0));
3547 }
3548 });
3549 }
3550 put(pathString, data, onComplete, hash) {
3551 this.putInternal('p', pathString, data, onComplete, hash);
3552 }
3553 merge(pathString, data, onComplete, hash) {
3554 this.putInternal('m', pathString, data, onComplete, hash);
3555 }
3556 putInternal(action, pathString, data, onComplete, hash) {
3557 this.initConnection_();
3558 const request = {
3559 /*path*/ p: pathString,
3560 /*data*/ d: data
3561 };
3562 if (hash !== undefined) {
3563 request[ /*hash*/'h'] = hash;
3564 }
3565 // TODO: Only keep track of the most recent put for a given path?
3566 this.outstandingPuts_.push({
3567 action,
3568 request,
3569 onComplete
3570 });
3571 this.outstandingPutCount_++;
3572 const index = this.outstandingPuts_.length - 1;
3573 if (this.connected_) {
3574 this.sendPut_(index);
3575 }
3576 else {
3577 this.log_('Buffering put: ' + pathString);
3578 }
3579 }
3580 sendPut_(index) {
3581 const action = this.outstandingPuts_[index].action;
3582 const request = this.outstandingPuts_[index].request;
3583 const onComplete = this.outstandingPuts_[index].onComplete;
3584 this.outstandingPuts_[index].queued = this.connected_;
3585 this.sendRequest(action, request, (message) => {
3586 this.log_(action + ' response', message);
3587 delete this.outstandingPuts_[index];
3588 this.outstandingPutCount_--;
3589 // Clean up array occasionally.
3590 if (this.outstandingPutCount_ === 0) {
3591 this.outstandingPuts_ = [];
3592 }
3593 if (onComplete) {
3594 onComplete(message[ /*status*/'s'], message[ /* data */'d']);
3595 }
3596 });
3597 }
3598 reportStats(stats) {
3599 // If we're not connected, we just drop the stats.
3600 if (this.connected_) {
3601 const request = { /*counters*/ c: stats };
3602 this.log_('reportStats', request);
3603 this.sendRequest(/*stats*/ 's', request, result => {
3604 const status = result[ /*status*/'s'];
3605 if (status !== 'ok') {
3606 const errorReason = result[ /* data */'d'];
3607 this.log_('reportStats', 'Error sending stats: ' + errorReason);
3608 }
3609 });
3610 }
3611 }
3612 onDataMessage_(message) {
3613 if ('r' in message) {
3614 // this is a response
3615 this.log_('from server: ' + stringify(message));
3616 const reqNum = message['r'];
3617 const onResponse = this.requestCBHash_[reqNum];
3618 if (onResponse) {
3619 delete this.requestCBHash_[reqNum];
3620 onResponse(message[ /*body*/'b']);
3621 }
3622 }
3623 else if ('error' in message) {
3624 throw 'A server-side error has occurred: ' + message['error'];
3625 }
3626 else if ('a' in message) {
3627 // a and b are action and body, respectively
3628 this.onDataPush_(message['a'], message['b']);
3629 }
3630 }
3631 onDataPush_(action, body) {
3632 this.log_('handleServerMessage', action, body);
3633 if (action === 'd') {
3634 this.onDataUpdate_(body[ /*path*/'p'], body[ /*data*/'d'],
3635 /*isMerge*/ false, body['t']);
3636 }
3637 else if (action === 'm') {
3638 this.onDataUpdate_(body[ /*path*/'p'], body[ /*data*/'d'],
3639 /*isMerge=*/ true, body['t']);
3640 }
3641 else if (action === 'c') {
3642 this.onListenRevoked_(body[ /*path*/'p'], body[ /*query*/'q']);
3643 }
3644 else if (action === 'ac') {
3645 this.onAuthRevoked_(body[ /*status code*/'s'], body[ /* explanation */'d']);
3646 }
3647 else if (action === 'apc') {
3648 this.onAppCheckRevoked_(body[ /*status code*/'s'], body[ /* explanation */'d']);
3649 }
3650 else if (action === 'sd') {
3651 this.onSecurityDebugPacket_(body);
3652 }
3653 else {
3654 error('Unrecognized action received from server: ' +
3655 stringify(action) +
3656 '\nAre you using the latest client?');
3657 }
3658 }
3659 onReady_(timestamp, sessionId) {
3660 this.log_('connection ready');
3661 this.connected_ = true;
3662 this.lastConnectionEstablishedTime_ = new Date().getTime();
3663 this.handleTimestamp_(timestamp);
3664 this.lastSessionId = sessionId;
3665 if (this.firstConnection_) {
3666 this.sendConnectStats_();
3667 }
3668 this.restoreState_();
3669 this.firstConnection_ = false;
3670 this.onConnectStatus_(true);
3671 }
3672 scheduleConnect_(timeout) {
3673 assert(!this.realtime_, "Scheduling a connect when we're already connected/ing?");
3674 if (this.establishConnectionTimer_) {
3675 clearTimeout(this.establishConnectionTimer_);
3676 }
3677 // NOTE: Even when timeout is 0, it's important to do a setTimeout to work around an infuriating "Security Error" in
3678 // Firefox when trying to write to our long-polling iframe in some scenarios (e.g. Forge or our unit tests).
3679 this.establishConnectionTimer_ = setTimeout(() => {
3680 this.establishConnectionTimer_ = null;
3681 this.establishConnection_();
3682 // eslint-disable-next-line @typescript-eslint/no-explicit-any
3683 }, Math.floor(timeout));
3684 }
3685 initConnection_() {
3686 if (!this.realtime_ && this.firstConnection_) {
3687 this.scheduleConnect_(0);
3688 }
3689 }
3690 onVisible_(visible) {
3691 // NOTE: Tabbing away and back to a window will defeat our reconnect backoff, but I think that's fine.
3692 if (visible &&
3693 !this.visible_ &&
3694 this.reconnectDelay_ === this.maxReconnectDelay_) {
3695 this.log_('Window became visible. Reducing delay.');
3696 this.reconnectDelay_ = RECONNECT_MIN_DELAY;
3697 if (!this.realtime_) {
3698 this.scheduleConnect_(0);
3699 }
3700 }
3701 this.visible_ = visible;
3702 }
3703 onOnline_(online) {
3704 if (online) {
3705 this.log_('Browser went online.');
3706 this.reconnectDelay_ = RECONNECT_MIN_DELAY;
3707 if (!this.realtime_) {
3708 this.scheduleConnect_(0);
3709 }
3710 }
3711 else {
3712 this.log_('Browser went offline. Killing connection.');
3713 if (this.realtime_) {
3714 this.realtime_.close();
3715 }
3716 }
3717 }
3718 onRealtimeDisconnect_() {
3719 this.log_('data client disconnected');
3720 this.connected_ = false;
3721 this.realtime_ = null;
3722 // Since we don't know if our sent transactions succeeded or not, we need to cancel them.
3723 this.cancelSentTransactions_();
3724 // Clear out the pending requests.
3725 this.requestCBHash_ = {};
3726 if (this.shouldReconnect_()) {
3727 if (!this.visible_) {
3728 this.log_("Window isn't visible. Delaying reconnect.");
3729 this.reconnectDelay_ = this.maxReconnectDelay_;
3730 this.lastConnectionAttemptTime_ = new Date().getTime();
3731 }
3732 else if (this.lastConnectionEstablishedTime_) {
3733 // If we've been connected long enough, reset reconnect delay to minimum.
3734 const timeSinceLastConnectSucceeded = new Date().getTime() - this.lastConnectionEstablishedTime_;
3735 if (timeSinceLastConnectSucceeded > RECONNECT_DELAY_RESET_TIMEOUT) {
3736 this.reconnectDelay_ = RECONNECT_MIN_DELAY;
3737 }
3738 this.lastConnectionEstablishedTime_ = null;
3739 }
3740 const timeSinceLastConnectAttempt = new Date().getTime() - this.lastConnectionAttemptTime_;
3741 let reconnectDelay = Math.max(0, this.reconnectDelay_ - timeSinceLastConnectAttempt);
3742 reconnectDelay = Math.random() * reconnectDelay;
3743 this.log_('Trying to reconnect in ' + reconnectDelay + 'ms');
3744 this.scheduleConnect_(reconnectDelay);
3745 // Adjust reconnect delay for next time.
3746 this.reconnectDelay_ = Math.min(this.maxReconnectDelay_, this.reconnectDelay_ * RECONNECT_DELAY_MULTIPLIER);
3747 }
3748 this.onConnectStatus_(false);
3749 }
3750 async establishConnection_() {
3751 if (this.shouldReconnect_()) {
3752 this.log_('Making a connection attempt');
3753 this.lastConnectionAttemptTime_ = new Date().getTime();
3754 this.lastConnectionEstablishedTime_ = null;
3755 const onDataMessage = this.onDataMessage_.bind(this);
3756 const onReady = this.onReady_.bind(this);
3757 const onDisconnect = this.onRealtimeDisconnect_.bind(this);
3758 const connId = this.id + ':' + PersistentConnection.nextConnectionId_++;
3759 const lastSessionId = this.lastSessionId;
3760 let canceled = false;
3761 let connection = null;
3762 const closeFn = function () {
3763 if (connection) {
3764 connection.close();
3765 }
3766 else {
3767 canceled = true;
3768 onDisconnect();
3769 }
3770 };
3771 const sendRequestFn = function (msg) {
3772 assert(connection, "sendRequest call when we're not connected not allowed.");
3773 connection.sendRequest(msg);
3774 };
3775 this.realtime_ = {
3776 close: closeFn,
3777 sendRequest: sendRequestFn
3778 };
3779 const forceRefresh = this.forceTokenRefresh_;
3780 this.forceTokenRefresh_ = false;
3781 try {
3782 // First fetch auth and app check token, and establish connection after
3783 // fetching the token was successful
3784 const [authToken, appCheckToken] = await Promise.all([
3785 this.authTokenProvider_.getToken(forceRefresh),
3786 this.appCheckTokenProvider_.getToken(forceRefresh)
3787 ]);
3788 if (!canceled) {
3789 log('getToken() completed. Creating connection.');
3790 this.authToken_ = authToken && authToken.accessToken;
3791 this.appCheckToken_ = appCheckToken && appCheckToken.token;
3792 connection = new Connection(connId, this.repoInfo_, this.applicationId_, this.appCheckToken_, this.authToken_, onDataMessage, onReady, onDisconnect,
3793 /* onKill= */ reason => {
3794 warn(reason + ' (' + this.repoInfo_.toString() + ')');
3795 this.interrupt(SERVER_KILL_INTERRUPT_REASON);
3796 }, lastSessionId);
3797 }
3798 else {
3799 log('getToken() completed but was canceled');
3800 }
3801 }
3802 catch (error) {
3803 this.log_('Failed to get token: ' + error);
3804 if (!canceled) {
3805 if (this.repoInfo_.nodeAdmin) {
3806 // This may be a critical error for the Admin Node.js SDK, so log a warning.
3807 // But getToken() may also just have temporarily failed, so we still want to
3808 // continue retrying.
3809 warn(error);
3810 }
3811 closeFn();
3812 }
3813 }
3814 }
3815 }
3816 interrupt(reason) {
3817 log('Interrupting connection for reason: ' + reason);
3818 this.interruptReasons_[reason] = true;
3819 if (this.realtime_) {
3820 this.realtime_.close();
3821 }
3822 else {
3823 if (this.establishConnectionTimer_) {
3824 clearTimeout(this.establishConnectionTimer_);
3825 this.establishConnectionTimer_ = null;
3826 }
3827 if (this.connected_) {
3828 this.onRealtimeDisconnect_();
3829 }
3830 }
3831 }
3832 resume(reason) {
3833 log('Resuming connection for reason: ' + reason);
3834 delete this.interruptReasons_[reason];
3835 if (isEmpty(this.interruptReasons_)) {
3836 this.reconnectDelay_ = RECONNECT_MIN_DELAY;
3837 if (!this.realtime_) {
3838 this.scheduleConnect_(0);
3839 }
3840 }
3841 }
3842 handleTimestamp_(timestamp) {
3843 const delta = timestamp - new Date().getTime();
3844 this.onServerInfoUpdate_({ serverTimeOffset: delta });
3845 }
3846 cancelSentTransactions_() {
3847 for (let i = 0; i < this.outstandingPuts_.length; i++) {
3848 const put = this.outstandingPuts_[i];
3849 if (put && /*hash*/ 'h' in put.request && put.queued) {
3850 if (put.onComplete) {
3851 put.onComplete('disconnect');
3852 }
3853 delete this.outstandingPuts_[i];
3854 this.outstandingPutCount_--;
3855 }
3856 }
3857 // Clean up array occasionally.
3858 if (this.outstandingPutCount_ === 0) {
3859 this.outstandingPuts_ = [];
3860 }
3861 }
3862 onListenRevoked_(pathString, query) {
3863 // Remove the listen and manufacture a "permission_denied" error for the failed listen.
3864 let queryId;
3865 if (!query) {
3866 queryId = 'default';
3867 }
3868 else {
3869 queryId = query.map(q => ObjectToUniqueKey(q)).join('$');
3870 }
3871 const listen = this.removeListen_(pathString, queryId);
3872 if (listen && listen.onComplete) {
3873 listen.onComplete('permission_denied');
3874 }
3875 }
3876 removeListen_(pathString, queryId) {
3877 const normalizedPathString = new Path(pathString).toString(); // normalize path.
3878 let listen;
3879 if (this.listens.has(normalizedPathString)) {
3880 const map = this.listens.get(normalizedPathString);
3881 listen = map.get(queryId);
3882 map.delete(queryId);
3883 if (map.size === 0) {
3884 this.listens.delete(normalizedPathString);
3885 }
3886 }
3887 else {
3888 // all listens for this path has already been removed
3889 listen = undefined;
3890 }
3891 return listen;
3892 }
3893 onAuthRevoked_(statusCode, explanation) {
3894 log('Auth token revoked: ' + statusCode + '/' + explanation);
3895 this.authToken_ = null;
3896 this.forceTokenRefresh_ = true;
3897 this.realtime_.close();
3898 if (statusCode === 'invalid_token' || statusCode === 'permission_denied') {
3899 // We'll wait a couple times before logging the warning / increasing the
3900 // retry period since oauth tokens will report as "invalid" if they're
3901 // just expired. Plus there may be transient issues that resolve themselves.
3902 this.invalidAuthTokenCount_++;
3903 if (this.invalidAuthTokenCount_ >= INVALID_TOKEN_THRESHOLD) {
3904 // Set a long reconnect delay because recovery is unlikely
3905 this.reconnectDelay_ = RECONNECT_MAX_DELAY_FOR_ADMINS;
3906 // Notify the auth token provider that the token is invalid, which will log
3907 // a warning
3908 this.authTokenProvider_.notifyForInvalidToken();
3909 }
3910 }
3911 }
3912 onAppCheckRevoked_(statusCode, explanation) {
3913 log('App check token revoked: ' + statusCode + '/' + explanation);
3914 this.appCheckToken_ = null;
3915 this.forceTokenRefresh_ = true;
3916 // Note: We don't close the connection as the developer may not have
3917 // enforcement enabled. The backend closes connections with enforcements.
3918 if (statusCode === 'invalid_token' || statusCode === 'permission_denied') {
3919 // We'll wait a couple times before logging the warning / increasing the
3920 // retry period since oauth tokens will report as "invalid" if they're
3921 // just expired. Plus there may be transient issues that resolve themselves.
3922 this.invalidAppCheckTokenCount_++;
3923 if (this.invalidAppCheckTokenCount_ >= INVALID_TOKEN_THRESHOLD) {
3924 this.appCheckTokenProvider_.notifyForInvalidToken();
3925 }
3926 }
3927 }
3928 onSecurityDebugPacket_(body) {
3929 if (this.securityDebugCallback_) {
3930 this.securityDebugCallback_(body);
3931 }
3932 else {
3933 if ('msg' in body) {
3934 console.log('FIREBASE: ' + body['msg'].replace('\n', '\nFIREBASE: '));
3935 }
3936 }
3937 }
3938 restoreState_() {
3939 //Re-authenticate ourselves if we have a credential stored.
3940 this.tryAuth();
3941 this.tryAppCheck();
3942 // Puts depend on having received the corresponding data update from the server before they complete, so we must
3943 // make sure to send listens before puts.
3944 for (const queries of this.listens.values()) {
3945 for (const listenSpec of queries.values()) {
3946 this.sendListen_(listenSpec);
3947 }
3948 }
3949 for (let i = 0; i < this.outstandingPuts_.length; i++) {
3950 if (this.outstandingPuts_[i]) {
3951 this.sendPut_(i);
3952 }
3953 }
3954 while (this.onDisconnectRequestQueue_.length) {
3955 const request = this.onDisconnectRequestQueue_.shift();
3956 this.sendOnDisconnect_(request.action, request.pathString, request.data, request.onComplete);
3957 }
3958 for (let i = 0; i < this.outstandingGets_.length; i++) {
3959 if (this.outstandingGets_[i]) {
3960 this.sendGet_(i);
3961 }
3962 }
3963 }
3964 /**
3965 * Sends client stats for first connection
3966 */
3967 sendConnectStats_() {
3968 const stats = {};
3969 let clientName = 'js';
3970 if (isNodeSdk()) {
3971 if (this.repoInfo_.nodeAdmin) {
3972 clientName = 'admin_node';
3973 }
3974 else {
3975 clientName = 'node';
3976 }
3977 }
3978 stats['sdk.' + clientName + '.' + SDK_VERSION.replace(/\./g, '-')] = 1;
3979 if (isMobileCordova()) {
3980 stats['framework.cordova'] = 1;
3981 }
3982 else if (isReactNative()) {
3983 stats['framework.reactnative'] = 1;
3984 }
3985 this.reportStats(stats);
3986 }
3987 shouldReconnect_() {
3988 const online = OnlineMonitor.getInstance().currentlyOnline();
3989 return isEmpty(this.interruptReasons_) && online;
3990 }
3991}
3992PersistentConnection.nextPersistentConnectionId_ = 0;
3993/**
3994 * Counter for number of connections created. Mainly used for tagging in the logs
3995 */
3996PersistentConnection.nextConnectionId_ = 0;
3997
3998/**
3999 * @license
4000 * Copyright 2017 Google LLC
4001 *
4002 * Licensed under the Apache License, Version 2.0 (the "License");
4003 * you may not use this file except in compliance with the License.
4004 * You may obtain a copy of the License at
4005 *
4006 * http://www.apache.org/licenses/LICENSE-2.0
4007 *
4008 * Unless required by applicable law or agreed to in writing, software
4009 * distributed under the License is distributed on an "AS IS" BASIS,
4010 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4011 * See the License for the specific language governing permissions and
4012 * limitations under the License.
4013 */
4014class NamedNode {
4015 constructor(name, node) {
4016 this.name = name;
4017 this.node = node;
4018 }
4019 static Wrap(name, node) {
4020 return new NamedNode(name, node);
4021 }
4022}
4023
4024/**
4025 * @license
4026 * Copyright 2017 Google LLC
4027 *
4028 * Licensed under the Apache License, Version 2.0 (the "License");
4029 * you may not use this file except in compliance with the License.
4030 * You may obtain a copy of the License at
4031 *
4032 * http://www.apache.org/licenses/LICENSE-2.0
4033 *
4034 * Unless required by applicable law or agreed to in writing, software
4035 * distributed under the License is distributed on an "AS IS" BASIS,
4036 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4037 * See the License for the specific language governing permissions and
4038 * limitations under the License.
4039 */
4040class Index {
4041 /**
4042 * @returns A standalone comparison function for
4043 * this index
4044 */
4045 getCompare() {
4046 return this.compare.bind(this);
4047 }
4048 /**
4049 * Given a before and after value for a node, determine if the indexed value has changed. Even if they are different,
4050 * it's possible that the changes are isolated to parts of the snapshot that are not indexed.
4051 *
4052 *
4053 * @returns True if the portion of the snapshot being indexed changed between oldNode and newNode
4054 */
4055 indexedValueChanged(oldNode, newNode) {
4056 const oldWrapped = new NamedNode(MIN_NAME, oldNode);
4057 const newWrapped = new NamedNode(MIN_NAME, newNode);
4058 return this.compare(oldWrapped, newWrapped) !== 0;
4059 }
4060 /**
4061 * @returns a node wrapper that will sort equal to or less than
4062 * any other node wrapper, using this index
4063 */
4064 minPost() {
4065 // eslint-disable-next-line @typescript-eslint/no-explicit-any
4066 return NamedNode.MIN;
4067 }
4068}
4069
4070/**
4071 * @license
4072 * Copyright 2017 Google LLC
4073 *
4074 * Licensed under the Apache License, Version 2.0 (the "License");
4075 * you may not use this file except in compliance with the License.
4076 * You may obtain a copy of the License at
4077 *
4078 * http://www.apache.org/licenses/LICENSE-2.0
4079 *
4080 * Unless required by applicable law or agreed to in writing, software
4081 * distributed under the License is distributed on an "AS IS" BASIS,
4082 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4083 * See the License for the specific language governing permissions and
4084 * limitations under the License.
4085 */
4086let __EMPTY_NODE;
4087class KeyIndex extends Index {
4088 static get __EMPTY_NODE() {
4089 return __EMPTY_NODE;
4090 }
4091 static set __EMPTY_NODE(val) {
4092 __EMPTY_NODE = val;
4093 }
4094 compare(a, b) {
4095 return nameCompare(a.name, b.name);
4096 }
4097 isDefinedOn(node) {
4098 // We could probably return true here (since every node has a key), but it's never called
4099 // so just leaving unimplemented for now.
4100 throw assertionError('KeyIndex.isDefinedOn not expected to be called.');
4101 }
4102 indexedValueChanged(oldNode, newNode) {
4103 return false; // The key for a node never changes.
4104 }
4105 minPost() {
4106 // eslint-disable-next-line @typescript-eslint/no-explicit-any
4107 return NamedNode.MIN;
4108 }
4109 maxPost() {
4110 // TODO: This should really be created once and cached in a static property, but
4111 // NamedNode isn't defined yet, so I can't use it in a static. Bleh.
4112 return new NamedNode(MAX_NAME, __EMPTY_NODE);
4113 }
4114 makePost(indexValue, name) {
4115 assert(typeof indexValue === 'string', 'KeyIndex indexValue must always be a string.');
4116 // We just use empty node, but it'll never be compared, since our comparator only looks at name.
4117 return new NamedNode(indexValue, __EMPTY_NODE);
4118 }
4119 /**
4120 * @returns String representation for inclusion in a query spec
4121 */
4122 toString() {
4123 return '.key';
4124 }
4125}
4126const KEY_INDEX = new KeyIndex();
4127
4128/**
4129 * @license
4130 * Copyright 2017 Google LLC
4131 *
4132 * Licensed under the Apache License, Version 2.0 (the "License");
4133 * you may not use this file except in compliance with the License.
4134 * You may obtain a copy of the License at
4135 *
4136 * http://www.apache.org/licenses/LICENSE-2.0
4137 *
4138 * Unless required by applicable law or agreed to in writing, software
4139 * distributed under the License is distributed on an "AS IS" BASIS,
4140 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4141 * See the License for the specific language governing permissions and
4142 * limitations under the License.
4143 */
4144/**
4145 * An iterator over an LLRBNode.
4146 */
4147class SortedMapIterator {
4148 /**
4149 * @param node - Node to iterate.
4150 * @param isReverse_ - Whether or not to iterate in reverse
4151 */
4152 constructor(node, startKey, comparator, isReverse_, resultGenerator_ = null) {
4153 this.isReverse_ = isReverse_;
4154 this.resultGenerator_ = resultGenerator_;
4155 this.nodeStack_ = [];
4156 let cmp = 1;
4157 while (!node.isEmpty()) {
4158 node = node;
4159 cmp = startKey ? comparator(node.key, startKey) : 1;
4160 // flip the comparison if we're going in reverse
4161 if (isReverse_) {
4162 cmp *= -1;
4163 }
4164 if (cmp < 0) {
4165 // This node is less than our start key. ignore it
4166 if (this.isReverse_) {
4167 node = node.left;
4168 }
4169 else {
4170 node = node.right;
4171 }
4172 }
4173 else if (cmp === 0) {
4174 // This node is exactly equal to our start key. Push it on the stack, but stop iterating;
4175 this.nodeStack_.push(node);
4176 break;
4177 }
4178 else {
4179 // This node is greater than our start key, add it to the stack and move to the next one
4180 this.nodeStack_.push(node);
4181 if (this.isReverse_) {
4182 node = node.right;
4183 }
4184 else {
4185 node = node.left;
4186 }
4187 }
4188 }
4189 }
4190 getNext() {
4191 if (this.nodeStack_.length === 0) {
4192 return null;
4193 }
4194 let node = this.nodeStack_.pop();
4195 let result;
4196 if (this.resultGenerator_) {
4197 result = this.resultGenerator_(node.key, node.value);
4198 }
4199 else {
4200 result = { key: node.key, value: node.value };
4201 }
4202 if (this.isReverse_) {
4203 node = node.left;
4204 while (!node.isEmpty()) {
4205 this.nodeStack_.push(node);
4206 node = node.right;
4207 }
4208 }
4209 else {
4210 node = node.right;
4211 while (!node.isEmpty()) {
4212 this.nodeStack_.push(node);
4213 node = node.left;
4214 }
4215 }
4216 return result;
4217 }
4218 hasNext() {
4219 return this.nodeStack_.length > 0;
4220 }
4221 peek() {
4222 if (this.nodeStack_.length === 0) {
4223 return null;
4224 }
4225 const node = this.nodeStack_[this.nodeStack_.length - 1];
4226 if (this.resultGenerator_) {
4227 return this.resultGenerator_(node.key, node.value);
4228 }
4229 else {
4230 return { key: node.key, value: node.value };
4231 }
4232 }
4233}
4234/**
4235 * Represents a node in a Left-leaning Red-Black tree.
4236 */
4237class LLRBNode {
4238 /**
4239 * @param key - Key associated with this node.
4240 * @param value - Value associated with this node.
4241 * @param color - Whether this node is red.
4242 * @param left - Left child.
4243 * @param right - Right child.
4244 */
4245 constructor(key, value, color, left, right) {
4246 this.key = key;
4247 this.value = value;
4248 this.color = color != null ? color : LLRBNode.RED;
4249 this.left =
4250 left != null ? left : SortedMap.EMPTY_NODE;
4251 this.right =
4252 right != null ? right : SortedMap.EMPTY_NODE;
4253 }
4254 /**
4255 * Returns a copy of the current node, optionally replacing pieces of it.
4256 *
4257 * @param key - New key for the node, or null.
4258 * @param value - New value for the node, or null.
4259 * @param color - New color for the node, or null.
4260 * @param left - New left child for the node, or null.
4261 * @param right - New right child for the node, or null.
4262 * @returns The node copy.
4263 */
4264 copy(key, value, color, left, right) {
4265 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);
4266 }
4267 /**
4268 * @returns The total number of nodes in the tree.
4269 */
4270 count() {
4271 return this.left.count() + 1 + this.right.count();
4272 }
4273 /**
4274 * @returns True if the tree is empty.
4275 */
4276 isEmpty() {
4277 return false;
4278 }
4279 /**
4280 * Traverses the tree in key order and calls the specified action function
4281 * for each node.
4282 *
4283 * @param action - Callback function to be called for each
4284 * node. If it returns true, traversal is aborted.
4285 * @returns The first truthy value returned by action, or the last falsey
4286 * value returned by action
4287 */
4288 inorderTraversal(action) {
4289 return (this.left.inorderTraversal(action) ||
4290 !!action(this.key, this.value) ||
4291 this.right.inorderTraversal(action));
4292 }
4293 /**
4294 * Traverses the tree in reverse key order and calls the specified action function
4295 * for each node.
4296 *
4297 * @param action - Callback function to be called for each
4298 * node. If it returns true, traversal is aborted.
4299 * @returns True if traversal was aborted.
4300 */
4301 reverseTraversal(action) {
4302 return (this.right.reverseTraversal(action) ||
4303 action(this.key, this.value) ||
4304 this.left.reverseTraversal(action));
4305 }
4306 /**
4307 * @returns The minimum node in the tree.
4308 */
4309 min_() {
4310 if (this.left.isEmpty()) {
4311 return this;
4312 }
4313 else {
4314 return this.left.min_();
4315 }
4316 }
4317 /**
4318 * @returns The maximum key in the tree.
4319 */
4320 minKey() {
4321 return this.min_().key;
4322 }
4323 /**
4324 * @returns The maximum key in the tree.
4325 */
4326 maxKey() {
4327 if (this.right.isEmpty()) {
4328 return this.key;
4329 }
4330 else {
4331 return this.right.maxKey();
4332 }
4333 }
4334 /**
4335 * @param key - Key to insert.
4336 * @param value - Value to insert.
4337 * @param comparator - Comparator.
4338 * @returns New tree, with the key/value added.
4339 */
4340 insert(key, value, comparator) {
4341 let n = this;
4342 const cmp = comparator(key, n.key);
4343 if (cmp < 0) {
4344 n = n.copy(null, null, null, n.left.insert(key, value, comparator), null);
4345 }
4346 else if (cmp === 0) {
4347 n = n.copy(null, value, null, null, null);
4348 }
4349 else {
4350 n = n.copy(null, null, null, null, n.right.insert(key, value, comparator));
4351 }
4352 return n.fixUp_();
4353 }
4354 /**
4355 * @returns New tree, with the minimum key removed.
4356 */
4357 removeMin_() {
4358 if (this.left.isEmpty()) {
4359 return SortedMap.EMPTY_NODE;
4360 }
4361 let n = this;
4362 if (!n.left.isRed_() && !n.left.left.isRed_()) {
4363 n = n.moveRedLeft_();
4364 }
4365 n = n.copy(null, null, null, n.left.removeMin_(), null);
4366 return n.fixUp_();
4367 }
4368 /**
4369 * @param key - The key of the item to remove.
4370 * @param comparator - Comparator.
4371 * @returns New tree, with the specified item removed.
4372 */
4373 remove(key, comparator) {
4374 let n, smallest;
4375 n = this;
4376 if (comparator(key, n.key) < 0) {
4377 if (!n.left.isEmpty() && !n.left.isRed_() && !n.left.left.isRed_()) {
4378 n = n.moveRedLeft_();
4379 }
4380 n = n.copy(null, null, null, n.left.remove(key, comparator), null);
4381 }
4382 else {
4383 if (n.left.isRed_()) {
4384 n = n.rotateRight_();
4385 }
4386 if (!n.right.isEmpty() && !n.right.isRed_() && !n.right.left.isRed_()) {
4387 n = n.moveRedRight_();
4388 }
4389 if (comparator(key, n.key) === 0) {
4390 if (n.right.isEmpty()) {
4391 return SortedMap.EMPTY_NODE;
4392 }
4393 else {
4394 smallest = n.right.min_();
4395 n = n.copy(smallest.key, smallest.value, null, null, n.right.removeMin_());
4396 }
4397 }
4398 n = n.copy(null, null, null, null, n.right.remove(key, comparator));
4399 }
4400 return n.fixUp_();
4401 }
4402 /**
4403 * @returns Whether this is a RED node.
4404 */
4405 isRed_() {
4406 return this.color;
4407 }
4408 /**
4409 * @returns New tree after performing any needed rotations.
4410 */
4411 fixUp_() {
4412 let n = this;
4413 if (n.right.isRed_() && !n.left.isRed_()) {
4414 n = n.rotateLeft_();
4415 }
4416 if (n.left.isRed_() && n.left.left.isRed_()) {
4417 n = n.rotateRight_();
4418 }
4419 if (n.left.isRed_() && n.right.isRed_()) {
4420 n = n.colorFlip_();
4421 }
4422 return n;
4423 }
4424 /**
4425 * @returns New tree, after moveRedLeft.
4426 */
4427 moveRedLeft_() {
4428 let n = this.colorFlip_();
4429 if (n.right.left.isRed_()) {
4430 n = n.copy(null, null, null, null, n.right.rotateRight_());
4431 n = n.rotateLeft_();
4432 n = n.colorFlip_();
4433 }
4434 return n;
4435 }
4436 /**
4437 * @returns New tree, after moveRedRight.
4438 */
4439 moveRedRight_() {
4440 let n = this.colorFlip_();
4441 if (n.left.left.isRed_()) {
4442 n = n.rotateRight_();
4443 n = n.colorFlip_();
4444 }
4445 return n;
4446 }
4447 /**
4448 * @returns New tree, after rotateLeft.
4449 */
4450 rotateLeft_() {
4451 const nl = this.copy(null, null, LLRBNode.RED, null, this.right.left);
4452 return this.right.copy(null, null, this.color, nl, null);
4453 }
4454 /**
4455 * @returns New tree, after rotateRight.
4456 */
4457 rotateRight_() {
4458 const nr = this.copy(null, null, LLRBNode.RED, this.left.right, null);
4459 return this.left.copy(null, null, this.color, null, nr);
4460 }
4461 /**
4462 * @returns Newt ree, after colorFlip.
4463 */
4464 colorFlip_() {
4465 const left = this.left.copy(null, null, !this.left.color, null, null);
4466 const right = this.right.copy(null, null, !this.right.color, null, null);
4467 return this.copy(null, null, !this.color, left, right);
4468 }
4469 /**
4470 * For testing.
4471 *
4472 * @returns True if all is well.
4473 */
4474 checkMaxDepth_() {
4475 const blackDepth = this.check_();
4476 return Math.pow(2.0, blackDepth) <= this.count() + 1;
4477 }
4478 check_() {
4479 if (this.isRed_() && this.left.isRed_()) {
4480 throw new Error('Red node has red child(' + this.key + ',' + this.value + ')');
4481 }
4482 if (this.right.isRed_()) {
4483 throw new Error('Right child of (' + this.key + ',' + this.value + ') is red');
4484 }
4485 const blackDepth = this.left.check_();
4486 if (blackDepth !== this.right.check_()) {
4487 throw new Error('Black depths differ');
4488 }
4489 else {
4490 return blackDepth + (this.isRed_() ? 0 : 1);
4491 }
4492 }
4493}
4494LLRBNode.RED = true;
4495LLRBNode.BLACK = false;
4496/**
4497 * Represents an empty node (a leaf node in the Red-Black Tree).
4498 */
4499class LLRBEmptyNode {
4500 /**
4501 * Returns a copy of the current node.
4502 *
4503 * @returns The node copy.
4504 */
4505 copy(key, value, color, left, right) {
4506 return this;
4507 }
4508 /**
4509 * Returns a copy of the tree, with the specified key/value added.
4510 *
4511 * @param key - Key to be added.
4512 * @param value - Value to be added.
4513 * @param comparator - Comparator.
4514 * @returns New tree, with item added.
4515 */
4516 insert(key, value, comparator) {
4517 return new LLRBNode(key, value, null);
4518 }
4519 /**
4520 * Returns a copy of the tree, with the specified key removed.
4521 *
4522 * @param key - The key to remove.
4523 * @param comparator - Comparator.
4524 * @returns New tree, with item removed.
4525 */
4526 remove(key, comparator) {
4527 return this;
4528 }
4529 /**
4530 * @returns The total number of nodes in the tree.
4531 */
4532 count() {
4533 return 0;
4534 }
4535 /**
4536 * @returns True if the tree is empty.
4537 */
4538 isEmpty() {
4539 return true;
4540 }
4541 /**
4542 * Traverses the tree in key order and calls the specified action function
4543 * for each node.
4544 *
4545 * @param action - Callback function to be called for each
4546 * node. If it returns true, traversal is aborted.
4547 * @returns True if traversal was aborted.
4548 */
4549 inorderTraversal(action) {
4550 return false;
4551 }
4552 /**
4553 * Traverses the tree in reverse key order and calls the specified action function
4554 * for each node.
4555 *
4556 * @param action - Callback function to be called for each
4557 * node. If it returns true, traversal is aborted.
4558 * @returns True if traversal was aborted.
4559 */
4560 reverseTraversal(action) {
4561 return false;
4562 }
4563 minKey() {
4564 return null;
4565 }
4566 maxKey() {
4567 return null;
4568 }
4569 check_() {
4570 return 0;
4571 }
4572 /**
4573 * @returns Whether this node is red.
4574 */
4575 isRed_() {
4576 return false;
4577 }
4578}
4579/**
4580 * An immutable sorted map implementation, based on a Left-leaning Red-Black
4581 * tree.
4582 */
4583class SortedMap {
4584 /**
4585 * @param comparator_ - Key comparator.
4586 * @param root_ - Optional root node for the map.
4587 */
4588 constructor(comparator_, root_ = SortedMap.EMPTY_NODE) {
4589 this.comparator_ = comparator_;
4590 this.root_ = root_;
4591 }
4592 /**
4593 * Returns a copy of the map, with the specified key/value added or replaced.
4594 * (TODO: We should perhaps rename this method to 'put')
4595 *
4596 * @param key - Key to be added.
4597 * @param value - Value to be added.
4598 * @returns New map, with item added.
4599 */
4600 insert(key, value) {
4601 return new SortedMap(this.comparator_, this.root_
4602 .insert(key, value, this.comparator_)
4603 .copy(null, null, LLRBNode.BLACK, null, null));
4604 }
4605 /**
4606 * Returns a copy of the map, with the specified key removed.
4607 *
4608 * @param key - The key to remove.
4609 * @returns New map, with item removed.
4610 */
4611 remove(key) {
4612 return new SortedMap(this.comparator_, this.root_
4613 .remove(key, this.comparator_)
4614 .copy(null, null, LLRBNode.BLACK, null, null));
4615 }
4616 /**
4617 * Returns the value of the node with the given key, or null.
4618 *
4619 * @param key - The key to look up.
4620 * @returns The value of the node with the given key, or null if the
4621 * key doesn't exist.
4622 */
4623 get(key) {
4624 let cmp;
4625 let node = this.root_;
4626 while (!node.isEmpty()) {
4627 cmp = this.comparator_(key, node.key);
4628 if (cmp === 0) {
4629 return node.value;
4630 }
4631 else if (cmp < 0) {
4632 node = node.left;
4633 }
4634 else if (cmp > 0) {
4635 node = node.right;
4636 }
4637 }
4638 return null;
4639 }
4640 /**
4641 * Returns the key of the item *before* the specified key, or null if key is the first item.
4642 * @param key - The key to find the predecessor of
4643 * @returns The predecessor key.
4644 */
4645 getPredecessorKey(key) {
4646 let cmp, node = this.root_, rightParent = null;
4647 while (!node.isEmpty()) {
4648 cmp = this.comparator_(key, node.key);
4649 if (cmp === 0) {
4650 if (!node.left.isEmpty()) {
4651 node = node.left;
4652 while (!node.right.isEmpty()) {
4653 node = node.right;
4654 }
4655 return node.key;
4656 }
4657 else if (rightParent) {
4658 return rightParent.key;
4659 }
4660 else {
4661 return null; // first item.
4662 }
4663 }
4664 else if (cmp < 0) {
4665 node = node.left;
4666 }
4667 else if (cmp > 0) {
4668 rightParent = node;
4669 node = node.right;
4670 }
4671 }
4672 throw new Error('Attempted to find predecessor key for a nonexistent key. What gives?');
4673 }
4674 /**
4675 * @returns True if the map is empty.
4676 */
4677 isEmpty() {
4678 return this.root_.isEmpty();
4679 }
4680 /**
4681 * @returns The total number of nodes in the map.
4682 */
4683 count() {
4684 return this.root_.count();
4685 }
4686 /**
4687 * @returns The minimum key in the map.
4688 */
4689 minKey() {
4690 return this.root_.minKey();
4691 }
4692 /**
4693 * @returns The maximum key in the map.
4694 */
4695 maxKey() {
4696 return this.root_.maxKey();
4697 }
4698 /**
4699 * Traverses the map in key order and calls the specified action function
4700 * for each key/value pair.
4701 *
4702 * @param action - Callback function to be called
4703 * for each key/value pair. If action returns true, traversal is aborted.
4704 * @returns The first truthy value returned by action, or the last falsey
4705 * value returned by action
4706 */
4707 inorderTraversal(action) {
4708 return this.root_.inorderTraversal(action);
4709 }
4710 /**
4711 * Traverses the map in reverse key order and calls the specified action function
4712 * for each key/value pair.
4713 *
4714 * @param action - Callback function to be called
4715 * for each key/value pair. If action returns true, traversal is aborted.
4716 * @returns True if the traversal was aborted.
4717 */
4718 reverseTraversal(action) {
4719 return this.root_.reverseTraversal(action);
4720 }
4721 /**
4722 * Returns an iterator over the SortedMap.
4723 * @returns The iterator.
4724 */
4725 getIterator(resultGenerator) {
4726 return new SortedMapIterator(this.root_, null, this.comparator_, false, resultGenerator);
4727 }
4728 getIteratorFrom(key, resultGenerator) {
4729 return new SortedMapIterator(this.root_, key, this.comparator_, false, resultGenerator);
4730 }
4731 getReverseIteratorFrom(key, resultGenerator) {
4732 return new SortedMapIterator(this.root_, key, this.comparator_, true, resultGenerator);
4733 }
4734 getReverseIterator(resultGenerator) {
4735 return new SortedMapIterator(this.root_, null, this.comparator_, true, resultGenerator);
4736 }
4737}
4738/**
4739 * Always use the same empty node, to reduce memory.
4740 */
4741SortedMap.EMPTY_NODE = new LLRBEmptyNode();
4742
4743/**
4744 * @license
4745 * Copyright 2017 Google LLC
4746 *
4747 * Licensed under the Apache License, Version 2.0 (the "License");
4748 * you may not use this file except in compliance with the License.
4749 * You may obtain a copy of the License at
4750 *
4751 * http://www.apache.org/licenses/LICENSE-2.0
4752 *
4753 * Unless required by applicable law or agreed to in writing, software
4754 * distributed under the License is distributed on an "AS IS" BASIS,
4755 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4756 * See the License for the specific language governing permissions and
4757 * limitations under the License.
4758 */
4759function NAME_ONLY_COMPARATOR(left, right) {
4760 return nameCompare(left.name, right.name);
4761}
4762function NAME_COMPARATOR(left, right) {
4763 return nameCompare(left, right);
4764}
4765
4766/**
4767 * @license
4768 * Copyright 2017 Google LLC
4769 *
4770 * Licensed under the Apache License, Version 2.0 (the "License");
4771 * you may not use this file except in compliance with the License.
4772 * You may obtain a copy of the License at
4773 *
4774 * http://www.apache.org/licenses/LICENSE-2.0
4775 *
4776 * Unless required by applicable law or agreed to in writing, software
4777 * distributed under the License is distributed on an "AS IS" BASIS,
4778 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4779 * See the License for the specific language governing permissions and
4780 * limitations under the License.
4781 */
4782let MAX_NODE$2;
4783function setMaxNode$1(val) {
4784 MAX_NODE$2 = val;
4785}
4786const priorityHashText = function (priority) {
4787 if (typeof priority === 'number') {
4788 return 'number:' + doubleToIEEE754String(priority);
4789 }
4790 else {
4791 return 'string:' + priority;
4792 }
4793};
4794/**
4795 * Validates that a priority snapshot Node is valid.
4796 */
4797const validatePriorityNode = function (priorityNode) {
4798 if (priorityNode.isLeafNode()) {
4799 const val = priorityNode.val();
4800 assert(typeof val === 'string' ||
4801 typeof val === 'number' ||
4802 (typeof val === 'object' && contains(val, '.sv')), 'Priority must be a string or number.');
4803 }
4804 else {
4805 assert(priorityNode === MAX_NODE$2 || priorityNode.isEmpty(), 'priority of unexpected type.');
4806 }
4807 // Don't call getPriority() on MAX_NODE to avoid hitting assertion.
4808 assert(priorityNode === MAX_NODE$2 || priorityNode.getPriority().isEmpty(), "Priority nodes can't have a priority of their own.");
4809};
4810
4811/**
4812 * @license
4813 * Copyright 2017 Google LLC
4814 *
4815 * Licensed under the Apache License, Version 2.0 (the "License");
4816 * you may not use this file except in compliance with the License.
4817 * You may obtain a copy of the License at
4818 *
4819 * http://www.apache.org/licenses/LICENSE-2.0
4820 *
4821 * Unless required by applicable law or agreed to in writing, software
4822 * distributed under the License is distributed on an "AS IS" BASIS,
4823 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4824 * See the License for the specific language governing permissions and
4825 * limitations under the License.
4826 */
4827let __childrenNodeConstructor;
4828/**
4829 * LeafNode is a class for storing leaf nodes in a DataSnapshot. It
4830 * implements Node and stores the value of the node (a string,
4831 * number, or boolean) accessible via getValue().
4832 */
4833class LeafNode {
4834 /**
4835 * @param value_ - The value to store in this leaf node. The object type is
4836 * possible in the event of a deferred value
4837 * @param priorityNode_ - The priority of this node.
4838 */
4839 constructor(value_, priorityNode_ = LeafNode.__childrenNodeConstructor.EMPTY_NODE) {
4840 this.value_ = value_;
4841 this.priorityNode_ = priorityNode_;
4842 this.lazyHash_ = null;
4843 assert(this.value_ !== undefined && this.value_ !== null, "LeafNode shouldn't be created with null/undefined value.");
4844 validatePriorityNode(this.priorityNode_);
4845 }
4846 static set __childrenNodeConstructor(val) {
4847 __childrenNodeConstructor = val;
4848 }
4849 static get __childrenNodeConstructor() {
4850 return __childrenNodeConstructor;
4851 }
4852 /** @inheritDoc */
4853 isLeafNode() {
4854 return true;
4855 }
4856 /** @inheritDoc */
4857 getPriority() {
4858 return this.priorityNode_;
4859 }
4860 /** @inheritDoc */
4861 updatePriority(newPriorityNode) {
4862 return new LeafNode(this.value_, newPriorityNode);
4863 }
4864 /** @inheritDoc */
4865 getImmediateChild(childName) {
4866 // Hack to treat priority as a regular child
4867 if (childName === '.priority') {
4868 return this.priorityNode_;
4869 }
4870 else {
4871 return LeafNode.__childrenNodeConstructor.EMPTY_NODE;
4872 }
4873 }
4874 /** @inheritDoc */
4875 getChild(path) {
4876 if (pathIsEmpty(path)) {
4877 return this;
4878 }
4879 else if (pathGetFront(path) === '.priority') {
4880 return this.priorityNode_;
4881 }
4882 else {
4883 return LeafNode.__childrenNodeConstructor.EMPTY_NODE;
4884 }
4885 }
4886 hasChild() {
4887 return false;
4888 }
4889 /** @inheritDoc */
4890 getPredecessorChildName(childName, childNode) {
4891 return null;
4892 }
4893 /** @inheritDoc */
4894 updateImmediateChild(childName, newChildNode) {
4895 if (childName === '.priority') {
4896 return this.updatePriority(newChildNode);
4897 }
4898 else if (newChildNode.isEmpty() && childName !== '.priority') {
4899 return this;
4900 }
4901 else {
4902 return LeafNode.__childrenNodeConstructor.EMPTY_NODE.updateImmediateChild(childName, newChildNode).updatePriority(this.priorityNode_);
4903 }
4904 }
4905 /** @inheritDoc */
4906 updateChild(path, newChildNode) {
4907 const front = pathGetFront(path);
4908 if (front === null) {
4909 return newChildNode;
4910 }
4911 else if (newChildNode.isEmpty() && front !== '.priority') {
4912 return this;
4913 }
4914 else {
4915 assert(front !== '.priority' || pathGetLength(path) === 1, '.priority must be the last token in a path');
4916 return this.updateImmediateChild(front, LeafNode.__childrenNodeConstructor.EMPTY_NODE.updateChild(pathPopFront(path), newChildNode));
4917 }
4918 }
4919 /** @inheritDoc */
4920 isEmpty() {
4921 return false;
4922 }
4923 /** @inheritDoc */
4924 numChildren() {
4925 return 0;
4926 }
4927 /** @inheritDoc */
4928 forEachChild(index, action) {
4929 return false;
4930 }
4931 val(exportFormat) {
4932 if (exportFormat && !this.getPriority().isEmpty()) {
4933 return {
4934 '.value': this.getValue(),
4935 '.priority': this.getPriority().val()
4936 };
4937 }
4938 else {
4939 return this.getValue();
4940 }
4941 }
4942 /** @inheritDoc */
4943 hash() {
4944 if (this.lazyHash_ === null) {
4945 let toHash = '';
4946 if (!this.priorityNode_.isEmpty()) {
4947 toHash +=
4948 'priority:' +
4949 priorityHashText(this.priorityNode_.val()) +
4950 ':';
4951 }
4952 const type = typeof this.value_;
4953 toHash += type + ':';
4954 if (type === 'number') {
4955 toHash += doubleToIEEE754String(this.value_);
4956 }
4957 else {
4958 toHash += this.value_;
4959 }
4960 this.lazyHash_ = sha1(toHash);
4961 }
4962 return this.lazyHash_;
4963 }
4964 /**
4965 * Returns the value of the leaf node.
4966 * @returns The value of the node.
4967 */
4968 getValue() {
4969 return this.value_;
4970 }
4971 compareTo(other) {
4972 if (other === LeafNode.__childrenNodeConstructor.EMPTY_NODE) {
4973 return 1;
4974 }
4975 else if (other instanceof LeafNode.__childrenNodeConstructor) {
4976 return -1;
4977 }
4978 else {
4979 assert(other.isLeafNode(), 'Unknown node type');
4980 return this.compareToLeafNode_(other);
4981 }
4982 }
4983 /**
4984 * Comparison specifically for two leaf nodes
4985 */
4986 compareToLeafNode_(otherLeaf) {
4987 const otherLeafType = typeof otherLeaf.value_;
4988 const thisLeafType = typeof this.value_;
4989 const otherIndex = LeafNode.VALUE_TYPE_ORDER.indexOf(otherLeafType);
4990 const thisIndex = LeafNode.VALUE_TYPE_ORDER.indexOf(thisLeafType);
4991 assert(otherIndex >= 0, 'Unknown leaf type: ' + otherLeafType);
4992 assert(thisIndex >= 0, 'Unknown leaf type: ' + thisLeafType);
4993 if (otherIndex === thisIndex) {
4994 // Same type, compare values
4995 if (thisLeafType === 'object') {
4996 // Deferred value nodes are all equal, but we should also never get to this point...
4997 return 0;
4998 }
4999 else {
5000 // Note that this works because true > false, all others are number or string comparisons
5001 if (this.value_ < otherLeaf.value_) {
5002 return -1;
5003 }
5004 else if (this.value_ === otherLeaf.value_) {
5005 return 0;
5006 }
5007 else {
5008 return 1;
5009 }
5010 }
5011 }
5012 else {
5013 return thisIndex - otherIndex;
5014 }
5015 }
5016 withIndex() {
5017 return this;
5018 }
5019 isIndexed() {
5020 return true;
5021 }
5022 equals(other) {
5023 if (other === this) {
5024 return true;
5025 }
5026 else if (other.isLeafNode()) {
5027 const otherLeaf = other;
5028 return (this.value_ === otherLeaf.value_ &&
5029 this.priorityNode_.equals(otherLeaf.priorityNode_));
5030 }
5031 else {
5032 return false;
5033 }
5034 }
5035}
5036/**
5037 * The sort order for comparing leaf nodes of different types. If two leaf nodes have
5038 * the same type, the comparison falls back to their value
5039 */
5040LeafNode.VALUE_TYPE_ORDER = ['object', 'boolean', 'number', 'string'];
5041
5042/**
5043 * @license
5044 * Copyright 2017 Google LLC
5045 *
5046 * Licensed under the Apache License, Version 2.0 (the "License");
5047 * you may not use this file except in compliance with the License.
5048 * You may obtain a copy of the License at
5049 *
5050 * http://www.apache.org/licenses/LICENSE-2.0
5051 *
5052 * Unless required by applicable law or agreed to in writing, software
5053 * distributed under the License is distributed on an "AS IS" BASIS,
5054 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
5055 * See the License for the specific language governing permissions and
5056 * limitations under the License.
5057 */
5058let nodeFromJSON$1;
5059let MAX_NODE$1;
5060function setNodeFromJSON(val) {
5061 nodeFromJSON$1 = val;
5062}
5063function setMaxNode(val) {
5064 MAX_NODE$1 = val;
5065}
5066class PriorityIndex extends Index {
5067 compare(a, b) {
5068 const aPriority = a.node.getPriority();
5069 const bPriority = b.node.getPriority();
5070 const indexCmp = aPriority.compareTo(bPriority);
5071 if (indexCmp === 0) {
5072 return nameCompare(a.name, b.name);
5073 }
5074 else {
5075 return indexCmp;
5076 }
5077 }
5078 isDefinedOn(node) {
5079 return !node.getPriority().isEmpty();
5080 }
5081 indexedValueChanged(oldNode, newNode) {
5082 return !oldNode.getPriority().equals(newNode.getPriority());
5083 }
5084 minPost() {
5085 // eslint-disable-next-line @typescript-eslint/no-explicit-any
5086 return NamedNode.MIN;
5087 }
5088 maxPost() {
5089 return new NamedNode(MAX_NAME, new LeafNode('[PRIORITY-POST]', MAX_NODE$1));
5090 }
5091 makePost(indexValue, name) {
5092 const priorityNode = nodeFromJSON$1(indexValue);
5093 return new NamedNode(name, new LeafNode('[PRIORITY-POST]', priorityNode));
5094 }
5095 /**
5096 * @returns String representation for inclusion in a query spec
5097 */
5098 toString() {
5099 return '.priority';
5100 }
5101}
5102const PRIORITY_INDEX = new PriorityIndex();
5103
5104/**
5105 * @license
5106 * Copyright 2017 Google LLC
5107 *
5108 * Licensed under the Apache License, Version 2.0 (the "License");
5109 * you may not use this file except in compliance with the License.
5110 * You may obtain a copy of the License at
5111 *
5112 * http://www.apache.org/licenses/LICENSE-2.0
5113 *
5114 * Unless required by applicable law or agreed to in writing, software
5115 * distributed under the License is distributed on an "AS IS" BASIS,
5116 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
5117 * See the License for the specific language governing permissions and
5118 * limitations under the License.
5119 */
5120const LOG_2 = Math.log(2);
5121class Base12Num {
5122 constructor(length) {
5123 const logBase2 = (num) =>
5124 // eslint-disable-next-line @typescript-eslint/no-explicit-any
5125 parseInt((Math.log(num) / LOG_2), 10);
5126 const bitMask = (bits) => parseInt(Array(bits + 1).join('1'), 2);
5127 this.count = logBase2(length + 1);
5128 this.current_ = this.count - 1;
5129 const mask = bitMask(this.count);
5130 this.bits_ = (length + 1) & mask;
5131 }
5132 nextBitIsOne() {
5133 //noinspection JSBitwiseOperatorUsage
5134 const result = !(this.bits_ & (0x1 << this.current_));
5135 this.current_--;
5136 return result;
5137 }
5138}
5139/**
5140 * Takes a list of child nodes and constructs a SortedSet using the given comparison
5141 * function
5142 *
5143 * Uses the algorithm described in the paper linked here:
5144 * http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.46.1458
5145 *
5146 * @param childList - Unsorted list of children
5147 * @param cmp - The comparison method to be used
5148 * @param keyFn - An optional function to extract K from a node wrapper, if K's
5149 * type is not NamedNode
5150 * @param mapSortFn - An optional override for comparator used by the generated sorted map
5151 */
5152const buildChildSet = function (childList, cmp, keyFn, mapSortFn) {
5153 childList.sort(cmp);
5154 const buildBalancedTree = function (low, high) {
5155 const length = high - low;
5156 let namedNode;
5157 let key;
5158 if (length === 0) {
5159 return null;
5160 }
5161 else if (length === 1) {
5162 namedNode = childList[low];
5163 key = keyFn ? keyFn(namedNode) : namedNode;
5164 return new LLRBNode(key, namedNode.node, LLRBNode.BLACK, null, null);
5165 }
5166 else {
5167 // eslint-disable-next-line @typescript-eslint/no-explicit-any
5168 const middle = parseInt((length / 2), 10) + low;
5169 const left = buildBalancedTree(low, middle);
5170 const right = buildBalancedTree(middle + 1, high);
5171 namedNode = childList[middle];
5172 key = keyFn ? keyFn(namedNode) : namedNode;
5173 return new LLRBNode(key, namedNode.node, LLRBNode.BLACK, left, right);
5174 }
5175 };
5176 const buildFrom12Array = function (base12) {
5177 let node = null;
5178 let root = null;
5179 let index = childList.length;
5180 const buildPennant = function (chunkSize, color) {
5181 const low = index - chunkSize;
5182 const high = index;
5183 index -= chunkSize;
5184 const childTree = buildBalancedTree(low + 1, high);
5185 const namedNode = childList[low];
5186 const key = keyFn ? keyFn(namedNode) : namedNode;
5187 attachPennant(new LLRBNode(key, namedNode.node, color, null, childTree));
5188 };
5189 const attachPennant = function (pennant) {
5190 if (node) {
5191 node.left = pennant;
5192 node = pennant;
5193 }
5194 else {
5195 root = pennant;
5196 node = pennant;
5197 }
5198 };
5199 for (let i = 0; i < base12.count; ++i) {
5200 const isOne = base12.nextBitIsOne();
5201 // The number of nodes taken in each slice is 2^(arr.length - (i + 1))
5202 const chunkSize = Math.pow(2, base12.count - (i + 1));
5203 if (isOne) {
5204 buildPennant(chunkSize, LLRBNode.BLACK);
5205 }
5206 else {
5207 // current == 2
5208 buildPennant(chunkSize, LLRBNode.BLACK);
5209 buildPennant(chunkSize, LLRBNode.RED);
5210 }
5211 }
5212 return root;
5213 };
5214 const base12 = new Base12Num(childList.length);
5215 const root = buildFrom12Array(base12);
5216 // eslint-disable-next-line @typescript-eslint/no-explicit-any
5217 return new SortedMap(mapSortFn || cmp, root);
5218};
5219
5220/**
5221 * @license
5222 * Copyright 2017 Google LLC
5223 *
5224 * Licensed under the Apache License, Version 2.0 (the "License");
5225 * you may not use this file except in compliance with the License.
5226 * You may obtain a copy of the License at
5227 *
5228 * http://www.apache.org/licenses/LICENSE-2.0
5229 *
5230 * Unless required by applicable law or agreed to in writing, software
5231 * distributed under the License is distributed on an "AS IS" BASIS,
5232 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
5233 * See the License for the specific language governing permissions and
5234 * limitations under the License.
5235 */
5236let _defaultIndexMap;
5237const fallbackObject = {};
5238class IndexMap {
5239 constructor(indexes_, indexSet_) {
5240 this.indexes_ = indexes_;
5241 this.indexSet_ = indexSet_;
5242 }
5243 /**
5244 * The default IndexMap for nodes without a priority
5245 */
5246 static get Default() {
5247 assert(fallbackObject && PRIORITY_INDEX, 'ChildrenNode.ts has not been loaded');
5248 _defaultIndexMap =
5249 _defaultIndexMap ||
5250 new IndexMap({ '.priority': fallbackObject }, { '.priority': PRIORITY_INDEX });
5251 return _defaultIndexMap;
5252 }
5253 get(indexKey) {
5254 const sortedMap = safeGet(this.indexes_, indexKey);
5255 if (!sortedMap) {
5256 throw new Error('No index defined for ' + indexKey);
5257 }
5258 if (sortedMap instanceof SortedMap) {
5259 return sortedMap;
5260 }
5261 else {
5262 // The index exists, but it falls back to just name comparison. Return null so that the calling code uses the
5263 // regular child map
5264 return null;
5265 }
5266 }
5267 hasIndex(indexDefinition) {
5268 return contains(this.indexSet_, indexDefinition.toString());
5269 }
5270 addIndex(indexDefinition, existingChildren) {
5271 assert(indexDefinition !== KEY_INDEX, "KeyIndex always exists and isn't meant to be added to the IndexMap.");
5272 const childList = [];
5273 let sawIndexedValue = false;
5274 const iter = existingChildren.getIterator(NamedNode.Wrap);
5275 let next = iter.getNext();
5276 while (next) {
5277 sawIndexedValue =
5278 sawIndexedValue || indexDefinition.isDefinedOn(next.node);
5279 childList.push(next);
5280 next = iter.getNext();
5281 }
5282 let newIndex;
5283 if (sawIndexedValue) {
5284 newIndex = buildChildSet(childList, indexDefinition.getCompare());
5285 }
5286 else {
5287 newIndex = fallbackObject;
5288 }
5289 const indexName = indexDefinition.toString();
5290 const newIndexSet = Object.assign({}, this.indexSet_);
5291 newIndexSet[indexName] = indexDefinition;
5292 const newIndexes = Object.assign({}, this.indexes_);
5293 newIndexes[indexName] = newIndex;
5294 return new IndexMap(newIndexes, newIndexSet);
5295 }
5296 /**
5297 * Ensure that this node is properly tracked in any indexes that we're maintaining
5298 */
5299 addToIndexes(namedNode, existingChildren) {
5300 const newIndexes = map(this.indexes_, (indexedChildren, indexName) => {
5301 const index = safeGet(this.indexSet_, indexName);
5302 assert(index, 'Missing index implementation for ' + indexName);
5303 if (indexedChildren === fallbackObject) {
5304 // Check to see if we need to index everything
5305 if (index.isDefinedOn(namedNode.node)) {
5306 // We need to build this index
5307 const childList = [];
5308 const iter = existingChildren.getIterator(NamedNode.Wrap);
5309 let next = iter.getNext();
5310 while (next) {
5311 if (next.name !== namedNode.name) {
5312 childList.push(next);
5313 }
5314 next = iter.getNext();
5315 }
5316 childList.push(namedNode);
5317 return buildChildSet(childList, index.getCompare());
5318 }
5319 else {
5320 // No change, this remains a fallback
5321 return fallbackObject;
5322 }
5323 }
5324 else {
5325 const existingSnap = existingChildren.get(namedNode.name);
5326 let newChildren = indexedChildren;
5327 if (existingSnap) {
5328 newChildren = newChildren.remove(new NamedNode(namedNode.name, existingSnap));
5329 }
5330 return newChildren.insert(namedNode, namedNode.node);
5331 }
5332 });
5333 return new IndexMap(newIndexes, this.indexSet_);
5334 }
5335 /**
5336 * Create a new IndexMap instance with the given value removed
5337 */
5338 removeFromIndexes(namedNode, existingChildren) {
5339 const newIndexes = map(this.indexes_, (indexedChildren) => {
5340 if (indexedChildren === fallbackObject) {
5341 // This is the fallback. Just return it, nothing to do in this case
5342 return indexedChildren;
5343 }
5344 else {
5345 const existingSnap = existingChildren.get(namedNode.name);
5346 if (existingSnap) {
5347 return indexedChildren.remove(new NamedNode(namedNode.name, existingSnap));
5348 }
5349 else {
5350 // No record of this child
5351 return indexedChildren;
5352 }
5353 }
5354 });
5355 return new IndexMap(newIndexes, this.indexSet_);
5356 }
5357}
5358
5359/**
5360 * @license
5361 * Copyright 2017 Google LLC
5362 *
5363 * Licensed under the Apache License, Version 2.0 (the "License");
5364 * you may not use this file except in compliance with the License.
5365 * You may obtain a copy of the License at
5366 *
5367 * http://www.apache.org/licenses/LICENSE-2.0
5368 *
5369 * Unless required by applicable law or agreed to in writing, software
5370 * distributed under the License is distributed on an "AS IS" BASIS,
5371 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
5372 * See the License for the specific language governing permissions and
5373 * limitations under the License.
5374 */
5375// TODO: For memory savings, don't store priorityNode_ if it's empty.
5376let EMPTY_NODE;
5377/**
5378 * ChildrenNode is a class for storing internal nodes in a DataSnapshot
5379 * (i.e. nodes with children). It implements Node and stores the
5380 * list of children in the children property, sorted by child name.
5381 */
5382class ChildrenNode {
5383 /**
5384 * @param children_ - List of children of this node..
5385 * @param priorityNode_ - The priority of this node (as a snapshot node).
5386 */
5387 constructor(children_, priorityNode_, indexMap_) {
5388 this.children_ = children_;
5389 this.priorityNode_ = priorityNode_;
5390 this.indexMap_ = indexMap_;
5391 this.lazyHash_ = null;
5392 /**
5393 * Note: The only reason we allow null priority is for EMPTY_NODE, since we can't use
5394 * EMPTY_NODE as the priority of EMPTY_NODE. We might want to consider making EMPTY_NODE its own
5395 * class instead of an empty ChildrenNode.
5396 */
5397 if (this.priorityNode_) {
5398 validatePriorityNode(this.priorityNode_);
5399 }
5400 if (this.children_.isEmpty()) {
5401 assert(!this.priorityNode_ || this.priorityNode_.isEmpty(), 'An empty node cannot have a priority');
5402 }
5403 }
5404 static get EMPTY_NODE() {
5405 return (EMPTY_NODE ||
5406 (EMPTY_NODE = new ChildrenNode(new SortedMap(NAME_COMPARATOR), null, IndexMap.Default)));
5407 }
5408 /** @inheritDoc */
5409 isLeafNode() {
5410 return false;
5411 }
5412 /** @inheritDoc */
5413 getPriority() {
5414 return this.priorityNode_ || EMPTY_NODE;
5415 }
5416 /** @inheritDoc */
5417 updatePriority(newPriorityNode) {
5418 if (this.children_.isEmpty()) {
5419 // Don't allow priorities on empty nodes
5420 return this;
5421 }
5422 else {
5423 return new ChildrenNode(this.children_, newPriorityNode, this.indexMap_);
5424 }
5425 }
5426 /** @inheritDoc */
5427 getImmediateChild(childName) {
5428 // Hack to treat priority as a regular child
5429 if (childName === '.priority') {
5430 return this.getPriority();
5431 }
5432 else {
5433 const child = this.children_.get(childName);
5434 return child === null ? EMPTY_NODE : child;
5435 }
5436 }
5437 /** @inheritDoc */
5438 getChild(path) {
5439 const front = pathGetFront(path);
5440 if (front === null) {
5441 return this;
5442 }
5443 return this.getImmediateChild(front).getChild(pathPopFront(path));
5444 }
5445 /** @inheritDoc */
5446 hasChild(childName) {
5447 return this.children_.get(childName) !== null;
5448 }
5449 /** @inheritDoc */
5450 updateImmediateChild(childName, newChildNode) {
5451 assert(newChildNode, 'We should always be passing snapshot nodes');
5452 if (childName === '.priority') {
5453 return this.updatePriority(newChildNode);
5454 }
5455 else {
5456 const namedNode = new NamedNode(childName, newChildNode);
5457 let newChildren, newIndexMap;
5458 if (newChildNode.isEmpty()) {
5459 newChildren = this.children_.remove(childName);
5460 newIndexMap = this.indexMap_.removeFromIndexes(namedNode, this.children_);
5461 }
5462 else {
5463 newChildren = this.children_.insert(childName, newChildNode);
5464 newIndexMap = this.indexMap_.addToIndexes(namedNode, this.children_);
5465 }
5466 const newPriority = newChildren.isEmpty()
5467 ? EMPTY_NODE
5468 : this.priorityNode_;
5469 return new ChildrenNode(newChildren, newPriority, newIndexMap);
5470 }
5471 }
5472 /** @inheritDoc */
5473 updateChild(path, newChildNode) {
5474 const front = pathGetFront(path);
5475 if (front === null) {
5476 return newChildNode;
5477 }
5478 else {
5479 assert(pathGetFront(path) !== '.priority' || pathGetLength(path) === 1, '.priority must be the last token in a path');
5480 const newImmediateChild = this.getImmediateChild(front).updateChild(pathPopFront(path), newChildNode);
5481 return this.updateImmediateChild(front, newImmediateChild);
5482 }
5483 }
5484 /** @inheritDoc */
5485 isEmpty() {
5486 return this.children_.isEmpty();
5487 }
5488 /** @inheritDoc */
5489 numChildren() {
5490 return this.children_.count();
5491 }
5492 /** @inheritDoc */
5493 val(exportFormat) {
5494 if (this.isEmpty()) {
5495 return null;
5496 }
5497 const obj = {};
5498 let numKeys = 0, maxKey = 0, allIntegerKeys = true;
5499 this.forEachChild(PRIORITY_INDEX, (key, childNode) => {
5500 obj[key] = childNode.val(exportFormat);
5501 numKeys++;
5502 if (allIntegerKeys && ChildrenNode.INTEGER_REGEXP_.test(key)) {
5503 maxKey = Math.max(maxKey, Number(key));
5504 }
5505 else {
5506 allIntegerKeys = false;
5507 }
5508 });
5509 if (!exportFormat && allIntegerKeys && maxKey < 2 * numKeys) {
5510 // convert to array.
5511 const array = [];
5512 // eslint-disable-next-line guard-for-in
5513 for (const key in obj) {
5514 array[key] = obj[key];
5515 }
5516 return array;
5517 }
5518 else {
5519 if (exportFormat && !this.getPriority().isEmpty()) {
5520 obj['.priority'] = this.getPriority().val();
5521 }
5522 return obj;
5523 }
5524 }
5525 /** @inheritDoc */
5526 hash() {
5527 if (this.lazyHash_ === null) {
5528 let toHash = '';
5529 if (!this.getPriority().isEmpty()) {
5530 toHash +=
5531 'priority:' +
5532 priorityHashText(this.getPriority().val()) +
5533 ':';
5534 }
5535 this.forEachChild(PRIORITY_INDEX, (key, childNode) => {
5536 const childHash = childNode.hash();
5537 if (childHash !== '') {
5538 toHash += ':' + key + ':' + childHash;
5539 }
5540 });
5541 this.lazyHash_ = toHash === '' ? '' : sha1(toHash);
5542 }
5543 return this.lazyHash_;
5544 }
5545 /** @inheritDoc */
5546 getPredecessorChildName(childName, childNode, index) {
5547 const idx = this.resolveIndex_(index);
5548 if (idx) {
5549 const predecessor = idx.getPredecessorKey(new NamedNode(childName, childNode));
5550 return predecessor ? predecessor.name : null;
5551 }
5552 else {
5553 return this.children_.getPredecessorKey(childName);
5554 }
5555 }
5556 getFirstChildName(indexDefinition) {
5557 const idx = this.resolveIndex_(indexDefinition);
5558 if (idx) {
5559 const minKey = idx.minKey();
5560 return minKey && minKey.name;
5561 }
5562 else {
5563 return this.children_.minKey();
5564 }
5565 }
5566 getFirstChild(indexDefinition) {
5567 const minKey = this.getFirstChildName(indexDefinition);
5568 if (minKey) {
5569 return new NamedNode(minKey, this.children_.get(minKey));
5570 }
5571 else {
5572 return null;
5573 }
5574 }
5575 /**
5576 * Given an index, return the key name of the largest value we have, according to that index
5577 */
5578 getLastChildName(indexDefinition) {
5579 const idx = this.resolveIndex_(indexDefinition);
5580 if (idx) {
5581 const maxKey = idx.maxKey();
5582 return maxKey && maxKey.name;
5583 }
5584 else {
5585 return this.children_.maxKey();
5586 }
5587 }
5588 getLastChild(indexDefinition) {
5589 const maxKey = this.getLastChildName(indexDefinition);
5590 if (maxKey) {
5591 return new NamedNode(maxKey, this.children_.get(maxKey));
5592 }
5593 else {
5594 return null;
5595 }
5596 }
5597 forEachChild(index, action) {
5598 const idx = this.resolveIndex_(index);
5599 if (idx) {
5600 return idx.inorderTraversal(wrappedNode => {
5601 return action(wrappedNode.name, wrappedNode.node);
5602 });
5603 }
5604 else {
5605 return this.children_.inorderTraversal(action);
5606 }
5607 }
5608 getIterator(indexDefinition) {
5609 return this.getIteratorFrom(indexDefinition.minPost(), indexDefinition);
5610 }
5611 getIteratorFrom(startPost, indexDefinition) {
5612 const idx = this.resolveIndex_(indexDefinition);
5613 if (idx) {
5614 return idx.getIteratorFrom(startPost, key => key);
5615 }
5616 else {
5617 const iterator = this.children_.getIteratorFrom(startPost.name, NamedNode.Wrap);
5618 let next = iterator.peek();
5619 while (next != null && indexDefinition.compare(next, startPost) < 0) {
5620 iterator.getNext();
5621 next = iterator.peek();
5622 }
5623 return iterator;
5624 }
5625 }
5626 getReverseIterator(indexDefinition) {
5627 return this.getReverseIteratorFrom(indexDefinition.maxPost(), indexDefinition);
5628 }
5629 getReverseIteratorFrom(endPost, indexDefinition) {
5630 const idx = this.resolveIndex_(indexDefinition);
5631 if (idx) {
5632 return idx.getReverseIteratorFrom(endPost, key => {
5633 return key;
5634 });
5635 }
5636 else {
5637 const iterator = this.children_.getReverseIteratorFrom(endPost.name, NamedNode.Wrap);
5638 let next = iterator.peek();
5639 while (next != null && indexDefinition.compare(next, endPost) > 0) {
5640 iterator.getNext();
5641 next = iterator.peek();
5642 }
5643 return iterator;
5644 }
5645 }
5646 compareTo(other) {
5647 if (this.isEmpty()) {
5648 if (other.isEmpty()) {
5649 return 0;
5650 }
5651 else {
5652 return -1;
5653 }
5654 }
5655 else if (other.isLeafNode() || other.isEmpty()) {
5656 return 1;
5657 }
5658 else if (other === MAX_NODE) {
5659 return -1;
5660 }
5661 else {
5662 // Must be another node with children.
5663 return 0;
5664 }
5665 }
5666 withIndex(indexDefinition) {
5667 if (indexDefinition === KEY_INDEX ||
5668 this.indexMap_.hasIndex(indexDefinition)) {
5669 return this;
5670 }
5671 else {
5672 const newIndexMap = this.indexMap_.addIndex(indexDefinition, this.children_);
5673 return new ChildrenNode(this.children_, this.priorityNode_, newIndexMap);
5674 }
5675 }
5676 isIndexed(index) {
5677 return index === KEY_INDEX || this.indexMap_.hasIndex(index);
5678 }
5679 equals(other) {
5680 if (other === this) {
5681 return true;
5682 }
5683 else if (other.isLeafNode()) {
5684 return false;
5685 }
5686 else {
5687 const otherChildrenNode = other;
5688 if (!this.getPriority().equals(otherChildrenNode.getPriority())) {
5689 return false;
5690 }
5691 else if (this.children_.count() === otherChildrenNode.children_.count()) {
5692 const thisIter = this.getIterator(PRIORITY_INDEX);
5693 const otherIter = otherChildrenNode.getIterator(PRIORITY_INDEX);
5694 let thisCurrent = thisIter.getNext();
5695 let otherCurrent = otherIter.getNext();
5696 while (thisCurrent && otherCurrent) {
5697 if (thisCurrent.name !== otherCurrent.name ||
5698 !thisCurrent.node.equals(otherCurrent.node)) {
5699 return false;
5700 }
5701 thisCurrent = thisIter.getNext();
5702 otherCurrent = otherIter.getNext();
5703 }
5704 return thisCurrent === null && otherCurrent === null;
5705 }
5706 else {
5707 return false;
5708 }
5709 }
5710 }
5711 /**
5712 * Returns a SortedMap ordered by index, or null if the default (by-key) ordering can be used
5713 * instead.
5714 *
5715 */
5716 resolveIndex_(indexDefinition) {
5717 if (indexDefinition === KEY_INDEX) {
5718 return null;
5719 }
5720 else {
5721 return this.indexMap_.get(indexDefinition.toString());
5722 }
5723 }
5724}
5725ChildrenNode.INTEGER_REGEXP_ = /^(0|[1-9]\d*)$/;
5726class MaxNode extends ChildrenNode {
5727 constructor() {
5728 super(new SortedMap(NAME_COMPARATOR), ChildrenNode.EMPTY_NODE, IndexMap.Default);
5729 }
5730 compareTo(other) {
5731 if (other === this) {
5732 return 0;
5733 }
5734 else {
5735 return 1;
5736 }
5737 }
5738 equals(other) {
5739 // Not that we every compare it, but MAX_NODE is only ever equal to itself
5740 return other === this;
5741 }
5742 getPriority() {
5743 return this;
5744 }
5745 getImmediateChild(childName) {
5746 return ChildrenNode.EMPTY_NODE;
5747 }
5748 isEmpty() {
5749 return false;
5750 }
5751}
5752/**
5753 * Marker that will sort higher than any other snapshot.
5754 */
5755const MAX_NODE = new MaxNode();
5756Object.defineProperties(NamedNode, {
5757 MIN: {
5758 value: new NamedNode(MIN_NAME, ChildrenNode.EMPTY_NODE)
5759 },
5760 MAX: {
5761 value: new NamedNode(MAX_NAME, MAX_NODE)
5762 }
5763});
5764/**
5765 * Reference Extensions
5766 */
5767KeyIndex.__EMPTY_NODE = ChildrenNode.EMPTY_NODE;
5768LeafNode.__childrenNodeConstructor = ChildrenNode;
5769setMaxNode$1(MAX_NODE);
5770setMaxNode(MAX_NODE);
5771
5772/**
5773 * @license
5774 * Copyright 2017 Google LLC
5775 *
5776 * Licensed under the Apache License, Version 2.0 (the "License");
5777 * you may not use this file except in compliance with the License.
5778 * You may obtain a copy of the License at
5779 *
5780 * http://www.apache.org/licenses/LICENSE-2.0
5781 *
5782 * Unless required by applicable law or agreed to in writing, software
5783 * distributed under the License is distributed on an "AS IS" BASIS,
5784 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
5785 * See the License for the specific language governing permissions and
5786 * limitations under the License.
5787 */
5788const USE_HINZE = true;
5789/**
5790 * Constructs a snapshot node representing the passed JSON and returns it.
5791 * @param json - JSON to create a node for.
5792 * @param priority - Optional priority to use. This will be ignored if the
5793 * passed JSON contains a .priority property.
5794 */
5795function nodeFromJSON(json, priority = null) {
5796 if (json === null) {
5797 return ChildrenNode.EMPTY_NODE;
5798 }
5799 if (typeof json === 'object' && '.priority' in json) {
5800 priority = json['.priority'];
5801 }
5802 assert(priority === null ||
5803 typeof priority === 'string' ||
5804 typeof priority === 'number' ||
5805 (typeof priority === 'object' && '.sv' in priority), 'Invalid priority type found: ' + typeof priority);
5806 if (typeof json === 'object' && '.value' in json && json['.value'] !== null) {
5807 json = json['.value'];
5808 }
5809 // Valid leaf nodes include non-objects or server-value wrapper objects
5810 if (typeof json !== 'object' || '.sv' in json) {
5811 const jsonLeaf = json;
5812 return new LeafNode(jsonLeaf, nodeFromJSON(priority));
5813 }
5814 if (!(json instanceof Array) && USE_HINZE) {
5815 const children = [];
5816 let childrenHavePriority = false;
5817 const hinzeJsonObj = json;
5818 each(hinzeJsonObj, (key, child) => {
5819 if (key.substring(0, 1) !== '.') {
5820 // Ignore metadata nodes
5821 const childNode = nodeFromJSON(child);
5822 if (!childNode.isEmpty()) {
5823 childrenHavePriority =
5824 childrenHavePriority || !childNode.getPriority().isEmpty();
5825 children.push(new NamedNode(key, childNode));
5826 }
5827 }
5828 });
5829 if (children.length === 0) {
5830 return ChildrenNode.EMPTY_NODE;
5831 }
5832 const childSet = buildChildSet(children, NAME_ONLY_COMPARATOR, namedNode => namedNode.name, NAME_COMPARATOR);
5833 if (childrenHavePriority) {
5834 const sortedChildSet = buildChildSet(children, PRIORITY_INDEX.getCompare());
5835 return new ChildrenNode(childSet, nodeFromJSON(priority), new IndexMap({ '.priority': sortedChildSet }, { '.priority': PRIORITY_INDEX }));
5836 }
5837 else {
5838 return new ChildrenNode(childSet, nodeFromJSON(priority), IndexMap.Default);
5839 }
5840 }
5841 else {
5842 let node = ChildrenNode.EMPTY_NODE;
5843 each(json, (key, childData) => {
5844 if (contains(json, key)) {
5845 if (key.substring(0, 1) !== '.') {
5846 // ignore metadata nodes.
5847 const childNode = nodeFromJSON(childData);
5848 if (childNode.isLeafNode() || !childNode.isEmpty()) {
5849 node = node.updateImmediateChild(key, childNode);
5850 }
5851 }
5852 }
5853 });
5854 return node.updatePriority(nodeFromJSON(priority));
5855 }
5856}
5857setNodeFromJSON(nodeFromJSON);
5858
5859/**
5860 * @license
5861 * Copyright 2017 Google LLC
5862 *
5863 * Licensed under the Apache License, Version 2.0 (the "License");
5864 * you may not use this file except in compliance with the License.
5865 * You may obtain a copy of the License at
5866 *
5867 * http://www.apache.org/licenses/LICENSE-2.0
5868 *
5869 * Unless required by applicable law or agreed to in writing, software
5870 * distributed under the License is distributed on an "AS IS" BASIS,
5871 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
5872 * See the License for the specific language governing permissions and
5873 * limitations under the License.
5874 */
5875class PathIndex extends Index {
5876 constructor(indexPath_) {
5877 super();
5878 this.indexPath_ = indexPath_;
5879 assert(!pathIsEmpty(indexPath_) && pathGetFront(indexPath_) !== '.priority', "Can't create PathIndex with empty path or .priority key");
5880 }
5881 extractChild(snap) {
5882 return snap.getChild(this.indexPath_);
5883 }
5884 isDefinedOn(node) {
5885 return !node.getChild(this.indexPath_).isEmpty();
5886 }
5887 compare(a, b) {
5888 const aChild = this.extractChild(a.node);
5889 const bChild = this.extractChild(b.node);
5890 const indexCmp = aChild.compareTo(bChild);
5891 if (indexCmp === 0) {
5892 return nameCompare(a.name, b.name);
5893 }
5894 else {
5895 return indexCmp;
5896 }
5897 }
5898 makePost(indexValue, name) {
5899 const valueNode = nodeFromJSON(indexValue);
5900 const node = ChildrenNode.EMPTY_NODE.updateChild(this.indexPath_, valueNode);
5901 return new NamedNode(name, node);
5902 }
5903 maxPost() {
5904 const node = ChildrenNode.EMPTY_NODE.updateChild(this.indexPath_, MAX_NODE);
5905 return new NamedNode(MAX_NAME, node);
5906 }
5907 toString() {
5908 return pathSlice(this.indexPath_, 0).join('/');
5909 }
5910}
5911
5912/**
5913 * @license
5914 * Copyright 2017 Google LLC
5915 *
5916 * Licensed under the Apache License, Version 2.0 (the "License");
5917 * you may not use this file except in compliance with the License.
5918 * You may obtain a copy of the License at
5919 *
5920 * http://www.apache.org/licenses/LICENSE-2.0
5921 *
5922 * Unless required by applicable law or agreed to in writing, software
5923 * distributed under the License is distributed on an "AS IS" BASIS,
5924 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
5925 * See the License for the specific language governing permissions and
5926 * limitations under the License.
5927 */
5928class ValueIndex extends Index {
5929 compare(a, b) {
5930 const indexCmp = a.node.compareTo(b.node);
5931 if (indexCmp === 0) {
5932 return nameCompare(a.name, b.name);
5933 }
5934 else {
5935 return indexCmp;
5936 }
5937 }
5938 isDefinedOn(node) {
5939 return true;
5940 }
5941 indexedValueChanged(oldNode, newNode) {
5942 return !oldNode.equals(newNode);
5943 }
5944 minPost() {
5945 // eslint-disable-next-line @typescript-eslint/no-explicit-any
5946 return NamedNode.MIN;
5947 }
5948 maxPost() {
5949 // eslint-disable-next-line @typescript-eslint/no-explicit-any
5950 return NamedNode.MAX;
5951 }
5952 makePost(indexValue, name) {
5953 const valueNode = nodeFromJSON(indexValue);
5954 return new NamedNode(name, valueNode);
5955 }
5956 /**
5957 * @returns String representation for inclusion in a query spec
5958 */
5959 toString() {
5960 return '.value';
5961 }
5962}
5963const VALUE_INDEX = new ValueIndex();
5964
5965/**
5966 * @license
5967 * Copyright 2017 Google LLC
5968 *
5969 * Licensed under the Apache License, Version 2.0 (the "License");
5970 * you may not use this file except in compliance with the License.
5971 * You may obtain a copy of the License at
5972 *
5973 * http://www.apache.org/licenses/LICENSE-2.0
5974 *
5975 * Unless required by applicable law or agreed to in writing, software
5976 * distributed under the License is distributed on an "AS IS" BASIS,
5977 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
5978 * See the License for the specific language governing permissions and
5979 * limitations under the License.
5980 */
5981// Modeled after base64 web-safe chars, but ordered by ASCII.
5982const PUSH_CHARS = '-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz';
5983const MIN_PUSH_CHAR = '-';
5984const MAX_PUSH_CHAR = 'z';
5985const MAX_KEY_LEN = 786;
5986/**
5987 * Fancy ID generator that creates 20-character string identifiers with the
5988 * following properties:
5989 *
5990 * 1. They're based on timestamp so that they sort *after* any existing ids.
5991 * 2. They contain 72-bits of random data after the timestamp so that IDs won't
5992 * collide with other clients' IDs.
5993 * 3. They sort *lexicographically* (so the timestamp is converted to characters
5994 * that will sort properly).
5995 * 4. They're monotonically increasing. Even if you generate more than one in
5996 * the same timestamp, the latter ones will sort after the former ones. We do
5997 * this by using the previous random bits but "incrementing" them by 1 (only
5998 * in the case of a timestamp collision).
5999 */
6000const nextPushId = (function () {
6001 // Timestamp of last push, used to prevent local collisions if you push twice
6002 // in one ms.
6003 let lastPushTime = 0;
6004 // We generate 72-bits of randomness which get turned into 12 characters and
6005 // appended to the timestamp to prevent collisions with other clients. We
6006 // store the last characters we generated because in the event of a collision,
6007 // we'll use those same characters except "incremented" by one.
6008 const lastRandChars = [];
6009 return function (now) {
6010 const duplicateTime = now === lastPushTime;
6011 lastPushTime = now;
6012 let i;
6013 const timeStampChars = new Array(8);
6014 for (i = 7; i >= 0; i--) {
6015 timeStampChars[i] = PUSH_CHARS.charAt(now % 64);
6016 // NOTE: Can't use << here because javascript will convert to int and lose
6017 // the upper bits.
6018 now = Math.floor(now / 64);
6019 }
6020 assert(now === 0, 'Cannot push at time == 0');
6021 let id = timeStampChars.join('');
6022 if (!duplicateTime) {
6023 for (i = 0; i < 12; i++) {
6024 lastRandChars[i] = Math.floor(Math.random() * 64);
6025 }
6026 }
6027 else {
6028 // If the timestamp hasn't changed since last push, use the same random
6029 // number, except incremented by 1.
6030 for (i = 11; i >= 0 && lastRandChars[i] === 63; i--) {
6031 lastRandChars[i] = 0;
6032 }
6033 lastRandChars[i]++;
6034 }
6035 for (i = 0; i < 12; i++) {
6036 id += PUSH_CHARS.charAt(lastRandChars[i]);
6037 }
6038 assert(id.length === 20, 'nextPushId: Length should be 20.');
6039 return id;
6040 };
6041})();
6042const successor = function (key) {
6043 if (key === '' + INTEGER_32_MAX) {
6044 // See https://firebase.google.com/docs/database/web/lists-of-data#data-order
6045 return MIN_PUSH_CHAR;
6046 }
6047 const keyAsInt = tryParseInt(key);
6048 if (keyAsInt != null) {
6049 return '' + (keyAsInt + 1);
6050 }
6051 const next = new Array(key.length);
6052 for (let i = 0; i < next.length; i++) {
6053 next[i] = key.charAt(i);
6054 }
6055 if (next.length < MAX_KEY_LEN) {
6056 next.push(MIN_PUSH_CHAR);
6057 return next.join('');
6058 }
6059 let i = next.length - 1;
6060 while (i >= 0 && next[i] === MAX_PUSH_CHAR) {
6061 i--;
6062 }
6063 // `successor` was called on the largest possible key, so return the
6064 // MAX_NAME, which sorts larger than all keys.
6065 if (i === -1) {
6066 return MAX_NAME;
6067 }
6068 const source = next[i];
6069 const sourcePlusOne = PUSH_CHARS.charAt(PUSH_CHARS.indexOf(source) + 1);
6070 next[i] = sourcePlusOne;
6071 return next.slice(0, i + 1).join('');
6072};
6073// `key` is assumed to be non-empty.
6074const predecessor = function (key) {
6075 if (key === '' + INTEGER_32_MIN) {
6076 return MIN_NAME;
6077 }
6078 const keyAsInt = tryParseInt(key);
6079 if (keyAsInt != null) {
6080 return '' + (keyAsInt - 1);
6081 }
6082 const next = new Array(key.length);
6083 for (let i = 0; i < next.length; i++) {
6084 next[i] = key.charAt(i);
6085 }
6086 // If `key` ends in `MIN_PUSH_CHAR`, the largest key lexicographically
6087 // smaller than `key`, is `key[0:key.length - 1]`. The next key smaller
6088 // than that, `predecessor(predecessor(key))`, is
6089 //
6090 // `key[0:key.length - 2] + (key[key.length - 1] - 1) + \
6091 // { MAX_PUSH_CHAR repeated MAX_KEY_LEN - (key.length - 1) times }
6092 //
6093 // analogous to increment/decrement for base-10 integers.
6094 //
6095 // This works because lexigographic comparison works character-by-character,
6096 // using length as a tie-breaker if one key is a prefix of the other.
6097 if (next[next.length - 1] === MIN_PUSH_CHAR) {
6098 if (next.length === 1) {
6099 // See https://firebase.google.com/docs/database/web/lists-of-data#orderbykey
6100 return '' + INTEGER_32_MAX;
6101 }
6102 delete next[next.length - 1];
6103 return next.join('');
6104 }
6105 // Replace the last character with it's immediate predecessor, and
6106 // fill the suffix of the key with MAX_PUSH_CHAR. This is the
6107 // lexicographically largest possible key smaller than `key`.
6108 next[next.length - 1] = PUSH_CHARS.charAt(PUSH_CHARS.indexOf(next[next.length - 1]) - 1);
6109 return next.join('') + MAX_PUSH_CHAR.repeat(MAX_KEY_LEN - next.length);
6110};
6111
6112/**
6113 * @license
6114 * Copyright 2017 Google LLC
6115 *
6116 * Licensed under the Apache License, Version 2.0 (the "License");
6117 * you may not use this file except in compliance with the License.
6118 * You may obtain a copy of the License at
6119 *
6120 * http://www.apache.org/licenses/LICENSE-2.0
6121 *
6122 * Unless required by applicable law or agreed to in writing, software
6123 * distributed under the License is distributed on an "AS IS" BASIS,
6124 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
6125 * See the License for the specific language governing permissions and
6126 * limitations under the License.
6127 */
6128function changeValue(snapshotNode) {
6129 return { type: "value" /* VALUE */, snapshotNode };
6130}
6131function changeChildAdded(childName, snapshotNode) {
6132 return { type: "child_added" /* CHILD_ADDED */, snapshotNode, childName };
6133}
6134function changeChildRemoved(childName, snapshotNode) {
6135 return { type: "child_removed" /* CHILD_REMOVED */, snapshotNode, childName };
6136}
6137function changeChildChanged(childName, snapshotNode, oldSnap) {
6138 return {
6139 type: "child_changed" /* CHILD_CHANGED */,
6140 snapshotNode,
6141 childName,
6142 oldSnap
6143 };
6144}
6145function changeChildMoved(childName, snapshotNode) {
6146 return { type: "child_moved" /* CHILD_MOVED */, snapshotNode, childName };
6147}
6148
6149/**
6150 * @license
6151 * Copyright 2017 Google LLC
6152 *
6153 * Licensed under the Apache License, Version 2.0 (the "License");
6154 * you may not use this file except in compliance with the License.
6155 * You may obtain a copy of the License at
6156 *
6157 * http://www.apache.org/licenses/LICENSE-2.0
6158 *
6159 * Unless required by applicable law or agreed to in writing, software
6160 * distributed under the License is distributed on an "AS IS" BASIS,
6161 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
6162 * See the License for the specific language governing permissions and
6163 * limitations under the License.
6164 */
6165/**
6166 * Doesn't really filter nodes but applies an index to the node and keeps track of any changes
6167 */
6168class IndexedFilter {
6169 constructor(index_) {
6170 this.index_ = index_;
6171 }
6172 updateChild(snap, key, newChild, affectedPath, source, optChangeAccumulator) {
6173 assert(snap.isIndexed(this.index_), 'A node must be indexed if only a child is updated');
6174 const oldChild = snap.getImmediateChild(key);
6175 // Check if anything actually changed.
6176 if (oldChild.getChild(affectedPath).equals(newChild.getChild(affectedPath))) {
6177 // There's an edge case where a child can enter or leave the view because affectedPath was set to null.
6178 // In this case, affectedPath will appear null in both the old and new snapshots. So we need
6179 // to avoid treating these cases as "nothing changed."
6180 if (oldChild.isEmpty() === newChild.isEmpty()) {
6181 // Nothing changed.
6182 // This assert should be valid, but it's expensive (can dominate perf testing) so don't actually do it.
6183 //assert(oldChild.equals(newChild), 'Old and new snapshots should be equal.');
6184 return snap;
6185 }
6186 }
6187 if (optChangeAccumulator != null) {
6188 if (newChild.isEmpty()) {
6189 if (snap.hasChild(key)) {
6190 optChangeAccumulator.trackChildChange(changeChildRemoved(key, oldChild));
6191 }
6192 else {
6193 assert(snap.isLeafNode(), 'A child remove without an old child only makes sense on a leaf node');
6194 }
6195 }
6196 else if (oldChild.isEmpty()) {
6197 optChangeAccumulator.trackChildChange(changeChildAdded(key, newChild));
6198 }
6199 else {
6200 optChangeAccumulator.trackChildChange(changeChildChanged(key, newChild, oldChild));
6201 }
6202 }
6203 if (snap.isLeafNode() && newChild.isEmpty()) {
6204 return snap;
6205 }
6206 else {
6207 // Make sure the node is indexed
6208 return snap.updateImmediateChild(key, newChild).withIndex(this.index_);
6209 }
6210 }
6211 updateFullNode(oldSnap, newSnap, optChangeAccumulator) {
6212 if (optChangeAccumulator != null) {
6213 if (!oldSnap.isLeafNode()) {
6214 oldSnap.forEachChild(PRIORITY_INDEX, (key, childNode) => {
6215 if (!newSnap.hasChild(key)) {
6216 optChangeAccumulator.trackChildChange(changeChildRemoved(key, childNode));
6217 }
6218 });
6219 }
6220 if (!newSnap.isLeafNode()) {
6221 newSnap.forEachChild(PRIORITY_INDEX, (key, childNode) => {
6222 if (oldSnap.hasChild(key)) {
6223 const oldChild = oldSnap.getImmediateChild(key);
6224 if (!oldChild.equals(childNode)) {
6225 optChangeAccumulator.trackChildChange(changeChildChanged(key, childNode, oldChild));
6226 }
6227 }
6228 else {
6229 optChangeAccumulator.trackChildChange(changeChildAdded(key, childNode));
6230 }
6231 });
6232 }
6233 }
6234 return newSnap.withIndex(this.index_);
6235 }
6236 updatePriority(oldSnap, newPriority) {
6237 if (oldSnap.isEmpty()) {
6238 return ChildrenNode.EMPTY_NODE;
6239 }
6240 else {
6241 return oldSnap.updatePriority(newPriority);
6242 }
6243 }
6244 filtersNodes() {
6245 return false;
6246 }
6247 getIndexedFilter() {
6248 return this;
6249 }
6250 getIndex() {
6251 return this.index_;
6252 }
6253}
6254
6255/**
6256 * @license
6257 * Copyright 2017 Google LLC
6258 *
6259 * Licensed under the Apache License, Version 2.0 (the "License");
6260 * you may not use this file except in compliance with the License.
6261 * You may obtain a copy of the License at
6262 *
6263 * http://www.apache.org/licenses/LICENSE-2.0
6264 *
6265 * Unless required by applicable law or agreed to in writing, software
6266 * distributed under the License is distributed on an "AS IS" BASIS,
6267 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
6268 * See the License for the specific language governing permissions and
6269 * limitations under the License.
6270 */
6271/**
6272 * Filters nodes by range and uses an IndexFilter to track any changes after filtering the node
6273 */
6274class RangedFilter {
6275 constructor(params) {
6276 this.indexedFilter_ = new IndexedFilter(params.getIndex());
6277 this.index_ = params.getIndex();
6278 this.startPost_ = RangedFilter.getStartPost_(params);
6279 this.endPost_ = RangedFilter.getEndPost_(params);
6280 }
6281 getStartPost() {
6282 return this.startPost_;
6283 }
6284 getEndPost() {
6285 return this.endPost_;
6286 }
6287 matches(node) {
6288 return (this.index_.compare(this.getStartPost(), node) <= 0 &&
6289 this.index_.compare(node, this.getEndPost()) <= 0);
6290 }
6291 updateChild(snap, key, newChild, affectedPath, source, optChangeAccumulator) {
6292 if (!this.matches(new NamedNode(key, newChild))) {
6293 newChild = ChildrenNode.EMPTY_NODE;
6294 }
6295 return this.indexedFilter_.updateChild(snap, key, newChild, affectedPath, source, optChangeAccumulator);
6296 }
6297 updateFullNode(oldSnap, newSnap, optChangeAccumulator) {
6298 if (newSnap.isLeafNode()) {
6299 // Make sure we have a children node with the correct index, not a leaf node;
6300 newSnap = ChildrenNode.EMPTY_NODE;
6301 }
6302 let filtered = newSnap.withIndex(this.index_);
6303 // Don't support priorities on queries
6304 filtered = filtered.updatePriority(ChildrenNode.EMPTY_NODE);
6305 const self = this;
6306 newSnap.forEachChild(PRIORITY_INDEX, (key, childNode) => {
6307 if (!self.matches(new NamedNode(key, childNode))) {
6308 filtered = filtered.updateImmediateChild(key, ChildrenNode.EMPTY_NODE);
6309 }
6310 });
6311 return this.indexedFilter_.updateFullNode(oldSnap, filtered, optChangeAccumulator);
6312 }
6313 updatePriority(oldSnap, newPriority) {
6314 // Don't support priorities on queries
6315 return oldSnap;
6316 }
6317 filtersNodes() {
6318 return true;
6319 }
6320 getIndexedFilter() {
6321 return this.indexedFilter_;
6322 }
6323 getIndex() {
6324 return this.index_;
6325 }
6326 static getStartPost_(params) {
6327 if (params.hasStart()) {
6328 const startName = params.getIndexStartName();
6329 return params.getIndex().makePost(params.getIndexStartValue(), startName);
6330 }
6331 else {
6332 return params.getIndex().minPost();
6333 }
6334 }
6335 static getEndPost_(params) {
6336 if (params.hasEnd()) {
6337 const endName = params.getIndexEndName();
6338 return params.getIndex().makePost(params.getIndexEndValue(), endName);
6339 }
6340 else {
6341 return params.getIndex().maxPost();
6342 }
6343 }
6344}
6345
6346/**
6347 * @license
6348 * Copyright 2017 Google LLC
6349 *
6350 * Licensed under the Apache License, Version 2.0 (the "License");
6351 * you may not use this file except in compliance with the License.
6352 * You may obtain a copy of the License at
6353 *
6354 * http://www.apache.org/licenses/LICENSE-2.0
6355 *
6356 * Unless required by applicable law or agreed to in writing, software
6357 * distributed under the License is distributed on an "AS IS" BASIS,
6358 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
6359 * See the License for the specific language governing permissions and
6360 * limitations under the License.
6361 */
6362/**
6363 * Applies a limit and a range to a node and uses RangedFilter to do the heavy lifting where possible
6364 */
6365class LimitedFilter {
6366 constructor(params) {
6367 this.rangedFilter_ = new RangedFilter(params);
6368 this.index_ = params.getIndex();
6369 this.limit_ = params.getLimit();
6370 this.reverse_ = !params.isViewFromLeft();
6371 }
6372 updateChild(snap, key, newChild, affectedPath, source, optChangeAccumulator) {
6373 if (!this.rangedFilter_.matches(new NamedNode(key, newChild))) {
6374 newChild = ChildrenNode.EMPTY_NODE;
6375 }
6376 if (snap.getImmediateChild(key).equals(newChild)) {
6377 // No change
6378 return snap;
6379 }
6380 else if (snap.numChildren() < this.limit_) {
6381 return this.rangedFilter_
6382 .getIndexedFilter()
6383 .updateChild(snap, key, newChild, affectedPath, source, optChangeAccumulator);
6384 }
6385 else {
6386 return this.fullLimitUpdateChild_(snap, key, newChild, source, optChangeAccumulator);
6387 }
6388 }
6389 updateFullNode(oldSnap, newSnap, optChangeAccumulator) {
6390 let filtered;
6391 if (newSnap.isLeafNode() || newSnap.isEmpty()) {
6392 // Make sure we have a children node with the correct index, not a leaf node;
6393 filtered = ChildrenNode.EMPTY_NODE.withIndex(this.index_);
6394 }
6395 else {
6396 if (this.limit_ * 2 < newSnap.numChildren() &&
6397 newSnap.isIndexed(this.index_)) {
6398 // Easier to build up a snapshot, since what we're given has more than twice the elements we want
6399 filtered = ChildrenNode.EMPTY_NODE.withIndex(this.index_);
6400 // anchor to the startPost, endPost, or last element as appropriate
6401 let iterator;
6402 if (this.reverse_) {
6403 iterator = newSnap.getReverseIteratorFrom(this.rangedFilter_.getEndPost(), this.index_);
6404 }
6405 else {
6406 iterator = newSnap.getIteratorFrom(this.rangedFilter_.getStartPost(), this.index_);
6407 }
6408 let count = 0;
6409 while (iterator.hasNext() && count < this.limit_) {
6410 const next = iterator.getNext();
6411 let inRange;
6412 if (this.reverse_) {
6413 inRange =
6414 this.index_.compare(this.rangedFilter_.getStartPost(), next) <= 0;
6415 }
6416 else {
6417 inRange =
6418 this.index_.compare(next, this.rangedFilter_.getEndPost()) <= 0;
6419 }
6420 if (inRange) {
6421 filtered = filtered.updateImmediateChild(next.name, next.node);
6422 count++;
6423 }
6424 else {
6425 // if we have reached the end post, we cannot keep adding elemments
6426 break;
6427 }
6428 }
6429 }
6430 else {
6431 // The snap contains less than twice the limit. Faster to delete from the snap than build up a new one
6432 filtered = newSnap.withIndex(this.index_);
6433 // Don't support priorities on queries
6434 filtered = filtered.updatePriority(ChildrenNode.EMPTY_NODE);
6435 let startPost;
6436 let endPost;
6437 let cmp;
6438 let iterator;
6439 if (this.reverse_) {
6440 iterator = filtered.getReverseIterator(this.index_);
6441 startPost = this.rangedFilter_.getEndPost();
6442 endPost = this.rangedFilter_.getStartPost();
6443 const indexCompare = this.index_.getCompare();
6444 cmp = (a, b) => indexCompare(b, a);
6445 }
6446 else {
6447 iterator = filtered.getIterator(this.index_);
6448 startPost = this.rangedFilter_.getStartPost();
6449 endPost = this.rangedFilter_.getEndPost();
6450 cmp = this.index_.getCompare();
6451 }
6452 let count = 0;
6453 let foundStartPost = false;
6454 while (iterator.hasNext()) {
6455 const next = iterator.getNext();
6456 if (!foundStartPost && cmp(startPost, next) <= 0) {
6457 // start adding
6458 foundStartPost = true;
6459 }
6460 const inRange = foundStartPost && count < this.limit_ && cmp(next, endPost) <= 0;
6461 if (inRange) {
6462 count++;
6463 }
6464 else {
6465 filtered = filtered.updateImmediateChild(next.name, ChildrenNode.EMPTY_NODE);
6466 }
6467 }
6468 }
6469 }
6470 return this.rangedFilter_
6471 .getIndexedFilter()
6472 .updateFullNode(oldSnap, filtered, optChangeAccumulator);
6473 }
6474 updatePriority(oldSnap, newPriority) {
6475 // Don't support priorities on queries
6476 return oldSnap;
6477 }
6478 filtersNodes() {
6479 return true;
6480 }
6481 getIndexedFilter() {
6482 return this.rangedFilter_.getIndexedFilter();
6483 }
6484 getIndex() {
6485 return this.index_;
6486 }
6487 fullLimitUpdateChild_(snap, childKey, childSnap, source, changeAccumulator) {
6488 // TODO: rename all cache stuff etc to general snap terminology
6489 let cmp;
6490 if (this.reverse_) {
6491 const indexCmp = this.index_.getCompare();
6492 cmp = (a, b) => indexCmp(b, a);
6493 }
6494 else {
6495 cmp = this.index_.getCompare();
6496 }
6497 const oldEventCache = snap;
6498 assert(oldEventCache.numChildren() === this.limit_, '');
6499 const newChildNamedNode = new NamedNode(childKey, childSnap);
6500 const windowBoundary = this.reverse_
6501 ? oldEventCache.getFirstChild(this.index_)
6502 : oldEventCache.getLastChild(this.index_);
6503 const inRange = this.rangedFilter_.matches(newChildNamedNode);
6504 if (oldEventCache.hasChild(childKey)) {
6505 const oldChildSnap = oldEventCache.getImmediateChild(childKey);
6506 let nextChild = source.getChildAfterChild(this.index_, windowBoundary, this.reverse_);
6507 while (nextChild != null &&
6508 (nextChild.name === childKey || oldEventCache.hasChild(nextChild.name))) {
6509 // There is a weird edge case where a node is updated as part of a merge in the write tree, but hasn't
6510 // been applied to the limited filter yet. Ignore this next child which will be updated later in
6511 // the limited filter...
6512 nextChild = source.getChildAfterChild(this.index_, nextChild, this.reverse_);
6513 }
6514 const compareNext = nextChild == null ? 1 : cmp(nextChild, newChildNamedNode);
6515 const remainsInWindow = inRange && !childSnap.isEmpty() && compareNext >= 0;
6516 if (remainsInWindow) {
6517 if (changeAccumulator != null) {
6518 changeAccumulator.trackChildChange(changeChildChanged(childKey, childSnap, oldChildSnap));
6519 }
6520 return oldEventCache.updateImmediateChild(childKey, childSnap);
6521 }
6522 else {
6523 if (changeAccumulator != null) {
6524 changeAccumulator.trackChildChange(changeChildRemoved(childKey, oldChildSnap));
6525 }
6526 const newEventCache = oldEventCache.updateImmediateChild(childKey, ChildrenNode.EMPTY_NODE);
6527 const nextChildInRange = nextChild != null && this.rangedFilter_.matches(nextChild);
6528 if (nextChildInRange) {
6529 if (changeAccumulator != null) {
6530 changeAccumulator.trackChildChange(changeChildAdded(nextChild.name, nextChild.node));
6531 }
6532 return newEventCache.updateImmediateChild(nextChild.name, nextChild.node);
6533 }
6534 else {
6535 return newEventCache;
6536 }
6537 }
6538 }
6539 else if (childSnap.isEmpty()) {
6540 // we're deleting a node, but it was not in the window, so ignore it
6541 return snap;
6542 }
6543 else if (inRange) {
6544 if (cmp(windowBoundary, newChildNamedNode) >= 0) {
6545 if (changeAccumulator != null) {
6546 changeAccumulator.trackChildChange(changeChildRemoved(windowBoundary.name, windowBoundary.node));
6547 changeAccumulator.trackChildChange(changeChildAdded(childKey, childSnap));
6548 }
6549 return oldEventCache
6550 .updateImmediateChild(childKey, childSnap)
6551 .updateImmediateChild(windowBoundary.name, ChildrenNode.EMPTY_NODE);
6552 }
6553 else {
6554 return snap;
6555 }
6556 }
6557 else {
6558 return snap;
6559 }
6560 }
6561}
6562
6563/**
6564 * @license
6565 * Copyright 2017 Google LLC
6566 *
6567 * Licensed under the Apache License, Version 2.0 (the "License");
6568 * you may not use this file except in compliance with the License.
6569 * You may obtain a copy of the License at
6570 *
6571 * http://www.apache.org/licenses/LICENSE-2.0
6572 *
6573 * Unless required by applicable law or agreed to in writing, software
6574 * distributed under the License is distributed on an "AS IS" BASIS,
6575 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
6576 * See the License for the specific language governing permissions and
6577 * limitations under the License.
6578 */
6579/**
6580 * This class is an immutable-from-the-public-api struct containing a set of query parameters defining a
6581 * range to be returned for a particular location. It is assumed that validation of parameters is done at the
6582 * user-facing API level, so it is not done here.
6583 *
6584 * @internal
6585 */
6586class QueryParams {
6587 constructor() {
6588 this.limitSet_ = false;
6589 this.startSet_ = false;
6590 this.startNameSet_ = false;
6591 this.startAfterSet_ = false;
6592 this.endSet_ = false;
6593 this.endNameSet_ = false;
6594 this.endBeforeSet_ = false;
6595 this.limit_ = 0;
6596 this.viewFrom_ = '';
6597 this.indexStartValue_ = null;
6598 this.indexStartName_ = '';
6599 this.indexEndValue_ = null;
6600 this.indexEndName_ = '';
6601 this.index_ = PRIORITY_INDEX;
6602 }
6603 hasStart() {
6604 return this.startSet_;
6605 }
6606 hasStartAfter() {
6607 return this.startAfterSet_;
6608 }
6609 hasEndBefore() {
6610 return this.endBeforeSet_;
6611 }
6612 /**
6613 * @returns True if it would return from left.
6614 */
6615 isViewFromLeft() {
6616 if (this.viewFrom_ === '') {
6617 // limit(), rather than limitToFirst or limitToLast was called.
6618 // This means that only one of startSet_ and endSet_ is true. Use them
6619 // to calculate which side of the view to anchor to. If neither is set,
6620 // anchor to the end.
6621 return this.startSet_;
6622 }
6623 else {
6624 return this.viewFrom_ === "l" /* VIEW_FROM_LEFT */;
6625 }
6626 }
6627 /**
6628 * Only valid to call if hasStart() returns true
6629 */
6630 getIndexStartValue() {
6631 assert(this.startSet_, 'Only valid if start has been set');
6632 return this.indexStartValue_;
6633 }
6634 /**
6635 * Only valid to call if hasStart() returns true.
6636 * Returns the starting key name for the range defined by these query parameters
6637 */
6638 getIndexStartName() {
6639 assert(this.startSet_, 'Only valid if start has been set');
6640 if (this.startNameSet_) {
6641 return this.indexStartName_;
6642 }
6643 else {
6644 return MIN_NAME;
6645 }
6646 }
6647 hasEnd() {
6648 return this.endSet_;
6649 }
6650 /**
6651 * Only valid to call if hasEnd() returns true.
6652 */
6653 getIndexEndValue() {
6654 assert(this.endSet_, 'Only valid if end has been set');
6655 return this.indexEndValue_;
6656 }
6657 /**
6658 * Only valid to call if hasEnd() returns true.
6659 * Returns the end key name for the range defined by these query parameters
6660 */
6661 getIndexEndName() {
6662 assert(this.endSet_, 'Only valid if end has been set');
6663 if (this.endNameSet_) {
6664 return this.indexEndName_;
6665 }
6666 else {
6667 return MAX_NAME;
6668 }
6669 }
6670 hasLimit() {
6671 return this.limitSet_;
6672 }
6673 /**
6674 * @returns True if a limit has been set and it has been explicitly anchored
6675 */
6676 hasAnchoredLimit() {
6677 return this.limitSet_ && this.viewFrom_ !== '';
6678 }
6679 /**
6680 * Only valid to call if hasLimit() returns true
6681 */
6682 getLimit() {
6683 assert(this.limitSet_, 'Only valid if limit has been set');
6684 return this.limit_;
6685 }
6686 getIndex() {
6687 return this.index_;
6688 }
6689 loadsAllData() {
6690 return !(this.startSet_ || this.endSet_ || this.limitSet_);
6691 }
6692 isDefault() {
6693 return this.loadsAllData() && this.index_ === PRIORITY_INDEX;
6694 }
6695 copy() {
6696 const copy = new QueryParams();
6697 copy.limitSet_ = this.limitSet_;
6698 copy.limit_ = this.limit_;
6699 copy.startSet_ = this.startSet_;
6700 copy.indexStartValue_ = this.indexStartValue_;
6701 copy.startNameSet_ = this.startNameSet_;
6702 copy.indexStartName_ = this.indexStartName_;
6703 copy.endSet_ = this.endSet_;
6704 copy.indexEndValue_ = this.indexEndValue_;
6705 copy.endNameSet_ = this.endNameSet_;
6706 copy.indexEndName_ = this.indexEndName_;
6707 copy.index_ = this.index_;
6708 copy.viewFrom_ = this.viewFrom_;
6709 return copy;
6710 }
6711}
6712function queryParamsGetNodeFilter(queryParams) {
6713 if (queryParams.loadsAllData()) {
6714 return new IndexedFilter(queryParams.getIndex());
6715 }
6716 else if (queryParams.hasLimit()) {
6717 return new LimitedFilter(queryParams);
6718 }
6719 else {
6720 return new RangedFilter(queryParams);
6721 }
6722}
6723function queryParamsLimitToFirst(queryParams, newLimit) {
6724 const newParams = queryParams.copy();
6725 newParams.limitSet_ = true;
6726 newParams.limit_ = newLimit;
6727 newParams.viewFrom_ = "l" /* VIEW_FROM_LEFT */;
6728 return newParams;
6729}
6730function queryParamsLimitToLast(queryParams, newLimit) {
6731 const newParams = queryParams.copy();
6732 newParams.limitSet_ = true;
6733 newParams.limit_ = newLimit;
6734 newParams.viewFrom_ = "r" /* VIEW_FROM_RIGHT */;
6735 return newParams;
6736}
6737function queryParamsStartAt(queryParams, indexValue, key) {
6738 const newParams = queryParams.copy();
6739 newParams.startSet_ = true;
6740 if (indexValue === undefined) {
6741 indexValue = null;
6742 }
6743 newParams.indexStartValue_ = indexValue;
6744 if (key != null) {
6745 newParams.startNameSet_ = true;
6746 newParams.indexStartName_ = key;
6747 }
6748 else {
6749 newParams.startNameSet_ = false;
6750 newParams.indexStartName_ = '';
6751 }
6752 return newParams;
6753}
6754function queryParamsStartAfter(queryParams, indexValue, key) {
6755 let params;
6756 if (queryParams.index_ === KEY_INDEX) {
6757 if (typeof indexValue === 'string') {
6758 indexValue = successor(indexValue);
6759 }
6760 params = queryParamsStartAt(queryParams, indexValue, key);
6761 }
6762 else {
6763 let childKey;
6764 if (key == null) {
6765 childKey = MAX_NAME;
6766 }
6767 else {
6768 childKey = successor(key);
6769 }
6770 params = queryParamsStartAt(queryParams, indexValue, childKey);
6771 }
6772 params.startAfterSet_ = true;
6773 return params;
6774}
6775function queryParamsEndAt(queryParams, indexValue, key) {
6776 const newParams = queryParams.copy();
6777 newParams.endSet_ = true;
6778 if (indexValue === undefined) {
6779 indexValue = null;
6780 }
6781 newParams.indexEndValue_ = indexValue;
6782 if (key !== undefined) {
6783 newParams.endNameSet_ = true;
6784 newParams.indexEndName_ = key;
6785 }
6786 else {
6787 newParams.endNameSet_ = false;
6788 newParams.indexEndName_ = '';
6789 }
6790 return newParams;
6791}
6792function queryParamsEndBefore(queryParams, indexValue, key) {
6793 let childKey;
6794 let params;
6795 if (queryParams.index_ === KEY_INDEX) {
6796 if (typeof indexValue === 'string') {
6797 indexValue = predecessor(indexValue);
6798 }
6799 params = queryParamsEndAt(queryParams, indexValue, key);
6800 }
6801 else {
6802 if (key == null) {
6803 childKey = MIN_NAME;
6804 }
6805 else {
6806 childKey = predecessor(key);
6807 }
6808 params = queryParamsEndAt(queryParams, indexValue, childKey);
6809 }
6810 params.endBeforeSet_ = true;
6811 return params;
6812}
6813function queryParamsOrderBy(queryParams, index) {
6814 const newParams = queryParams.copy();
6815 newParams.index_ = index;
6816 return newParams;
6817}
6818/**
6819 * Returns a set of REST query string parameters representing this query.
6820 *
6821 * @returns query string parameters
6822 */
6823function queryParamsToRestQueryStringParameters(queryParams) {
6824 const qs = {};
6825 if (queryParams.isDefault()) {
6826 return qs;
6827 }
6828 let orderBy;
6829 if (queryParams.index_ === PRIORITY_INDEX) {
6830 orderBy = "$priority" /* PRIORITY_INDEX */;
6831 }
6832 else if (queryParams.index_ === VALUE_INDEX) {
6833 orderBy = "$value" /* VALUE_INDEX */;
6834 }
6835 else if (queryParams.index_ === KEY_INDEX) {
6836 orderBy = "$key" /* KEY_INDEX */;
6837 }
6838 else {
6839 assert(queryParams.index_ instanceof PathIndex, 'Unrecognized index type!');
6840 orderBy = queryParams.index_.toString();
6841 }
6842 qs["orderBy" /* ORDER_BY */] = stringify(orderBy);
6843 if (queryParams.startSet_) {
6844 qs["startAt" /* START_AT */] = stringify(queryParams.indexStartValue_);
6845 if (queryParams.startNameSet_) {
6846 qs["startAt" /* START_AT */] +=
6847 ',' + stringify(queryParams.indexStartName_);
6848 }
6849 }
6850 if (queryParams.endSet_) {
6851 qs["endAt" /* END_AT */] = stringify(queryParams.indexEndValue_);
6852 if (queryParams.endNameSet_) {
6853 qs["endAt" /* END_AT */] +=
6854 ',' + stringify(queryParams.indexEndName_);
6855 }
6856 }
6857 if (queryParams.limitSet_) {
6858 if (queryParams.isViewFromLeft()) {
6859 qs["limitToFirst" /* LIMIT_TO_FIRST */] = queryParams.limit_;
6860 }
6861 else {
6862 qs["limitToLast" /* LIMIT_TO_LAST */] = queryParams.limit_;
6863 }
6864 }
6865 return qs;
6866}
6867function queryParamsGetQueryObject(queryParams) {
6868 const obj = {};
6869 if (queryParams.startSet_) {
6870 obj["sp" /* INDEX_START_VALUE */] =
6871 queryParams.indexStartValue_;
6872 if (queryParams.startNameSet_) {
6873 obj["sn" /* INDEX_START_NAME */] =
6874 queryParams.indexStartName_;
6875 }
6876 }
6877 if (queryParams.endSet_) {
6878 obj["ep" /* INDEX_END_VALUE */] = queryParams.indexEndValue_;
6879 if (queryParams.endNameSet_) {
6880 obj["en" /* INDEX_END_NAME */] = queryParams.indexEndName_;
6881 }
6882 }
6883 if (queryParams.limitSet_) {
6884 obj["l" /* LIMIT */] = queryParams.limit_;
6885 let viewFrom = queryParams.viewFrom_;
6886 if (viewFrom === '') {
6887 if (queryParams.isViewFromLeft()) {
6888 viewFrom = "l" /* VIEW_FROM_LEFT */;
6889 }
6890 else {
6891 viewFrom = "r" /* VIEW_FROM_RIGHT */;
6892 }
6893 }
6894 obj["vf" /* VIEW_FROM */] = viewFrom;
6895 }
6896 // For now, priority index is the default, so we only specify if it's some other index
6897 if (queryParams.index_ !== PRIORITY_INDEX) {
6898 obj["i" /* INDEX */] = queryParams.index_.toString();
6899 }
6900 return obj;
6901}
6902
6903/**
6904 * @license
6905 * Copyright 2017 Google LLC
6906 *
6907 * Licensed under the Apache License, Version 2.0 (the "License");
6908 * you may not use this file except in compliance with the License.
6909 * You may obtain a copy of the License at
6910 *
6911 * http://www.apache.org/licenses/LICENSE-2.0
6912 *
6913 * Unless required by applicable law or agreed to in writing, software
6914 * distributed under the License is distributed on an "AS IS" BASIS,
6915 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
6916 * See the License for the specific language governing permissions and
6917 * limitations under the License.
6918 */
6919/**
6920 * An implementation of ServerActions that communicates with the server via REST requests.
6921 * This is mostly useful for compatibility with crawlers, where we don't want to spin up a full
6922 * persistent connection (using WebSockets or long-polling)
6923 */
6924class ReadonlyRestClient extends ServerActions {
6925 /**
6926 * @param repoInfo_ - Data about the namespace we are connecting to
6927 * @param onDataUpdate_ - A callback for new data from the server
6928 */
6929 constructor(repoInfo_, onDataUpdate_, authTokenProvider_, appCheckTokenProvider_) {
6930 super();
6931 this.repoInfo_ = repoInfo_;
6932 this.onDataUpdate_ = onDataUpdate_;
6933 this.authTokenProvider_ = authTokenProvider_;
6934 this.appCheckTokenProvider_ = appCheckTokenProvider_;
6935 /** @private {function(...[*])} */
6936 this.log_ = logWrapper('p:rest:');
6937 /**
6938 * We don't actually need to track listens, except to prevent us calling an onComplete for a listen
6939 * that's been removed. :-/
6940 */
6941 this.listens_ = {};
6942 }
6943 reportStats(stats) {
6944 throw new Error('Method not implemented.');
6945 }
6946 static getListenId_(query, tag) {
6947 if (tag !== undefined) {
6948 return 'tag$' + tag;
6949 }
6950 else {
6951 assert(query._queryParams.isDefault(), "should have a tag if it's not a default query.");
6952 return query._path.toString();
6953 }
6954 }
6955 /** @inheritDoc */
6956 listen(query, currentHashFn, tag, onComplete) {
6957 const pathString = query._path.toString();
6958 this.log_('Listen called for ' + pathString + ' ' + query._queryIdentifier);
6959 // Mark this listener so we can tell if it's removed.
6960 const listenId = ReadonlyRestClient.getListenId_(query, tag);
6961 const thisListen = {};
6962 this.listens_[listenId] = thisListen;
6963 const queryStringParameters = queryParamsToRestQueryStringParameters(query._queryParams);
6964 this.restRequest_(pathString + '.json', queryStringParameters, (error, result) => {
6965 let data = result;
6966 if (error === 404) {
6967 data = null;
6968 error = null;
6969 }
6970 if (error === null) {
6971 this.onDataUpdate_(pathString, data, /*isMerge=*/ false, tag);
6972 }
6973 if (safeGet(this.listens_, listenId) === thisListen) {
6974 let status;
6975 if (!error) {
6976 status = 'ok';
6977 }
6978 else if (error === 401) {
6979 status = 'permission_denied';
6980 }
6981 else {
6982 status = 'rest_error:' + error;
6983 }
6984 onComplete(status, null);
6985 }
6986 });
6987 }
6988 /** @inheritDoc */
6989 unlisten(query, tag) {
6990 const listenId = ReadonlyRestClient.getListenId_(query, tag);
6991 delete this.listens_[listenId];
6992 }
6993 get(query) {
6994 const queryStringParameters = queryParamsToRestQueryStringParameters(query._queryParams);
6995 const pathString = query._path.toString();
6996 const deferred = new Deferred();
6997 this.restRequest_(pathString + '.json', queryStringParameters, (error, result) => {
6998 let data = result;
6999 if (error === 404) {
7000 data = null;
7001 error = null;
7002 }
7003 if (error === null) {
7004 this.onDataUpdate_(pathString, data,
7005 /*isMerge=*/ false,
7006 /*tag=*/ null);
7007 deferred.resolve(data);
7008 }
7009 else {
7010 deferred.reject(new Error(data));
7011 }
7012 });
7013 return deferred.promise;
7014 }
7015 /** @inheritDoc */
7016 refreshAuthToken(token) {
7017 // no-op since we just always call getToken.
7018 }
7019 /**
7020 * Performs a REST request to the given path, with the provided query string parameters,
7021 * and any auth credentials we have.
7022 */
7023 restRequest_(pathString, queryStringParameters = {}, callback) {
7024 queryStringParameters['format'] = 'export';
7025 return Promise.all([
7026 this.authTokenProvider_.getToken(/*forceRefresh=*/ false),
7027 this.appCheckTokenProvider_.getToken(/*forceRefresh=*/ false)
7028 ]).then(([authToken, appCheckToken]) => {
7029 if (authToken && authToken.accessToken) {
7030 queryStringParameters['auth'] = authToken.accessToken;
7031 }
7032 if (appCheckToken && appCheckToken.token) {
7033 queryStringParameters['ac'] = appCheckToken.token;
7034 }
7035 const url = (this.repoInfo_.secure ? 'https://' : 'http://') +
7036 this.repoInfo_.host +
7037 pathString +
7038 '?' +
7039 'ns=' +
7040 this.repoInfo_.namespace +
7041 querystring(queryStringParameters);
7042 this.log_('Sending REST request for ' + url);
7043 const xhr = new XMLHttpRequest();
7044 xhr.onreadystatechange = () => {
7045 if (callback && xhr.readyState === 4) {
7046 this.log_('REST Response for ' + url + ' received. status:', xhr.status, 'response:', xhr.responseText);
7047 let res = null;
7048 if (xhr.status >= 200 && xhr.status < 300) {
7049 try {
7050 res = jsonEval(xhr.responseText);
7051 }
7052 catch (e) {
7053 warn('Failed to parse JSON response for ' +
7054 url +
7055 ': ' +
7056 xhr.responseText);
7057 }
7058 callback(null, res);
7059 }
7060 else {
7061 // 401 and 404 are expected.
7062 if (xhr.status !== 401 && xhr.status !== 404) {
7063 warn('Got unsuccessful REST response for ' +
7064 url +
7065 ' Status: ' +
7066 xhr.status);
7067 }
7068 callback(xhr.status);
7069 }
7070 callback = null;
7071 }
7072 };
7073 xhr.open('GET', url, /*asynchronous=*/ true);
7074 xhr.send();
7075 });
7076 }
7077}
7078
7079/**
7080 * @license
7081 * Copyright 2017 Google LLC
7082 *
7083 * Licensed under the Apache License, Version 2.0 (the "License");
7084 * you may not use this file except in compliance with the License.
7085 * You may obtain a copy of the License at
7086 *
7087 * http://www.apache.org/licenses/LICENSE-2.0
7088 *
7089 * Unless required by applicable law or agreed to in writing, software
7090 * distributed under the License is distributed on an "AS IS" BASIS,
7091 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7092 * See the License for the specific language governing permissions and
7093 * limitations under the License.
7094 */
7095/**
7096 * Mutable object which basically just stores a reference to the "latest" immutable snapshot.
7097 */
7098class SnapshotHolder {
7099 constructor() {
7100 this.rootNode_ = ChildrenNode.EMPTY_NODE;
7101 }
7102 getNode(path) {
7103 return this.rootNode_.getChild(path);
7104 }
7105 updateSnapshot(path, newSnapshotNode) {
7106 this.rootNode_ = this.rootNode_.updateChild(path, newSnapshotNode);
7107 }
7108}
7109
7110/**
7111 * @license
7112 * Copyright 2017 Google LLC
7113 *
7114 * Licensed under the Apache License, Version 2.0 (the "License");
7115 * you may not use this file except in compliance with the License.
7116 * You may obtain a copy of the License at
7117 *
7118 * http://www.apache.org/licenses/LICENSE-2.0
7119 *
7120 * Unless required by applicable law or agreed to in writing, software
7121 * distributed under the License is distributed on an "AS IS" BASIS,
7122 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7123 * See the License for the specific language governing permissions and
7124 * limitations under the License.
7125 */
7126function newSparseSnapshotTree() {
7127 return {
7128 value: null,
7129 children: new Map()
7130 };
7131}
7132/**
7133 * Stores the given node at the specified path. If there is already a node
7134 * at a shallower path, it merges the new data into that snapshot node.
7135 *
7136 * @param path - Path to look up snapshot for.
7137 * @param data - The new data, or null.
7138 */
7139function sparseSnapshotTreeRemember(sparseSnapshotTree, path, data) {
7140 if (pathIsEmpty(path)) {
7141 sparseSnapshotTree.value = data;
7142 sparseSnapshotTree.children.clear();
7143 }
7144 else if (sparseSnapshotTree.value !== null) {
7145 sparseSnapshotTree.value = sparseSnapshotTree.value.updateChild(path, data);
7146 }
7147 else {
7148 const childKey = pathGetFront(path);
7149 if (!sparseSnapshotTree.children.has(childKey)) {
7150 sparseSnapshotTree.children.set(childKey, newSparseSnapshotTree());
7151 }
7152 const child = sparseSnapshotTree.children.get(childKey);
7153 path = pathPopFront(path);
7154 sparseSnapshotTreeRemember(child, path, data);
7155 }
7156}
7157/**
7158 * Purge the data at path from the cache.
7159 *
7160 * @param path - Path to look up snapshot for.
7161 * @returns True if this node should now be removed.
7162 */
7163function sparseSnapshotTreeForget(sparseSnapshotTree, path) {
7164 if (pathIsEmpty(path)) {
7165 sparseSnapshotTree.value = null;
7166 sparseSnapshotTree.children.clear();
7167 return true;
7168 }
7169 else {
7170 if (sparseSnapshotTree.value !== null) {
7171 if (sparseSnapshotTree.value.isLeafNode()) {
7172 // We're trying to forget a node that doesn't exist
7173 return false;
7174 }
7175 else {
7176 const value = sparseSnapshotTree.value;
7177 sparseSnapshotTree.value = null;
7178 value.forEachChild(PRIORITY_INDEX, (key, tree) => {
7179 sparseSnapshotTreeRemember(sparseSnapshotTree, new Path(key), tree);
7180 });
7181 return sparseSnapshotTreeForget(sparseSnapshotTree, path);
7182 }
7183 }
7184 else if (sparseSnapshotTree.children.size > 0) {
7185 const childKey = pathGetFront(path);
7186 path = pathPopFront(path);
7187 if (sparseSnapshotTree.children.has(childKey)) {
7188 const safeToRemove = sparseSnapshotTreeForget(sparseSnapshotTree.children.get(childKey), path);
7189 if (safeToRemove) {
7190 sparseSnapshotTree.children.delete(childKey);
7191 }
7192 }
7193 return sparseSnapshotTree.children.size === 0;
7194 }
7195 else {
7196 return true;
7197 }
7198 }
7199}
7200/**
7201 * Recursively iterates through all of the stored tree and calls the
7202 * callback on each one.
7203 *
7204 * @param prefixPath - Path to look up node for.
7205 * @param func - The function to invoke for each tree.
7206 */
7207function sparseSnapshotTreeForEachTree(sparseSnapshotTree, prefixPath, func) {
7208 if (sparseSnapshotTree.value !== null) {
7209 func(prefixPath, sparseSnapshotTree.value);
7210 }
7211 else {
7212 sparseSnapshotTreeForEachChild(sparseSnapshotTree, (key, tree) => {
7213 const path = new Path(prefixPath.toString() + '/' + key);
7214 sparseSnapshotTreeForEachTree(tree, path, func);
7215 });
7216 }
7217}
7218/**
7219 * Iterates through each immediate child and triggers the callback.
7220 * Only seems to be used in tests.
7221 *
7222 * @param func - The function to invoke for each child.
7223 */
7224function sparseSnapshotTreeForEachChild(sparseSnapshotTree, func) {
7225 sparseSnapshotTree.children.forEach((tree, key) => {
7226 func(key, tree);
7227 });
7228}
7229
7230/**
7231 * @license
7232 * Copyright 2017 Google LLC
7233 *
7234 * Licensed under the Apache License, Version 2.0 (the "License");
7235 * you may not use this file except in compliance with the License.
7236 * You may obtain a copy of the License at
7237 *
7238 * http://www.apache.org/licenses/LICENSE-2.0
7239 *
7240 * Unless required by applicable law or agreed to in writing, software
7241 * distributed under the License is distributed on an "AS IS" BASIS,
7242 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7243 * See the License for the specific language governing permissions and
7244 * limitations under the License.
7245 */
7246/**
7247 * Returns the delta from the previous call to get stats.
7248 *
7249 * @param collection_ - The collection to "listen" to.
7250 */
7251class StatsListener {
7252 constructor(collection_) {
7253 this.collection_ = collection_;
7254 this.last_ = null;
7255 }
7256 get() {
7257 const newStats = this.collection_.get();
7258 const delta = Object.assign({}, newStats);
7259 if (this.last_) {
7260 each(this.last_, (stat, value) => {
7261 delta[stat] = delta[stat] - value;
7262 });
7263 }
7264 this.last_ = newStats;
7265 return delta;
7266 }
7267}
7268
7269/**
7270 * @license
7271 * Copyright 2017 Google LLC
7272 *
7273 * Licensed under the Apache License, Version 2.0 (the "License");
7274 * you may not use this file except in compliance with the License.
7275 * You may obtain a copy of the License at
7276 *
7277 * http://www.apache.org/licenses/LICENSE-2.0
7278 *
7279 * Unless required by applicable law or agreed to in writing, software
7280 * distributed under the License is distributed on an "AS IS" BASIS,
7281 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7282 * See the License for the specific language governing permissions and
7283 * limitations under the License.
7284 */
7285// Assuming some apps may have a short amount of time on page, and a bulk of firebase operations probably
7286// happen on page load, we try to report our first set of stats pretty quickly, but we wait at least 10
7287// seconds to try to ensure the Firebase connection is established / settled.
7288const FIRST_STATS_MIN_TIME = 10 * 1000;
7289const FIRST_STATS_MAX_TIME = 30 * 1000;
7290// We'll continue to report stats on average every 5 minutes.
7291const REPORT_STATS_INTERVAL = 5 * 60 * 1000;
7292class StatsReporter {
7293 constructor(collection, server_) {
7294 this.server_ = server_;
7295 this.statsToReport_ = {};
7296 this.statsListener_ = new StatsListener(collection);
7297 const timeout = FIRST_STATS_MIN_TIME +
7298 (FIRST_STATS_MAX_TIME - FIRST_STATS_MIN_TIME) * Math.random();
7299 setTimeoutNonBlocking(this.reportStats_.bind(this), Math.floor(timeout));
7300 }
7301 reportStats_() {
7302 const stats = this.statsListener_.get();
7303 const reportedStats = {};
7304 let haveStatsToReport = false;
7305 each(stats, (stat, value) => {
7306 if (value > 0 && contains(this.statsToReport_, stat)) {
7307 reportedStats[stat] = value;
7308 haveStatsToReport = true;
7309 }
7310 });
7311 if (haveStatsToReport) {
7312 this.server_.reportStats(reportedStats);
7313 }
7314 // queue our next run.
7315 setTimeoutNonBlocking(this.reportStats_.bind(this), Math.floor(Math.random() * 2 * REPORT_STATS_INTERVAL));
7316 }
7317}
7318
7319/**
7320 * @license
7321 * Copyright 2017 Google LLC
7322 *
7323 * Licensed under the Apache License, Version 2.0 (the "License");
7324 * you may not use this file except in compliance with the License.
7325 * You may obtain a copy of the License at
7326 *
7327 * http://www.apache.org/licenses/LICENSE-2.0
7328 *
7329 * Unless required by applicable law or agreed to in writing, software
7330 * distributed under the License is distributed on an "AS IS" BASIS,
7331 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7332 * See the License for the specific language governing permissions and
7333 * limitations under the License.
7334 */
7335/**
7336 *
7337 * @enum
7338 */
7339var OperationType;
7340(function (OperationType) {
7341 OperationType[OperationType["OVERWRITE"] = 0] = "OVERWRITE";
7342 OperationType[OperationType["MERGE"] = 1] = "MERGE";
7343 OperationType[OperationType["ACK_USER_WRITE"] = 2] = "ACK_USER_WRITE";
7344 OperationType[OperationType["LISTEN_COMPLETE"] = 3] = "LISTEN_COMPLETE";
7345})(OperationType || (OperationType = {}));
7346function newOperationSourceUser() {
7347 return {
7348 fromUser: true,
7349 fromServer: false,
7350 queryId: null,
7351 tagged: false
7352 };
7353}
7354function newOperationSourceServer() {
7355 return {
7356 fromUser: false,
7357 fromServer: true,
7358 queryId: null,
7359 tagged: false
7360 };
7361}
7362function newOperationSourceServerTaggedQuery(queryId) {
7363 return {
7364 fromUser: false,
7365 fromServer: true,
7366 queryId,
7367 tagged: true
7368 };
7369}
7370
7371/**
7372 * @license
7373 * Copyright 2017 Google LLC
7374 *
7375 * Licensed under the Apache License, Version 2.0 (the "License");
7376 * you may not use this file except in compliance with the License.
7377 * You may obtain a copy of the License at
7378 *
7379 * http://www.apache.org/licenses/LICENSE-2.0
7380 *
7381 * Unless required by applicable law or agreed to in writing, software
7382 * distributed under the License is distributed on an "AS IS" BASIS,
7383 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7384 * See the License for the specific language governing permissions and
7385 * limitations under the License.
7386 */
7387class AckUserWrite {
7388 /**
7389 * @param affectedTree - A tree containing true for each affected path. Affected paths can't overlap.
7390 */
7391 constructor(
7392 /** @inheritDoc */ path,
7393 /** @inheritDoc */ affectedTree,
7394 /** @inheritDoc */ revert) {
7395 this.path = path;
7396 this.affectedTree = affectedTree;
7397 this.revert = revert;
7398 /** @inheritDoc */
7399 this.type = OperationType.ACK_USER_WRITE;
7400 /** @inheritDoc */
7401 this.source = newOperationSourceUser();
7402 }
7403 operationForChild(childName) {
7404 if (!pathIsEmpty(this.path)) {
7405 assert(pathGetFront(this.path) === childName, 'operationForChild called for unrelated child.');
7406 return new AckUserWrite(pathPopFront(this.path), this.affectedTree, this.revert);
7407 }
7408 else if (this.affectedTree.value != null) {
7409 assert(this.affectedTree.children.isEmpty(), 'affectedTree should not have overlapping affected paths.');
7410 // All child locations are affected as well; just return same operation.
7411 return this;
7412 }
7413 else {
7414 const childTree = this.affectedTree.subtree(new Path(childName));
7415 return new AckUserWrite(newEmptyPath(), childTree, this.revert);
7416 }
7417 }
7418}
7419
7420/**
7421 * @license
7422 * Copyright 2017 Google LLC
7423 *
7424 * Licensed under the Apache License, Version 2.0 (the "License");
7425 * you may not use this file except in compliance with the License.
7426 * You may obtain a copy of the License at
7427 *
7428 * http://www.apache.org/licenses/LICENSE-2.0
7429 *
7430 * Unless required by applicable law or agreed to in writing, software
7431 * distributed under the License is distributed on an "AS IS" BASIS,
7432 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7433 * See the License for the specific language governing permissions and
7434 * limitations under the License.
7435 */
7436class ListenComplete {
7437 constructor(source, path) {
7438 this.source = source;
7439 this.path = path;
7440 /** @inheritDoc */
7441 this.type = OperationType.LISTEN_COMPLETE;
7442 }
7443 operationForChild(childName) {
7444 if (pathIsEmpty(this.path)) {
7445 return new ListenComplete(this.source, newEmptyPath());
7446 }
7447 else {
7448 return new ListenComplete(this.source, pathPopFront(this.path));
7449 }
7450 }
7451}
7452
7453/**
7454 * @license
7455 * Copyright 2017 Google LLC
7456 *
7457 * Licensed under the Apache License, Version 2.0 (the "License");
7458 * you may not use this file except in compliance with the License.
7459 * You may obtain a copy of the License at
7460 *
7461 * http://www.apache.org/licenses/LICENSE-2.0
7462 *
7463 * Unless required by applicable law or agreed to in writing, software
7464 * distributed under the License is distributed on an "AS IS" BASIS,
7465 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7466 * See the License for the specific language governing permissions and
7467 * limitations under the License.
7468 */
7469class Overwrite {
7470 constructor(source, path, snap) {
7471 this.source = source;
7472 this.path = path;
7473 this.snap = snap;
7474 /** @inheritDoc */
7475 this.type = OperationType.OVERWRITE;
7476 }
7477 operationForChild(childName) {
7478 if (pathIsEmpty(this.path)) {
7479 return new Overwrite(this.source, newEmptyPath(), this.snap.getImmediateChild(childName));
7480 }
7481 else {
7482 return new Overwrite(this.source, pathPopFront(this.path), this.snap);
7483 }
7484 }
7485}
7486
7487/**
7488 * @license
7489 * Copyright 2017 Google LLC
7490 *
7491 * Licensed under the Apache License, Version 2.0 (the "License");
7492 * you may not use this file except in compliance with the License.
7493 * You may obtain a copy of the License at
7494 *
7495 * http://www.apache.org/licenses/LICENSE-2.0
7496 *
7497 * Unless required by applicable law or agreed to in writing, software
7498 * distributed under the License is distributed on an "AS IS" BASIS,
7499 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7500 * See the License for the specific language governing permissions and
7501 * limitations under the License.
7502 */
7503class Merge {
7504 constructor(
7505 /** @inheritDoc */ source,
7506 /** @inheritDoc */ path,
7507 /** @inheritDoc */ children) {
7508 this.source = source;
7509 this.path = path;
7510 this.children = children;
7511 /** @inheritDoc */
7512 this.type = OperationType.MERGE;
7513 }
7514 operationForChild(childName) {
7515 if (pathIsEmpty(this.path)) {
7516 const childTree = this.children.subtree(new Path(childName));
7517 if (childTree.isEmpty()) {
7518 // This child is unaffected
7519 return null;
7520 }
7521 else if (childTree.value) {
7522 // We have a snapshot for the child in question. This becomes an overwrite of the child.
7523 return new Overwrite(this.source, newEmptyPath(), childTree.value);
7524 }
7525 else {
7526 // This is a merge at a deeper level
7527 return new Merge(this.source, newEmptyPath(), childTree);
7528 }
7529 }
7530 else {
7531 assert(pathGetFront(this.path) === childName, "Can't get a merge for a child not on the path of the operation");
7532 return new Merge(this.source, pathPopFront(this.path), this.children);
7533 }
7534 }
7535 toString() {
7536 return ('Operation(' +
7537 this.path +
7538 ': ' +
7539 this.source.toString() +
7540 ' merge: ' +
7541 this.children.toString() +
7542 ')');
7543 }
7544}
7545
7546/**
7547 * @license
7548 * Copyright 2017 Google LLC
7549 *
7550 * Licensed under the Apache License, Version 2.0 (the "License");
7551 * you may not use this file except in compliance with the License.
7552 * You may obtain a copy of the License at
7553 *
7554 * http://www.apache.org/licenses/LICENSE-2.0
7555 *
7556 * Unless required by applicable law or agreed to in writing, software
7557 * distributed under the License is distributed on an "AS IS" BASIS,
7558 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7559 * See the License for the specific language governing permissions and
7560 * limitations under the License.
7561 */
7562/**
7563 * A cache node only stores complete children. Additionally it holds a flag whether the node can be considered fully
7564 * initialized in the sense that we know at one point in time this represented a valid state of the world, e.g.
7565 * initialized with data from the server, or a complete overwrite by the client. The filtered flag also tracks
7566 * whether a node potentially had children removed due to a filter.
7567 */
7568class CacheNode {
7569 constructor(node_, fullyInitialized_, filtered_) {
7570 this.node_ = node_;
7571 this.fullyInitialized_ = fullyInitialized_;
7572 this.filtered_ = filtered_;
7573 }
7574 /**
7575 * Returns whether this node was fully initialized with either server data or a complete overwrite by the client
7576 */
7577 isFullyInitialized() {
7578 return this.fullyInitialized_;
7579 }
7580 /**
7581 * Returns whether this node is potentially missing children due to a filter applied to the node
7582 */
7583 isFiltered() {
7584 return this.filtered_;
7585 }
7586 isCompleteForPath(path) {
7587 if (pathIsEmpty(path)) {
7588 return this.isFullyInitialized() && !this.filtered_;
7589 }
7590 const childKey = pathGetFront(path);
7591 return this.isCompleteForChild(childKey);
7592 }
7593 isCompleteForChild(key) {
7594 return ((this.isFullyInitialized() && !this.filtered_) || this.node_.hasChild(key));
7595 }
7596 getNode() {
7597 return this.node_;
7598 }
7599}
7600
7601/**
7602 * @license
7603 * Copyright 2017 Google LLC
7604 *
7605 * Licensed under the Apache License, Version 2.0 (the "License");
7606 * you may not use this file except in compliance with the License.
7607 * You may obtain a copy of the License at
7608 *
7609 * http://www.apache.org/licenses/LICENSE-2.0
7610 *
7611 * Unless required by applicable law or agreed to in writing, software
7612 * distributed under the License is distributed on an "AS IS" BASIS,
7613 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7614 * See the License for the specific language governing permissions and
7615 * limitations under the License.
7616 */
7617/**
7618 * An EventGenerator is used to convert "raw" changes (Change) as computed by the
7619 * CacheDiffer into actual events (Event) that can be raised. See generateEventsForChanges()
7620 * for details.
7621 *
7622 */
7623class EventGenerator {
7624 constructor(query_) {
7625 this.query_ = query_;
7626 this.index_ = this.query_._queryParams.getIndex();
7627 }
7628}
7629/**
7630 * Given a set of raw changes (no moved events and prevName not specified yet), and a set of
7631 * EventRegistrations that should be notified of these changes, generate the actual events to be raised.
7632 *
7633 * Notes:
7634 * - child_moved events will be synthesized at this time for any child_changed events that affect
7635 * our index.
7636 * - prevName will be calculated based on the index ordering.
7637 */
7638function eventGeneratorGenerateEventsForChanges(eventGenerator, changes, eventCache, eventRegistrations) {
7639 const events = [];
7640 const moves = [];
7641 changes.forEach(change => {
7642 if (change.type === "child_changed" /* CHILD_CHANGED */ &&
7643 eventGenerator.index_.indexedValueChanged(change.oldSnap, change.snapshotNode)) {
7644 moves.push(changeChildMoved(change.childName, change.snapshotNode));
7645 }
7646 });
7647 eventGeneratorGenerateEventsForType(eventGenerator, events, "child_removed" /* CHILD_REMOVED */, changes, eventRegistrations, eventCache);
7648 eventGeneratorGenerateEventsForType(eventGenerator, events, "child_added" /* CHILD_ADDED */, changes, eventRegistrations, eventCache);
7649 eventGeneratorGenerateEventsForType(eventGenerator, events, "child_moved" /* CHILD_MOVED */, moves, eventRegistrations, eventCache);
7650 eventGeneratorGenerateEventsForType(eventGenerator, events, "child_changed" /* CHILD_CHANGED */, changes, eventRegistrations, eventCache);
7651 eventGeneratorGenerateEventsForType(eventGenerator, events, "value" /* VALUE */, changes, eventRegistrations, eventCache);
7652 return events;
7653}
7654/**
7655 * Given changes of a single change type, generate the corresponding events.
7656 */
7657function eventGeneratorGenerateEventsForType(eventGenerator, events, eventType, changes, registrations, eventCache) {
7658 const filteredChanges = changes.filter(change => change.type === eventType);
7659 filteredChanges.sort((a, b) => eventGeneratorCompareChanges(eventGenerator, a, b));
7660 filteredChanges.forEach(change => {
7661 const materializedChange = eventGeneratorMaterializeSingleChange(eventGenerator, change, eventCache);
7662 registrations.forEach(registration => {
7663 if (registration.respondsTo(change.type)) {
7664 events.push(registration.createEvent(materializedChange, eventGenerator.query_));
7665 }
7666 });
7667 });
7668}
7669function eventGeneratorMaterializeSingleChange(eventGenerator, change, eventCache) {
7670 if (change.type === 'value' || change.type === 'child_removed') {
7671 return change;
7672 }
7673 else {
7674 change.prevName = eventCache.getPredecessorChildName(change.childName, change.snapshotNode, eventGenerator.index_);
7675 return change;
7676 }
7677}
7678function eventGeneratorCompareChanges(eventGenerator, a, b) {
7679 if (a.childName == null || b.childName == null) {
7680 throw assertionError('Should only compare child_ events.');
7681 }
7682 const aWrapped = new NamedNode(a.childName, a.snapshotNode);
7683 const bWrapped = new NamedNode(b.childName, b.snapshotNode);
7684 return eventGenerator.index_.compare(aWrapped, bWrapped);
7685}
7686
7687/**
7688 * @license
7689 * Copyright 2017 Google LLC
7690 *
7691 * Licensed under the Apache License, Version 2.0 (the "License");
7692 * you may not use this file except in compliance with the License.
7693 * You may obtain a copy of the License at
7694 *
7695 * http://www.apache.org/licenses/LICENSE-2.0
7696 *
7697 * Unless required by applicable law or agreed to in writing, software
7698 * distributed under the License is distributed on an "AS IS" BASIS,
7699 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7700 * See the License for the specific language governing permissions and
7701 * limitations under the License.
7702 */
7703function newViewCache(eventCache, serverCache) {
7704 return { eventCache, serverCache };
7705}
7706function viewCacheUpdateEventSnap(viewCache, eventSnap, complete, filtered) {
7707 return newViewCache(new CacheNode(eventSnap, complete, filtered), viewCache.serverCache);
7708}
7709function viewCacheUpdateServerSnap(viewCache, serverSnap, complete, filtered) {
7710 return newViewCache(viewCache.eventCache, new CacheNode(serverSnap, complete, filtered));
7711}
7712function viewCacheGetCompleteEventSnap(viewCache) {
7713 return viewCache.eventCache.isFullyInitialized()
7714 ? viewCache.eventCache.getNode()
7715 : null;
7716}
7717function viewCacheGetCompleteServerSnap(viewCache) {
7718 return viewCache.serverCache.isFullyInitialized()
7719 ? viewCache.serverCache.getNode()
7720 : null;
7721}
7722
7723/**
7724 * @license
7725 * Copyright 2017 Google LLC
7726 *
7727 * Licensed under the Apache License, Version 2.0 (the "License");
7728 * you may not use this file except in compliance with the License.
7729 * You may obtain a copy of the License at
7730 *
7731 * http://www.apache.org/licenses/LICENSE-2.0
7732 *
7733 * Unless required by applicable law or agreed to in writing, software
7734 * distributed under the License is distributed on an "AS IS" BASIS,
7735 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7736 * See the License for the specific language governing permissions and
7737 * limitations under the License.
7738 */
7739let emptyChildrenSingleton;
7740/**
7741 * Singleton empty children collection.
7742 *
7743 */
7744const EmptyChildren = () => {
7745 if (!emptyChildrenSingleton) {
7746 emptyChildrenSingleton = new SortedMap(stringCompare);
7747 }
7748 return emptyChildrenSingleton;
7749};
7750/**
7751 * A tree with immutable elements.
7752 */
7753class ImmutableTree {
7754 constructor(value, children = EmptyChildren()) {
7755 this.value = value;
7756 this.children = children;
7757 }
7758 static fromObject(obj) {
7759 let tree = new ImmutableTree(null);
7760 each(obj, (childPath, childSnap) => {
7761 tree = tree.set(new Path(childPath), childSnap);
7762 });
7763 return tree;
7764 }
7765 /**
7766 * True if the value is empty and there are no children
7767 */
7768 isEmpty() {
7769 return this.value === null && this.children.isEmpty();
7770 }
7771 /**
7772 * Given a path and predicate, return the first node and the path to that node
7773 * where the predicate returns true.
7774 *
7775 * TODO Do a perf test -- If we're creating a bunch of `{path: value:}`
7776 * objects on the way back out, it may be better to pass down a pathSoFar obj.
7777 *
7778 * @param relativePath - The remainder of the path
7779 * @param predicate - The predicate to satisfy to return a node
7780 */
7781 findRootMostMatchingPathAndValue(relativePath, predicate) {
7782 if (this.value != null && predicate(this.value)) {
7783 return { path: newEmptyPath(), value: this.value };
7784 }
7785 else {
7786 if (pathIsEmpty(relativePath)) {
7787 return null;
7788 }
7789 else {
7790 const front = pathGetFront(relativePath);
7791 const child = this.children.get(front);
7792 if (child !== null) {
7793 const childExistingPathAndValue = child.findRootMostMatchingPathAndValue(pathPopFront(relativePath), predicate);
7794 if (childExistingPathAndValue != null) {
7795 const fullPath = pathChild(new Path(front), childExistingPathAndValue.path);
7796 return { path: fullPath, value: childExistingPathAndValue.value };
7797 }
7798 else {
7799 return null;
7800 }
7801 }
7802 else {
7803 return null;
7804 }
7805 }
7806 }
7807 }
7808 /**
7809 * Find, if it exists, the shortest subpath of the given path that points a defined
7810 * value in the tree
7811 */
7812 findRootMostValueAndPath(relativePath) {
7813 return this.findRootMostMatchingPathAndValue(relativePath, () => true);
7814 }
7815 /**
7816 * @returns The subtree at the given path
7817 */
7818 subtree(relativePath) {
7819 if (pathIsEmpty(relativePath)) {
7820 return this;
7821 }
7822 else {
7823 const front = pathGetFront(relativePath);
7824 const childTree = this.children.get(front);
7825 if (childTree !== null) {
7826 return childTree.subtree(pathPopFront(relativePath));
7827 }
7828 else {
7829 return new ImmutableTree(null);
7830 }
7831 }
7832 }
7833 /**
7834 * Sets a value at the specified path.
7835 *
7836 * @param relativePath - Path to set value at.
7837 * @param toSet - Value to set.
7838 * @returns Resulting tree.
7839 */
7840 set(relativePath, toSet) {
7841 if (pathIsEmpty(relativePath)) {
7842 return new ImmutableTree(toSet, this.children);
7843 }
7844 else {
7845 const front = pathGetFront(relativePath);
7846 const child = this.children.get(front) || new ImmutableTree(null);
7847 const newChild = child.set(pathPopFront(relativePath), toSet);
7848 const newChildren = this.children.insert(front, newChild);
7849 return new ImmutableTree(this.value, newChildren);
7850 }
7851 }
7852 /**
7853 * Removes the value at the specified path.
7854 *
7855 * @param relativePath - Path to value to remove.
7856 * @returns Resulting tree.
7857 */
7858 remove(relativePath) {
7859 if (pathIsEmpty(relativePath)) {
7860 if (this.children.isEmpty()) {
7861 return new ImmutableTree(null);
7862 }
7863 else {
7864 return new ImmutableTree(null, this.children);
7865 }
7866 }
7867 else {
7868 const front = pathGetFront(relativePath);
7869 const child = this.children.get(front);
7870 if (child) {
7871 const newChild = child.remove(pathPopFront(relativePath));
7872 let newChildren;
7873 if (newChild.isEmpty()) {
7874 newChildren = this.children.remove(front);
7875 }
7876 else {
7877 newChildren = this.children.insert(front, newChild);
7878 }
7879 if (this.value === null && newChildren.isEmpty()) {
7880 return new ImmutableTree(null);
7881 }
7882 else {
7883 return new ImmutableTree(this.value, newChildren);
7884 }
7885 }
7886 else {
7887 return this;
7888 }
7889 }
7890 }
7891 /**
7892 * Gets a value from the tree.
7893 *
7894 * @param relativePath - Path to get value for.
7895 * @returns Value at path, or null.
7896 */
7897 get(relativePath) {
7898 if (pathIsEmpty(relativePath)) {
7899 return this.value;
7900 }
7901 else {
7902 const front = pathGetFront(relativePath);
7903 const child = this.children.get(front);
7904 if (child) {
7905 return child.get(pathPopFront(relativePath));
7906 }
7907 else {
7908 return null;
7909 }
7910 }
7911 }
7912 /**
7913 * Replace the subtree at the specified path with the given new tree.
7914 *
7915 * @param relativePath - Path to replace subtree for.
7916 * @param newTree - New tree.
7917 * @returns Resulting tree.
7918 */
7919 setTree(relativePath, newTree) {
7920 if (pathIsEmpty(relativePath)) {
7921 return newTree;
7922 }
7923 else {
7924 const front = pathGetFront(relativePath);
7925 const child = this.children.get(front) || new ImmutableTree(null);
7926 const newChild = child.setTree(pathPopFront(relativePath), newTree);
7927 let newChildren;
7928 if (newChild.isEmpty()) {
7929 newChildren = this.children.remove(front);
7930 }
7931 else {
7932 newChildren = this.children.insert(front, newChild);
7933 }
7934 return new ImmutableTree(this.value, newChildren);
7935 }
7936 }
7937 /**
7938 * Performs a depth first fold on this tree. Transforms a tree into a single
7939 * value, given a function that operates on the path to a node, an optional
7940 * current value, and a map of child names to folded subtrees
7941 */
7942 fold(fn) {
7943 return this.fold_(newEmptyPath(), fn);
7944 }
7945 /**
7946 * Recursive helper for public-facing fold() method
7947 */
7948 fold_(pathSoFar, fn) {
7949 const accum = {};
7950 this.children.inorderTraversal((childKey, childTree) => {
7951 accum[childKey] = childTree.fold_(pathChild(pathSoFar, childKey), fn);
7952 });
7953 return fn(pathSoFar, this.value, accum);
7954 }
7955 /**
7956 * Find the first matching value on the given path. Return the result of applying f to it.
7957 */
7958 findOnPath(path, f) {
7959 return this.findOnPath_(path, newEmptyPath(), f);
7960 }
7961 findOnPath_(pathToFollow, pathSoFar, f) {
7962 const result = this.value ? f(pathSoFar, this.value) : false;
7963 if (result) {
7964 return result;
7965 }
7966 else {
7967 if (pathIsEmpty(pathToFollow)) {
7968 return null;
7969 }
7970 else {
7971 const front = pathGetFront(pathToFollow);
7972 const nextChild = this.children.get(front);
7973 if (nextChild) {
7974 return nextChild.findOnPath_(pathPopFront(pathToFollow), pathChild(pathSoFar, front), f);
7975 }
7976 else {
7977 return null;
7978 }
7979 }
7980 }
7981 }
7982 foreachOnPath(path, f) {
7983 return this.foreachOnPath_(path, newEmptyPath(), f);
7984 }
7985 foreachOnPath_(pathToFollow, currentRelativePath, f) {
7986 if (pathIsEmpty(pathToFollow)) {
7987 return this;
7988 }
7989 else {
7990 if (this.value) {
7991 f(currentRelativePath, this.value);
7992 }
7993 const front = pathGetFront(pathToFollow);
7994 const nextChild = this.children.get(front);
7995 if (nextChild) {
7996 return nextChild.foreachOnPath_(pathPopFront(pathToFollow), pathChild(currentRelativePath, front), f);
7997 }
7998 else {
7999 return new ImmutableTree(null);
8000 }
8001 }
8002 }
8003 /**
8004 * Calls the given function for each node in the tree that has a value.
8005 *
8006 * @param f - A function to be called with the path from the root of the tree to
8007 * a node, and the value at that node. Called in depth-first order.
8008 */
8009 foreach(f) {
8010 this.foreach_(newEmptyPath(), f);
8011 }
8012 foreach_(currentRelativePath, f) {
8013 this.children.inorderTraversal((childName, childTree) => {
8014 childTree.foreach_(pathChild(currentRelativePath, childName), f);
8015 });
8016 if (this.value) {
8017 f(currentRelativePath, this.value);
8018 }
8019 }
8020 foreachChild(f) {
8021 this.children.inorderTraversal((childName, childTree) => {
8022 if (childTree.value) {
8023 f(childName, childTree.value);
8024 }
8025 });
8026 }
8027}
8028
8029/**
8030 * @license
8031 * Copyright 2017 Google LLC
8032 *
8033 * Licensed under the Apache License, Version 2.0 (the "License");
8034 * you may not use this file except in compliance with the License.
8035 * You may obtain a copy of the License at
8036 *
8037 * http://www.apache.org/licenses/LICENSE-2.0
8038 *
8039 * Unless required by applicable law or agreed to in writing, software
8040 * distributed under the License is distributed on an "AS IS" BASIS,
8041 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
8042 * See the License for the specific language governing permissions and
8043 * limitations under the License.
8044 */
8045/**
8046 * This class holds a collection of writes that can be applied to nodes in unison. It abstracts away the logic with
8047 * dealing with priority writes and multiple nested writes. At any given path there is only allowed to be one write
8048 * modifying that path. Any write to an existing path or shadowing an existing path will modify that existing write
8049 * to reflect the write added.
8050 */
8051class CompoundWrite {
8052 constructor(writeTree_) {
8053 this.writeTree_ = writeTree_;
8054 }
8055 static empty() {
8056 return new CompoundWrite(new ImmutableTree(null));
8057 }
8058}
8059function compoundWriteAddWrite(compoundWrite, path, node) {
8060 if (pathIsEmpty(path)) {
8061 return new CompoundWrite(new ImmutableTree(node));
8062 }
8063 else {
8064 const rootmost = compoundWrite.writeTree_.findRootMostValueAndPath(path);
8065 if (rootmost != null) {
8066 const rootMostPath = rootmost.path;
8067 let value = rootmost.value;
8068 const relativePath = newRelativePath(rootMostPath, path);
8069 value = value.updateChild(relativePath, node);
8070 return new CompoundWrite(compoundWrite.writeTree_.set(rootMostPath, value));
8071 }
8072 else {
8073 const subtree = new ImmutableTree(node);
8074 const newWriteTree = compoundWrite.writeTree_.setTree(path, subtree);
8075 return new CompoundWrite(newWriteTree);
8076 }
8077 }
8078}
8079function compoundWriteAddWrites(compoundWrite, path, updates) {
8080 let newWrite = compoundWrite;
8081 each(updates, (childKey, node) => {
8082 newWrite = compoundWriteAddWrite(newWrite, pathChild(path, childKey), node);
8083 });
8084 return newWrite;
8085}
8086/**
8087 * Will remove a write at the given path and deeper paths. This will <em>not</em> modify a write at a higher
8088 * location, which must be removed by calling this method with that path.
8089 *
8090 * @param compoundWrite - The CompoundWrite to remove.
8091 * @param path - The path at which a write and all deeper writes should be removed
8092 * @returns The new CompoundWrite with the removed path
8093 */
8094function compoundWriteRemoveWrite(compoundWrite, path) {
8095 if (pathIsEmpty(path)) {
8096 return CompoundWrite.empty();
8097 }
8098 else {
8099 const newWriteTree = compoundWrite.writeTree_.setTree(path, new ImmutableTree(null));
8100 return new CompoundWrite(newWriteTree);
8101 }
8102}
8103/**
8104 * Returns whether this CompoundWrite will fully overwrite a node at a given location and can therefore be
8105 * considered "complete".
8106 *
8107 * @param compoundWrite - The CompoundWrite to check.
8108 * @param path - The path to check for
8109 * @returns Whether there is a complete write at that path
8110 */
8111function compoundWriteHasCompleteWrite(compoundWrite, path) {
8112 return compoundWriteGetCompleteNode(compoundWrite, path) != null;
8113}
8114/**
8115 * Returns a node for a path if and only if the node is a "complete" overwrite at that path. This will not aggregate
8116 * writes from deeper paths, but will return child nodes from a more shallow path.
8117 *
8118 * @param compoundWrite - The CompoundWrite to get the node from.
8119 * @param path - The path to get a complete write
8120 * @returns The node if complete at that path, or null otherwise.
8121 */
8122function compoundWriteGetCompleteNode(compoundWrite, path) {
8123 const rootmost = compoundWrite.writeTree_.findRootMostValueAndPath(path);
8124 if (rootmost != null) {
8125 return compoundWrite.writeTree_
8126 .get(rootmost.path)
8127 .getChild(newRelativePath(rootmost.path, path));
8128 }
8129 else {
8130 return null;
8131 }
8132}
8133/**
8134 * Returns all children that are guaranteed to be a complete overwrite.
8135 *
8136 * @param compoundWrite - The CompoundWrite to get children from.
8137 * @returns A list of all complete children.
8138 */
8139function compoundWriteGetCompleteChildren(compoundWrite) {
8140 const children = [];
8141 const node = compoundWrite.writeTree_.value;
8142 if (node != null) {
8143 // If it's a leaf node, it has no children; so nothing to do.
8144 if (!node.isLeafNode()) {
8145 node.forEachChild(PRIORITY_INDEX, (childName, childNode) => {
8146 children.push(new NamedNode(childName, childNode));
8147 });
8148 }
8149 }
8150 else {
8151 compoundWrite.writeTree_.children.inorderTraversal((childName, childTree) => {
8152 if (childTree.value != null) {
8153 children.push(new NamedNode(childName, childTree.value));
8154 }
8155 });
8156 }
8157 return children;
8158}
8159function compoundWriteChildCompoundWrite(compoundWrite, path) {
8160 if (pathIsEmpty(path)) {
8161 return compoundWrite;
8162 }
8163 else {
8164 const shadowingNode = compoundWriteGetCompleteNode(compoundWrite, path);
8165 if (shadowingNode != null) {
8166 return new CompoundWrite(new ImmutableTree(shadowingNode));
8167 }
8168 else {
8169 return new CompoundWrite(compoundWrite.writeTree_.subtree(path));
8170 }
8171 }
8172}
8173/**
8174 * Returns true if this CompoundWrite is empty and therefore does not modify any nodes.
8175 * @returns Whether this CompoundWrite is empty
8176 */
8177function compoundWriteIsEmpty(compoundWrite) {
8178 return compoundWrite.writeTree_.isEmpty();
8179}
8180/**
8181 * Applies this CompoundWrite to a node. The node is returned with all writes from this CompoundWrite applied to the
8182 * node
8183 * @param node - The node to apply this CompoundWrite to
8184 * @returns The node with all writes applied
8185 */
8186function compoundWriteApply(compoundWrite, node) {
8187 return applySubtreeWrite(newEmptyPath(), compoundWrite.writeTree_, node);
8188}
8189function applySubtreeWrite(relativePath, writeTree, node) {
8190 if (writeTree.value != null) {
8191 // Since there a write is always a leaf, we're done here
8192 return node.updateChild(relativePath, writeTree.value);
8193 }
8194 else {
8195 let priorityWrite = null;
8196 writeTree.children.inorderTraversal((childKey, childTree) => {
8197 if (childKey === '.priority') {
8198 // Apply priorities at the end so we don't update priorities for either empty nodes or forget
8199 // to apply priorities to empty nodes that are later filled
8200 assert(childTree.value !== null, 'Priority writes must always be leaf nodes');
8201 priorityWrite = childTree.value;
8202 }
8203 else {
8204 node = applySubtreeWrite(pathChild(relativePath, childKey), childTree, node);
8205 }
8206 });
8207 // If there was a priority write, we only apply it if the node is not empty
8208 if (!node.getChild(relativePath).isEmpty() && priorityWrite !== null) {
8209 node = node.updateChild(pathChild(relativePath, '.priority'), priorityWrite);
8210 }
8211 return node;
8212 }
8213}
8214
8215/**
8216 * @license
8217 * Copyright 2017 Google LLC
8218 *
8219 * Licensed under the Apache License, Version 2.0 (the "License");
8220 * you may not use this file except in compliance with the License.
8221 * You may obtain a copy of the License at
8222 *
8223 * http://www.apache.org/licenses/LICENSE-2.0
8224 *
8225 * Unless required by applicable law or agreed to in writing, software
8226 * distributed under the License is distributed on an "AS IS" BASIS,
8227 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
8228 * See the License for the specific language governing permissions and
8229 * limitations under the License.
8230 */
8231/**
8232 * Create a new WriteTreeRef for the given path. For use with a new sync point at the given path.
8233 *
8234 */
8235function writeTreeChildWrites(writeTree, path) {
8236 return newWriteTreeRef(path, writeTree);
8237}
8238/**
8239 * Record a new overwrite from user code.
8240 *
8241 * @param visible - This is set to false by some transactions. It should be excluded from event caches
8242 */
8243function writeTreeAddOverwrite(writeTree, path, snap, writeId, visible) {
8244 assert(writeId > writeTree.lastWriteId, 'Stacking an older write on top of newer ones');
8245 if (visible === undefined) {
8246 visible = true;
8247 }
8248 writeTree.allWrites.push({
8249 path,
8250 snap,
8251 writeId,
8252 visible
8253 });
8254 if (visible) {
8255 writeTree.visibleWrites = compoundWriteAddWrite(writeTree.visibleWrites, path, snap);
8256 }
8257 writeTree.lastWriteId = writeId;
8258}
8259/**
8260 * Record a new merge from user code.
8261 */
8262function writeTreeAddMerge(writeTree, path, changedChildren, writeId) {
8263 assert(writeId > writeTree.lastWriteId, 'Stacking an older merge on top of newer ones');
8264 writeTree.allWrites.push({
8265 path,
8266 children: changedChildren,
8267 writeId,
8268 visible: true
8269 });
8270 writeTree.visibleWrites = compoundWriteAddWrites(writeTree.visibleWrites, path, changedChildren);
8271 writeTree.lastWriteId = writeId;
8272}
8273function writeTreeGetWrite(writeTree, writeId) {
8274 for (let i = 0; i < writeTree.allWrites.length; i++) {
8275 const record = writeTree.allWrites[i];
8276 if (record.writeId === writeId) {
8277 return record;
8278 }
8279 }
8280 return null;
8281}
8282/**
8283 * Remove a write (either an overwrite or merge) that has been successfully acknowledge by the server. Recalculates
8284 * the tree if necessary. We return true if it may have been visible, meaning views need to reevaluate.
8285 *
8286 * @returns true if the write may have been visible (meaning we'll need to reevaluate / raise
8287 * events as a result).
8288 */
8289function writeTreeRemoveWrite(writeTree, writeId) {
8290 // Note: disabling this check. It could be a transaction that preempted another transaction, and thus was applied
8291 // out of order.
8292 //const validClear = revert || this.allWrites_.length === 0 || writeId <= this.allWrites_[0].writeId;
8293 //assert(validClear, "Either we don't have this write, or it's the first one in the queue");
8294 const idx = writeTree.allWrites.findIndex(s => {
8295 return s.writeId === writeId;
8296 });
8297 assert(idx >= 0, 'removeWrite called with nonexistent writeId.');
8298 const writeToRemove = writeTree.allWrites[idx];
8299 writeTree.allWrites.splice(idx, 1);
8300 let removedWriteWasVisible = writeToRemove.visible;
8301 let removedWriteOverlapsWithOtherWrites = false;
8302 let i = writeTree.allWrites.length - 1;
8303 while (removedWriteWasVisible && i >= 0) {
8304 const currentWrite = writeTree.allWrites[i];
8305 if (currentWrite.visible) {
8306 if (i >= idx &&
8307 writeTreeRecordContainsPath_(currentWrite, writeToRemove.path)) {
8308 // The removed write was completely shadowed by a subsequent write.
8309 removedWriteWasVisible = false;
8310 }
8311 else if (pathContains(writeToRemove.path, currentWrite.path)) {
8312 // Either we're covering some writes or they're covering part of us (depending on which came first).
8313 removedWriteOverlapsWithOtherWrites = true;
8314 }
8315 }
8316 i--;
8317 }
8318 if (!removedWriteWasVisible) {
8319 return false;
8320 }
8321 else if (removedWriteOverlapsWithOtherWrites) {
8322 // There's some shadowing going on. Just rebuild the visible writes from scratch.
8323 writeTreeResetTree_(writeTree);
8324 return true;
8325 }
8326 else {
8327 // There's no shadowing. We can safely just remove the write(s) from visibleWrites.
8328 if (writeToRemove.snap) {
8329 writeTree.visibleWrites = compoundWriteRemoveWrite(writeTree.visibleWrites, writeToRemove.path);
8330 }
8331 else {
8332 const children = writeToRemove.children;
8333 each(children, (childName) => {
8334 writeTree.visibleWrites = compoundWriteRemoveWrite(writeTree.visibleWrites, pathChild(writeToRemove.path, childName));
8335 });
8336 }
8337 return true;
8338 }
8339}
8340function writeTreeRecordContainsPath_(writeRecord, path) {
8341 if (writeRecord.snap) {
8342 return pathContains(writeRecord.path, path);
8343 }
8344 else {
8345 for (const childName in writeRecord.children) {
8346 if (writeRecord.children.hasOwnProperty(childName) &&
8347 pathContains(pathChild(writeRecord.path, childName), path)) {
8348 return true;
8349 }
8350 }
8351 return false;
8352 }
8353}
8354/**
8355 * Re-layer the writes and merges into a tree so we can efficiently calculate event snapshots
8356 */
8357function writeTreeResetTree_(writeTree) {
8358 writeTree.visibleWrites = writeTreeLayerTree_(writeTree.allWrites, writeTreeDefaultFilter_, newEmptyPath());
8359 if (writeTree.allWrites.length > 0) {
8360 writeTree.lastWriteId =
8361 writeTree.allWrites[writeTree.allWrites.length - 1].writeId;
8362 }
8363 else {
8364 writeTree.lastWriteId = -1;
8365 }
8366}
8367/**
8368 * The default filter used when constructing the tree. Keep everything that's visible.
8369 */
8370function writeTreeDefaultFilter_(write) {
8371 return write.visible;
8372}
8373/**
8374 * Static method. Given an array of WriteRecords, a filter for which ones to include, and a path, construct the tree of
8375 * event data at that path.
8376 */
8377function writeTreeLayerTree_(writes, filter, treeRoot) {
8378 let compoundWrite = CompoundWrite.empty();
8379 for (let i = 0; i < writes.length; ++i) {
8380 const write = writes[i];
8381 // Theory, a later set will either:
8382 // a) abort a relevant transaction, so no need to worry about excluding it from calculating that transaction
8383 // b) not be relevant to a transaction (separate branch), so again will not affect the data for that transaction
8384 if (filter(write)) {
8385 const writePath = write.path;
8386 let relativePath;
8387 if (write.snap) {
8388 if (pathContains(treeRoot, writePath)) {
8389 relativePath = newRelativePath(treeRoot, writePath);
8390 compoundWrite = compoundWriteAddWrite(compoundWrite, relativePath, write.snap);
8391 }
8392 else if (pathContains(writePath, treeRoot)) {
8393 relativePath = newRelativePath(writePath, treeRoot);
8394 compoundWrite = compoundWriteAddWrite(compoundWrite, newEmptyPath(), write.snap.getChild(relativePath));
8395 }
8396 else ;
8397 }
8398 else if (write.children) {
8399 if (pathContains(treeRoot, writePath)) {
8400 relativePath = newRelativePath(treeRoot, writePath);
8401 compoundWrite = compoundWriteAddWrites(compoundWrite, relativePath, write.children);
8402 }
8403 else if (pathContains(writePath, treeRoot)) {
8404 relativePath = newRelativePath(writePath, treeRoot);
8405 if (pathIsEmpty(relativePath)) {
8406 compoundWrite = compoundWriteAddWrites(compoundWrite, newEmptyPath(), write.children);
8407 }
8408 else {
8409 const child = safeGet(write.children, pathGetFront(relativePath));
8410 if (child) {
8411 // There exists a child in this node that matches the root path
8412 const deepNode = child.getChild(pathPopFront(relativePath));
8413 compoundWrite = compoundWriteAddWrite(compoundWrite, newEmptyPath(), deepNode);
8414 }
8415 }
8416 }
8417 else ;
8418 }
8419 else {
8420 throw assertionError('WriteRecord should have .snap or .children');
8421 }
8422 }
8423 }
8424 return compoundWrite;
8425}
8426/**
8427 * Given optional, underlying server data, and an optional set of constraints (exclude some sets, include hidden
8428 * writes), attempt to calculate a complete snapshot for the given path
8429 *
8430 * @param writeIdsToExclude - An optional set to be excluded
8431 * @param includeHiddenWrites - Defaults to false, whether or not to layer on writes with visible set to false
8432 */
8433function writeTreeCalcCompleteEventCache(writeTree, treePath, completeServerCache, writeIdsToExclude, includeHiddenWrites) {
8434 if (!writeIdsToExclude && !includeHiddenWrites) {
8435 const shadowingNode = compoundWriteGetCompleteNode(writeTree.visibleWrites, treePath);
8436 if (shadowingNode != null) {
8437 return shadowingNode;
8438 }
8439 else {
8440 const subMerge = compoundWriteChildCompoundWrite(writeTree.visibleWrites, treePath);
8441 if (compoundWriteIsEmpty(subMerge)) {
8442 return completeServerCache;
8443 }
8444 else if (completeServerCache == null &&
8445 !compoundWriteHasCompleteWrite(subMerge, newEmptyPath())) {
8446 // We wouldn't have a complete snapshot, since there's no underlying data and no complete shadow
8447 return null;
8448 }
8449 else {
8450 const layeredCache = completeServerCache || ChildrenNode.EMPTY_NODE;
8451 return compoundWriteApply(subMerge, layeredCache);
8452 }
8453 }
8454 }
8455 else {
8456 const merge = compoundWriteChildCompoundWrite(writeTree.visibleWrites, treePath);
8457 if (!includeHiddenWrites && compoundWriteIsEmpty(merge)) {
8458 return completeServerCache;
8459 }
8460 else {
8461 // If the server cache is null, and we don't have a complete cache, we need to return null
8462 if (!includeHiddenWrites &&
8463 completeServerCache == null &&
8464 !compoundWriteHasCompleteWrite(merge, newEmptyPath())) {
8465 return null;
8466 }
8467 else {
8468 const filter = function (write) {
8469 return ((write.visible || includeHiddenWrites) &&
8470 (!writeIdsToExclude ||
8471 !~writeIdsToExclude.indexOf(write.writeId)) &&
8472 (pathContains(write.path, treePath) ||
8473 pathContains(treePath, write.path)));
8474 };
8475 const mergeAtPath = writeTreeLayerTree_(writeTree.allWrites, filter, treePath);
8476 const layeredCache = completeServerCache || ChildrenNode.EMPTY_NODE;
8477 return compoundWriteApply(mergeAtPath, layeredCache);
8478 }
8479 }
8480 }
8481}
8482/**
8483 * With optional, underlying server data, attempt to return a children node of children that we have complete data for.
8484 * Used when creating new views, to pre-fill their complete event children snapshot.
8485 */
8486function writeTreeCalcCompleteEventChildren(writeTree, treePath, completeServerChildren) {
8487 let completeChildren = ChildrenNode.EMPTY_NODE;
8488 const topLevelSet = compoundWriteGetCompleteNode(writeTree.visibleWrites, treePath);
8489 if (topLevelSet) {
8490 if (!topLevelSet.isLeafNode()) {
8491 // we're shadowing everything. Return the children.
8492 topLevelSet.forEachChild(PRIORITY_INDEX, (childName, childSnap) => {
8493 completeChildren = completeChildren.updateImmediateChild(childName, childSnap);
8494 });
8495 }
8496 return completeChildren;
8497 }
8498 else if (completeServerChildren) {
8499 // Layer any children we have on top of this
8500 // We know we don't have a top-level set, so just enumerate existing children
8501 const merge = compoundWriteChildCompoundWrite(writeTree.visibleWrites, treePath);
8502 completeServerChildren.forEachChild(PRIORITY_INDEX, (childName, childNode) => {
8503 const node = compoundWriteApply(compoundWriteChildCompoundWrite(merge, new Path(childName)), childNode);
8504 completeChildren = completeChildren.updateImmediateChild(childName, node);
8505 });
8506 // Add any complete children we have from the set
8507 compoundWriteGetCompleteChildren(merge).forEach(namedNode => {
8508 completeChildren = completeChildren.updateImmediateChild(namedNode.name, namedNode.node);
8509 });
8510 return completeChildren;
8511 }
8512 else {
8513 // We don't have anything to layer on top of. Layer on any children we have
8514 // Note that we can return an empty snap if we have a defined delete
8515 const merge = compoundWriteChildCompoundWrite(writeTree.visibleWrites, treePath);
8516 compoundWriteGetCompleteChildren(merge).forEach(namedNode => {
8517 completeChildren = completeChildren.updateImmediateChild(namedNode.name, namedNode.node);
8518 });
8519 return completeChildren;
8520 }
8521}
8522/**
8523 * Given that the underlying server data has updated, determine what, if anything, needs to be
8524 * applied to the event cache.
8525 *
8526 * Possibilities:
8527 *
8528 * 1. No writes are shadowing. Events should be raised, the snap to be applied comes from the server data
8529 *
8530 * 2. Some write is completely shadowing. No events to be raised
8531 *
8532 * 3. Is partially shadowed. Events
8533 *
8534 * Either existingEventSnap or existingServerSnap must exist
8535 */
8536function writeTreeCalcEventCacheAfterServerOverwrite(writeTree, treePath, childPath, existingEventSnap, existingServerSnap) {
8537 assert(existingEventSnap || existingServerSnap, 'Either existingEventSnap or existingServerSnap must exist');
8538 const path = pathChild(treePath, childPath);
8539 if (compoundWriteHasCompleteWrite(writeTree.visibleWrites, path)) {
8540 // At this point we can probably guarantee that we're in case 2, meaning no events
8541 // May need to check visibility while doing the findRootMostValueAndPath call
8542 return null;
8543 }
8544 else {
8545 // No complete shadowing. We're either partially shadowing or not shadowing at all.
8546 const childMerge = compoundWriteChildCompoundWrite(writeTree.visibleWrites, path);
8547 if (compoundWriteIsEmpty(childMerge)) {
8548 // We're not shadowing at all. Case 1
8549 return existingServerSnap.getChild(childPath);
8550 }
8551 else {
8552 // This could be more efficient if the serverNode + updates doesn't change the eventSnap
8553 // However this is tricky to find out, since user updates don't necessary change the server
8554 // snap, e.g. priority updates on empty nodes, or deep deletes. Another special case is if the server
8555 // adds nodes, but doesn't change any existing writes. It is therefore not enough to
8556 // only check if the updates change the serverNode.
8557 // Maybe check if the merge tree contains these special cases and only do a full overwrite in that case?
8558 return compoundWriteApply(childMerge, existingServerSnap.getChild(childPath));
8559 }
8560 }
8561}
8562/**
8563 * Returns a complete child for a given server snap after applying all user writes or null if there is no
8564 * complete child for this ChildKey.
8565 */
8566function writeTreeCalcCompleteChild(writeTree, treePath, childKey, existingServerSnap) {
8567 const path = pathChild(treePath, childKey);
8568 const shadowingNode = compoundWriteGetCompleteNode(writeTree.visibleWrites, path);
8569 if (shadowingNode != null) {
8570 return shadowingNode;
8571 }
8572 else {
8573 if (existingServerSnap.isCompleteForChild(childKey)) {
8574 const childMerge = compoundWriteChildCompoundWrite(writeTree.visibleWrites, path);
8575 return compoundWriteApply(childMerge, existingServerSnap.getNode().getImmediateChild(childKey));
8576 }
8577 else {
8578 return null;
8579 }
8580 }
8581}
8582/**
8583 * Returns a node if there is a complete overwrite for this path. More specifically, if there is a write at
8584 * a higher path, this will return the child of that write relative to the write and this path.
8585 * Returns null if there is no write at this path.
8586 */
8587function writeTreeShadowingWrite(writeTree, path) {
8588 return compoundWriteGetCompleteNode(writeTree.visibleWrites, path);
8589}
8590/**
8591 * This method is used when processing child remove events on a query. If we can, we pull in children that were outside
8592 * the window, but may now be in the window.
8593 */
8594function writeTreeCalcIndexedSlice(writeTree, treePath, completeServerData, startPost, count, reverse, index) {
8595 let toIterate;
8596 const merge = compoundWriteChildCompoundWrite(writeTree.visibleWrites, treePath);
8597 const shadowingNode = compoundWriteGetCompleteNode(merge, newEmptyPath());
8598 if (shadowingNode != null) {
8599 toIterate = shadowingNode;
8600 }
8601 else if (completeServerData != null) {
8602 toIterate = compoundWriteApply(merge, completeServerData);
8603 }
8604 else {
8605 // no children to iterate on
8606 return [];
8607 }
8608 toIterate = toIterate.withIndex(index);
8609 if (!toIterate.isEmpty() && !toIterate.isLeafNode()) {
8610 const nodes = [];
8611 const cmp = index.getCompare();
8612 const iter = reverse
8613 ? toIterate.getReverseIteratorFrom(startPost, index)
8614 : toIterate.getIteratorFrom(startPost, index);
8615 let next = iter.getNext();
8616 while (next && nodes.length < count) {
8617 if (cmp(next, startPost) !== 0) {
8618 nodes.push(next);
8619 }
8620 next = iter.getNext();
8621 }
8622 return nodes;
8623 }
8624 else {
8625 return [];
8626 }
8627}
8628function newWriteTree() {
8629 return {
8630 visibleWrites: CompoundWrite.empty(),
8631 allWrites: [],
8632 lastWriteId: -1
8633 };
8634}
8635/**
8636 * If possible, returns a complete event cache, using the underlying server data if possible. In addition, can be used
8637 * to get a cache that includes hidden writes, and excludes arbitrary writes. Note that customizing the returned node
8638 * can lead to a more expensive calculation.
8639 *
8640 * @param writeIdsToExclude - Optional writes to exclude.
8641 * @param includeHiddenWrites - Defaults to false, whether or not to layer on writes with visible set to false
8642 */
8643function writeTreeRefCalcCompleteEventCache(writeTreeRef, completeServerCache, writeIdsToExclude, includeHiddenWrites) {
8644 return writeTreeCalcCompleteEventCache(writeTreeRef.writeTree, writeTreeRef.treePath, completeServerCache, writeIdsToExclude, includeHiddenWrites);
8645}
8646/**
8647 * If possible, returns a children node containing all of the complete children we have data for. The returned data is a
8648 * mix of the given server data and write data.
8649 *
8650 */
8651function writeTreeRefCalcCompleteEventChildren(writeTreeRef, completeServerChildren) {
8652 return writeTreeCalcCompleteEventChildren(writeTreeRef.writeTree, writeTreeRef.treePath, completeServerChildren);
8653}
8654/**
8655 * Given that either the underlying server data has updated or the outstanding writes have updated, determine what,
8656 * if anything, needs to be applied to the event cache.
8657 *
8658 * Possibilities:
8659 *
8660 * 1. No writes are shadowing. Events should be raised, the snap to be applied comes from the server data
8661 *
8662 * 2. Some write is completely shadowing. No events to be raised
8663 *
8664 * 3. Is partially shadowed. Events should be raised
8665 *
8666 * Either existingEventSnap or existingServerSnap must exist, this is validated via an assert
8667 *
8668 *
8669 */
8670function writeTreeRefCalcEventCacheAfterServerOverwrite(writeTreeRef, path, existingEventSnap, existingServerSnap) {
8671 return writeTreeCalcEventCacheAfterServerOverwrite(writeTreeRef.writeTree, writeTreeRef.treePath, path, existingEventSnap, existingServerSnap);
8672}
8673/**
8674 * Returns a node if there is a complete overwrite for this path. More specifically, if there is a write at
8675 * a higher path, this will return the child of that write relative to the write and this path.
8676 * Returns null if there is no write at this path.
8677 *
8678 */
8679function writeTreeRefShadowingWrite(writeTreeRef, path) {
8680 return writeTreeShadowingWrite(writeTreeRef.writeTree, pathChild(writeTreeRef.treePath, path));
8681}
8682/**
8683 * This method is used when processing child remove events on a query. If we can, we pull in children that were outside
8684 * the window, but may now be in the window
8685 */
8686function writeTreeRefCalcIndexedSlice(writeTreeRef, completeServerData, startPost, count, reverse, index) {
8687 return writeTreeCalcIndexedSlice(writeTreeRef.writeTree, writeTreeRef.treePath, completeServerData, startPost, count, reverse, index);
8688}
8689/**
8690 * Returns a complete child for a given server snap after applying all user writes or null if there is no
8691 * complete child for this ChildKey.
8692 */
8693function writeTreeRefCalcCompleteChild(writeTreeRef, childKey, existingServerCache) {
8694 return writeTreeCalcCompleteChild(writeTreeRef.writeTree, writeTreeRef.treePath, childKey, existingServerCache);
8695}
8696/**
8697 * Return a WriteTreeRef for a child.
8698 */
8699function writeTreeRefChild(writeTreeRef, childName) {
8700 return newWriteTreeRef(pathChild(writeTreeRef.treePath, childName), writeTreeRef.writeTree);
8701}
8702function newWriteTreeRef(path, writeTree) {
8703 return {
8704 treePath: path,
8705 writeTree
8706 };
8707}
8708
8709/**
8710 * @license
8711 * Copyright 2017 Google LLC
8712 *
8713 * Licensed under the Apache License, Version 2.0 (the "License");
8714 * you may not use this file except in compliance with the License.
8715 * You may obtain a copy of the License at
8716 *
8717 * http://www.apache.org/licenses/LICENSE-2.0
8718 *
8719 * Unless required by applicable law or agreed to in writing, software
8720 * distributed under the License is distributed on an "AS IS" BASIS,
8721 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
8722 * See the License for the specific language governing permissions and
8723 * limitations under the License.
8724 */
8725class ChildChangeAccumulator {
8726 constructor() {
8727 this.changeMap = new Map();
8728 }
8729 trackChildChange(change) {
8730 const type = change.type;
8731 const childKey = change.childName;
8732 assert(type === "child_added" /* CHILD_ADDED */ ||
8733 type === "child_changed" /* CHILD_CHANGED */ ||
8734 type === "child_removed" /* CHILD_REMOVED */, 'Only child changes supported for tracking');
8735 assert(childKey !== '.priority', 'Only non-priority child changes can be tracked.');
8736 const oldChange = this.changeMap.get(childKey);
8737 if (oldChange) {
8738 const oldType = oldChange.type;
8739 if (type === "child_added" /* CHILD_ADDED */ &&
8740 oldType === "child_removed" /* CHILD_REMOVED */) {
8741 this.changeMap.set(childKey, changeChildChanged(childKey, change.snapshotNode, oldChange.snapshotNode));
8742 }
8743 else if (type === "child_removed" /* CHILD_REMOVED */ &&
8744 oldType === "child_added" /* CHILD_ADDED */) {
8745 this.changeMap.delete(childKey);
8746 }
8747 else if (type === "child_removed" /* CHILD_REMOVED */ &&
8748 oldType === "child_changed" /* CHILD_CHANGED */) {
8749 this.changeMap.set(childKey, changeChildRemoved(childKey, oldChange.oldSnap));
8750 }
8751 else if (type === "child_changed" /* CHILD_CHANGED */ &&
8752 oldType === "child_added" /* CHILD_ADDED */) {
8753 this.changeMap.set(childKey, changeChildAdded(childKey, change.snapshotNode));
8754 }
8755 else if (type === "child_changed" /* CHILD_CHANGED */ &&
8756 oldType === "child_changed" /* CHILD_CHANGED */) {
8757 this.changeMap.set(childKey, changeChildChanged(childKey, change.snapshotNode, oldChange.oldSnap));
8758 }
8759 else {
8760 throw assertionError('Illegal combination of changes: ' +
8761 change +
8762 ' occurred after ' +
8763 oldChange);
8764 }
8765 }
8766 else {
8767 this.changeMap.set(childKey, change);
8768 }
8769 }
8770 getChanges() {
8771 return Array.from(this.changeMap.values());
8772 }
8773}
8774
8775/**
8776 * @license
8777 * Copyright 2017 Google LLC
8778 *
8779 * Licensed under the Apache License, Version 2.0 (the "License");
8780 * you may not use this file except in compliance with the License.
8781 * You may obtain a copy of the License at
8782 *
8783 * http://www.apache.org/licenses/LICENSE-2.0
8784 *
8785 * Unless required by applicable law or agreed to in writing, software
8786 * distributed under the License is distributed on an "AS IS" BASIS,
8787 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
8788 * See the License for the specific language governing permissions and
8789 * limitations under the License.
8790 */
8791/**
8792 * An implementation of CompleteChildSource that never returns any additional children
8793 */
8794// eslint-disable-next-line @typescript-eslint/naming-convention
8795class NoCompleteChildSource_ {
8796 getCompleteChild(childKey) {
8797 return null;
8798 }
8799 getChildAfterChild(index, child, reverse) {
8800 return null;
8801 }
8802}
8803/**
8804 * Singleton instance.
8805 */
8806const NO_COMPLETE_CHILD_SOURCE = new NoCompleteChildSource_();
8807/**
8808 * An implementation of CompleteChildSource that uses a WriteTree in addition to any other server data or
8809 * old event caches available to calculate complete children.
8810 */
8811class WriteTreeCompleteChildSource {
8812 constructor(writes_, viewCache_, optCompleteServerCache_ = null) {
8813 this.writes_ = writes_;
8814 this.viewCache_ = viewCache_;
8815 this.optCompleteServerCache_ = optCompleteServerCache_;
8816 }
8817 getCompleteChild(childKey) {
8818 const node = this.viewCache_.eventCache;
8819 if (node.isCompleteForChild(childKey)) {
8820 return node.getNode().getImmediateChild(childKey);
8821 }
8822 else {
8823 const serverNode = this.optCompleteServerCache_ != null
8824 ? new CacheNode(this.optCompleteServerCache_, true, false)
8825 : this.viewCache_.serverCache;
8826 return writeTreeRefCalcCompleteChild(this.writes_, childKey, serverNode);
8827 }
8828 }
8829 getChildAfterChild(index, child, reverse) {
8830 const completeServerData = this.optCompleteServerCache_ != null
8831 ? this.optCompleteServerCache_
8832 : viewCacheGetCompleteServerSnap(this.viewCache_);
8833 const nodes = writeTreeRefCalcIndexedSlice(this.writes_, completeServerData, child, 1, reverse, index);
8834 if (nodes.length === 0) {
8835 return null;
8836 }
8837 else {
8838 return nodes[0];
8839 }
8840 }
8841}
8842
8843/**
8844 * @license
8845 * Copyright 2017 Google LLC
8846 *
8847 * Licensed under the Apache License, Version 2.0 (the "License");
8848 * you may not use this file except in compliance with the License.
8849 * You may obtain a copy of the License at
8850 *
8851 * http://www.apache.org/licenses/LICENSE-2.0
8852 *
8853 * Unless required by applicable law or agreed to in writing, software
8854 * distributed under the License is distributed on an "AS IS" BASIS,
8855 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
8856 * See the License for the specific language governing permissions and
8857 * limitations under the License.
8858 */
8859function newViewProcessor(filter) {
8860 return { filter };
8861}
8862function viewProcessorAssertIndexed(viewProcessor, viewCache) {
8863 assert(viewCache.eventCache.getNode().isIndexed(viewProcessor.filter.getIndex()), 'Event snap not indexed');
8864 assert(viewCache.serverCache.getNode().isIndexed(viewProcessor.filter.getIndex()), 'Server snap not indexed');
8865}
8866function viewProcessorApplyOperation(viewProcessor, oldViewCache, operation, writesCache, completeCache) {
8867 const accumulator = new ChildChangeAccumulator();
8868 let newViewCache, filterServerNode;
8869 if (operation.type === OperationType.OVERWRITE) {
8870 const overwrite = operation;
8871 if (overwrite.source.fromUser) {
8872 newViewCache = viewProcessorApplyUserOverwrite(viewProcessor, oldViewCache, overwrite.path, overwrite.snap, writesCache, completeCache, accumulator);
8873 }
8874 else {
8875 assert(overwrite.source.fromServer, 'Unknown source.');
8876 // We filter the node if it's a tagged update or the node has been previously filtered and the
8877 // update is not at the root in which case it is ok (and necessary) to mark the node unfiltered
8878 // again
8879 filterServerNode =
8880 overwrite.source.tagged ||
8881 (oldViewCache.serverCache.isFiltered() && !pathIsEmpty(overwrite.path));
8882 newViewCache = viewProcessorApplyServerOverwrite(viewProcessor, oldViewCache, overwrite.path, overwrite.snap, writesCache, completeCache, filterServerNode, accumulator);
8883 }
8884 }
8885 else if (operation.type === OperationType.MERGE) {
8886 const merge = operation;
8887 if (merge.source.fromUser) {
8888 newViewCache = viewProcessorApplyUserMerge(viewProcessor, oldViewCache, merge.path, merge.children, writesCache, completeCache, accumulator);
8889 }
8890 else {
8891 assert(merge.source.fromServer, 'Unknown source.');
8892 // We filter the node if it's a tagged update or the node has been previously filtered
8893 filterServerNode =
8894 merge.source.tagged || oldViewCache.serverCache.isFiltered();
8895 newViewCache = viewProcessorApplyServerMerge(viewProcessor, oldViewCache, merge.path, merge.children, writesCache, completeCache, filterServerNode, accumulator);
8896 }
8897 }
8898 else if (operation.type === OperationType.ACK_USER_WRITE) {
8899 const ackUserWrite = operation;
8900 if (!ackUserWrite.revert) {
8901 newViewCache = viewProcessorAckUserWrite(viewProcessor, oldViewCache, ackUserWrite.path, ackUserWrite.affectedTree, writesCache, completeCache, accumulator);
8902 }
8903 else {
8904 newViewCache = viewProcessorRevertUserWrite(viewProcessor, oldViewCache, ackUserWrite.path, writesCache, completeCache, accumulator);
8905 }
8906 }
8907 else if (operation.type === OperationType.LISTEN_COMPLETE) {
8908 newViewCache = viewProcessorListenComplete(viewProcessor, oldViewCache, operation.path, writesCache, accumulator);
8909 }
8910 else {
8911 throw assertionError('Unknown operation type: ' + operation.type);
8912 }
8913 const changes = accumulator.getChanges();
8914 viewProcessorMaybeAddValueEvent(oldViewCache, newViewCache, changes);
8915 return { viewCache: newViewCache, changes };
8916}
8917function viewProcessorMaybeAddValueEvent(oldViewCache, newViewCache, accumulator) {
8918 const eventSnap = newViewCache.eventCache;
8919 if (eventSnap.isFullyInitialized()) {
8920 const isLeafOrEmpty = eventSnap.getNode().isLeafNode() || eventSnap.getNode().isEmpty();
8921 const oldCompleteSnap = viewCacheGetCompleteEventSnap(oldViewCache);
8922 if (accumulator.length > 0 ||
8923 !oldViewCache.eventCache.isFullyInitialized() ||
8924 (isLeafOrEmpty && !eventSnap.getNode().equals(oldCompleteSnap)) ||
8925 !eventSnap.getNode().getPriority().equals(oldCompleteSnap.getPriority())) {
8926 accumulator.push(changeValue(viewCacheGetCompleteEventSnap(newViewCache)));
8927 }
8928 }
8929}
8930function viewProcessorGenerateEventCacheAfterServerEvent(viewProcessor, viewCache, changePath, writesCache, source, accumulator) {
8931 const oldEventSnap = viewCache.eventCache;
8932 if (writeTreeRefShadowingWrite(writesCache, changePath) != null) {
8933 // we have a shadowing write, ignore changes
8934 return viewCache;
8935 }
8936 else {
8937 let newEventCache, serverNode;
8938 if (pathIsEmpty(changePath)) {
8939 // TODO: figure out how this plays with "sliding ack windows"
8940 assert(viewCache.serverCache.isFullyInitialized(), 'If change path is empty, we must have complete server data');
8941 if (viewCache.serverCache.isFiltered()) {
8942 // We need to special case this, because we need to only apply writes to complete children, or
8943 // we might end up raising events for incomplete children. If the server data is filtered deep
8944 // writes cannot be guaranteed to be complete
8945 const serverCache = viewCacheGetCompleteServerSnap(viewCache);
8946 const completeChildren = serverCache instanceof ChildrenNode
8947 ? serverCache
8948 : ChildrenNode.EMPTY_NODE;
8949 const completeEventChildren = writeTreeRefCalcCompleteEventChildren(writesCache, completeChildren);
8950 newEventCache = viewProcessor.filter.updateFullNode(viewCache.eventCache.getNode(), completeEventChildren, accumulator);
8951 }
8952 else {
8953 const completeNode = writeTreeRefCalcCompleteEventCache(writesCache, viewCacheGetCompleteServerSnap(viewCache));
8954 newEventCache = viewProcessor.filter.updateFullNode(viewCache.eventCache.getNode(), completeNode, accumulator);
8955 }
8956 }
8957 else {
8958 const childKey = pathGetFront(changePath);
8959 if (childKey === '.priority') {
8960 assert(pathGetLength(changePath) === 1, "Can't have a priority with additional path components");
8961 const oldEventNode = oldEventSnap.getNode();
8962 serverNode = viewCache.serverCache.getNode();
8963 // we might have overwrites for this priority
8964 const updatedPriority = writeTreeRefCalcEventCacheAfterServerOverwrite(writesCache, changePath, oldEventNode, serverNode);
8965 if (updatedPriority != null) {
8966 newEventCache = viewProcessor.filter.updatePriority(oldEventNode, updatedPriority);
8967 }
8968 else {
8969 // priority didn't change, keep old node
8970 newEventCache = oldEventSnap.getNode();
8971 }
8972 }
8973 else {
8974 const childChangePath = pathPopFront(changePath);
8975 // update child
8976 let newEventChild;
8977 if (oldEventSnap.isCompleteForChild(childKey)) {
8978 serverNode = viewCache.serverCache.getNode();
8979 const eventChildUpdate = writeTreeRefCalcEventCacheAfterServerOverwrite(writesCache, changePath, oldEventSnap.getNode(), serverNode);
8980 if (eventChildUpdate != null) {
8981 newEventChild = oldEventSnap
8982 .getNode()
8983 .getImmediateChild(childKey)
8984 .updateChild(childChangePath, eventChildUpdate);
8985 }
8986 else {
8987 // Nothing changed, just keep the old child
8988 newEventChild = oldEventSnap.getNode().getImmediateChild(childKey);
8989 }
8990 }
8991 else {
8992 newEventChild = writeTreeRefCalcCompleteChild(writesCache, childKey, viewCache.serverCache);
8993 }
8994 if (newEventChild != null) {
8995 newEventCache = viewProcessor.filter.updateChild(oldEventSnap.getNode(), childKey, newEventChild, childChangePath, source, accumulator);
8996 }
8997 else {
8998 // no complete child available or no change
8999 newEventCache = oldEventSnap.getNode();
9000 }
9001 }
9002 }
9003 return viewCacheUpdateEventSnap(viewCache, newEventCache, oldEventSnap.isFullyInitialized() || pathIsEmpty(changePath), viewProcessor.filter.filtersNodes());
9004 }
9005}
9006function viewProcessorApplyServerOverwrite(viewProcessor, oldViewCache, changePath, changedSnap, writesCache, completeCache, filterServerNode, accumulator) {
9007 const oldServerSnap = oldViewCache.serverCache;
9008 let newServerCache;
9009 const serverFilter = filterServerNode
9010 ? viewProcessor.filter
9011 : viewProcessor.filter.getIndexedFilter();
9012 if (pathIsEmpty(changePath)) {
9013 newServerCache = serverFilter.updateFullNode(oldServerSnap.getNode(), changedSnap, null);
9014 }
9015 else if (serverFilter.filtersNodes() && !oldServerSnap.isFiltered()) {
9016 // we want to filter the server node, but we didn't filter the server node yet, so simulate a full update
9017 const newServerNode = oldServerSnap
9018 .getNode()
9019 .updateChild(changePath, changedSnap);
9020 newServerCache = serverFilter.updateFullNode(oldServerSnap.getNode(), newServerNode, null);
9021 }
9022 else {
9023 const childKey = pathGetFront(changePath);
9024 if (!oldServerSnap.isCompleteForPath(changePath) &&
9025 pathGetLength(changePath) > 1) {
9026 // We don't update incomplete nodes with updates intended for other listeners
9027 return oldViewCache;
9028 }
9029 const childChangePath = pathPopFront(changePath);
9030 const childNode = oldServerSnap.getNode().getImmediateChild(childKey);
9031 const newChildNode = childNode.updateChild(childChangePath, changedSnap);
9032 if (childKey === '.priority') {
9033 newServerCache = serverFilter.updatePriority(oldServerSnap.getNode(), newChildNode);
9034 }
9035 else {
9036 newServerCache = serverFilter.updateChild(oldServerSnap.getNode(), childKey, newChildNode, childChangePath, NO_COMPLETE_CHILD_SOURCE, null);
9037 }
9038 }
9039 const newViewCache = viewCacheUpdateServerSnap(oldViewCache, newServerCache, oldServerSnap.isFullyInitialized() || pathIsEmpty(changePath), serverFilter.filtersNodes());
9040 const source = new WriteTreeCompleteChildSource(writesCache, newViewCache, completeCache);
9041 return viewProcessorGenerateEventCacheAfterServerEvent(viewProcessor, newViewCache, changePath, writesCache, source, accumulator);
9042}
9043function viewProcessorApplyUserOverwrite(viewProcessor, oldViewCache, changePath, changedSnap, writesCache, completeCache, accumulator) {
9044 const oldEventSnap = oldViewCache.eventCache;
9045 let newViewCache, newEventCache;
9046 const source = new WriteTreeCompleteChildSource(writesCache, oldViewCache, completeCache);
9047 if (pathIsEmpty(changePath)) {
9048 newEventCache = viewProcessor.filter.updateFullNode(oldViewCache.eventCache.getNode(), changedSnap, accumulator);
9049 newViewCache = viewCacheUpdateEventSnap(oldViewCache, newEventCache, true, viewProcessor.filter.filtersNodes());
9050 }
9051 else {
9052 const childKey = pathGetFront(changePath);
9053 if (childKey === '.priority') {
9054 newEventCache = viewProcessor.filter.updatePriority(oldViewCache.eventCache.getNode(), changedSnap);
9055 newViewCache = viewCacheUpdateEventSnap(oldViewCache, newEventCache, oldEventSnap.isFullyInitialized(), oldEventSnap.isFiltered());
9056 }
9057 else {
9058 const childChangePath = pathPopFront(changePath);
9059 const oldChild = oldEventSnap.getNode().getImmediateChild(childKey);
9060 let newChild;
9061 if (pathIsEmpty(childChangePath)) {
9062 // Child overwrite, we can replace the child
9063 newChild = changedSnap;
9064 }
9065 else {
9066 const childNode = source.getCompleteChild(childKey);
9067 if (childNode != null) {
9068 if (pathGetBack(childChangePath) === '.priority' &&
9069 childNode.getChild(pathParent(childChangePath)).isEmpty()) {
9070 // This is a priority update on an empty node. If this node exists on the server, the
9071 // server will send down the priority in the update, so ignore for now
9072 newChild = childNode;
9073 }
9074 else {
9075 newChild = childNode.updateChild(childChangePath, changedSnap);
9076 }
9077 }
9078 else {
9079 // There is no complete child node available
9080 newChild = ChildrenNode.EMPTY_NODE;
9081 }
9082 }
9083 if (!oldChild.equals(newChild)) {
9084 const newEventSnap = viewProcessor.filter.updateChild(oldEventSnap.getNode(), childKey, newChild, childChangePath, source, accumulator);
9085 newViewCache = viewCacheUpdateEventSnap(oldViewCache, newEventSnap, oldEventSnap.isFullyInitialized(), viewProcessor.filter.filtersNodes());
9086 }
9087 else {
9088 newViewCache = oldViewCache;
9089 }
9090 }
9091 }
9092 return newViewCache;
9093}
9094function viewProcessorCacheHasChild(viewCache, childKey) {
9095 return viewCache.eventCache.isCompleteForChild(childKey);
9096}
9097function viewProcessorApplyUserMerge(viewProcessor, viewCache, path, changedChildren, writesCache, serverCache, accumulator) {
9098 // HACK: In the case of a limit query, there may be some changes that bump things out of the
9099 // window leaving room for new items. It's important we process these changes first, so we
9100 // iterate the changes twice, first processing any that affect items currently in view.
9101 // TODO: I consider an item "in view" if cacheHasChild is true, which checks both the server
9102 // and event snap. I'm not sure if this will result in edge cases when a child is in one but
9103 // not the other.
9104 let curViewCache = viewCache;
9105 changedChildren.foreach((relativePath, childNode) => {
9106 const writePath = pathChild(path, relativePath);
9107 if (viewProcessorCacheHasChild(viewCache, pathGetFront(writePath))) {
9108 curViewCache = viewProcessorApplyUserOverwrite(viewProcessor, curViewCache, writePath, childNode, writesCache, serverCache, accumulator);
9109 }
9110 });
9111 changedChildren.foreach((relativePath, childNode) => {
9112 const writePath = pathChild(path, relativePath);
9113 if (!viewProcessorCacheHasChild(viewCache, pathGetFront(writePath))) {
9114 curViewCache = viewProcessorApplyUserOverwrite(viewProcessor, curViewCache, writePath, childNode, writesCache, serverCache, accumulator);
9115 }
9116 });
9117 return curViewCache;
9118}
9119function viewProcessorApplyMerge(viewProcessor, node, merge) {
9120 merge.foreach((relativePath, childNode) => {
9121 node = node.updateChild(relativePath, childNode);
9122 });
9123 return node;
9124}
9125function viewProcessorApplyServerMerge(viewProcessor, viewCache, path, changedChildren, writesCache, serverCache, filterServerNode, accumulator) {
9126 // If we don't have a cache yet, this merge was intended for a previously listen in the same location. Ignore it and
9127 // wait for the complete data update coming soon.
9128 if (viewCache.serverCache.getNode().isEmpty() &&
9129 !viewCache.serverCache.isFullyInitialized()) {
9130 return viewCache;
9131 }
9132 // HACK: In the case of a limit query, there may be some changes that bump things out of the
9133 // window leaving room for new items. It's important we process these changes first, so we
9134 // iterate the changes twice, first processing any that affect items currently in view.
9135 // TODO: I consider an item "in view" if cacheHasChild is true, which checks both the server
9136 // and event snap. I'm not sure if this will result in edge cases when a child is in one but
9137 // not the other.
9138 let curViewCache = viewCache;
9139 let viewMergeTree;
9140 if (pathIsEmpty(path)) {
9141 viewMergeTree = changedChildren;
9142 }
9143 else {
9144 viewMergeTree = new ImmutableTree(null).setTree(path, changedChildren);
9145 }
9146 const serverNode = viewCache.serverCache.getNode();
9147 viewMergeTree.children.inorderTraversal((childKey, childTree) => {
9148 if (serverNode.hasChild(childKey)) {
9149 const serverChild = viewCache.serverCache
9150 .getNode()
9151 .getImmediateChild(childKey);
9152 const newChild = viewProcessorApplyMerge(viewProcessor, serverChild, childTree);
9153 curViewCache = viewProcessorApplyServerOverwrite(viewProcessor, curViewCache, new Path(childKey), newChild, writesCache, serverCache, filterServerNode, accumulator);
9154 }
9155 });
9156 viewMergeTree.children.inorderTraversal((childKey, childMergeTree) => {
9157 const isUnknownDeepMerge = !viewCache.serverCache.isCompleteForChild(childKey) &&
9158 childMergeTree.value === undefined;
9159 if (!serverNode.hasChild(childKey) && !isUnknownDeepMerge) {
9160 const serverChild = viewCache.serverCache
9161 .getNode()
9162 .getImmediateChild(childKey);
9163 const newChild = viewProcessorApplyMerge(viewProcessor, serverChild, childMergeTree);
9164 curViewCache = viewProcessorApplyServerOverwrite(viewProcessor, curViewCache, new Path(childKey), newChild, writesCache, serverCache, filterServerNode, accumulator);
9165 }
9166 });
9167 return curViewCache;
9168}
9169function viewProcessorAckUserWrite(viewProcessor, viewCache, ackPath, affectedTree, writesCache, completeCache, accumulator) {
9170 if (writeTreeRefShadowingWrite(writesCache, ackPath) != null) {
9171 return viewCache;
9172 }
9173 // Only filter server node if it is currently filtered
9174 const filterServerNode = viewCache.serverCache.isFiltered();
9175 // Essentially we'll just get our existing server cache for the affected paths and re-apply it as a server update
9176 // now that it won't be shadowed.
9177 const serverCache = viewCache.serverCache;
9178 if (affectedTree.value != null) {
9179 // This is an overwrite.
9180 if ((pathIsEmpty(ackPath) && serverCache.isFullyInitialized()) ||
9181 serverCache.isCompleteForPath(ackPath)) {
9182 return viewProcessorApplyServerOverwrite(viewProcessor, viewCache, ackPath, serverCache.getNode().getChild(ackPath), writesCache, completeCache, filterServerNode, accumulator);
9183 }
9184 else if (pathIsEmpty(ackPath)) {
9185 // This is a goofy edge case where we are acking data at this location but don't have full data. We
9186 // should just re-apply whatever we have in our cache as a merge.
9187 let changedChildren = new ImmutableTree(null);
9188 serverCache.getNode().forEachChild(KEY_INDEX, (name, node) => {
9189 changedChildren = changedChildren.set(new Path(name), node);
9190 });
9191 return viewProcessorApplyServerMerge(viewProcessor, viewCache, ackPath, changedChildren, writesCache, completeCache, filterServerNode, accumulator);
9192 }
9193 else {
9194 return viewCache;
9195 }
9196 }
9197 else {
9198 // This is a merge.
9199 let changedChildren = new ImmutableTree(null);
9200 affectedTree.foreach((mergePath, value) => {
9201 const serverCachePath = pathChild(ackPath, mergePath);
9202 if (serverCache.isCompleteForPath(serverCachePath)) {
9203 changedChildren = changedChildren.set(mergePath, serverCache.getNode().getChild(serverCachePath));
9204 }
9205 });
9206 return viewProcessorApplyServerMerge(viewProcessor, viewCache, ackPath, changedChildren, writesCache, completeCache, filterServerNode, accumulator);
9207 }
9208}
9209function viewProcessorListenComplete(viewProcessor, viewCache, path, writesCache, accumulator) {
9210 const oldServerNode = viewCache.serverCache;
9211 const newViewCache = viewCacheUpdateServerSnap(viewCache, oldServerNode.getNode(), oldServerNode.isFullyInitialized() || pathIsEmpty(path), oldServerNode.isFiltered());
9212 return viewProcessorGenerateEventCacheAfterServerEvent(viewProcessor, newViewCache, path, writesCache, NO_COMPLETE_CHILD_SOURCE, accumulator);
9213}
9214function viewProcessorRevertUserWrite(viewProcessor, viewCache, path, writesCache, completeServerCache, accumulator) {
9215 let complete;
9216 if (writeTreeRefShadowingWrite(writesCache, path) != null) {
9217 return viewCache;
9218 }
9219 else {
9220 const source = new WriteTreeCompleteChildSource(writesCache, viewCache, completeServerCache);
9221 const oldEventCache = viewCache.eventCache.getNode();
9222 let newEventCache;
9223 if (pathIsEmpty(path) || pathGetFront(path) === '.priority') {
9224 let newNode;
9225 if (viewCache.serverCache.isFullyInitialized()) {
9226 newNode = writeTreeRefCalcCompleteEventCache(writesCache, viewCacheGetCompleteServerSnap(viewCache));
9227 }
9228 else {
9229 const serverChildren = viewCache.serverCache.getNode();
9230 assert(serverChildren instanceof ChildrenNode, 'serverChildren would be complete if leaf node');
9231 newNode = writeTreeRefCalcCompleteEventChildren(writesCache, serverChildren);
9232 }
9233 newNode = newNode;
9234 newEventCache = viewProcessor.filter.updateFullNode(oldEventCache, newNode, accumulator);
9235 }
9236 else {
9237 const childKey = pathGetFront(path);
9238 let newChild = writeTreeRefCalcCompleteChild(writesCache, childKey, viewCache.serverCache);
9239 if (newChild == null &&
9240 viewCache.serverCache.isCompleteForChild(childKey)) {
9241 newChild = oldEventCache.getImmediateChild(childKey);
9242 }
9243 if (newChild != null) {
9244 newEventCache = viewProcessor.filter.updateChild(oldEventCache, childKey, newChild, pathPopFront(path), source, accumulator);
9245 }
9246 else if (viewCache.eventCache.getNode().hasChild(childKey)) {
9247 // No complete child available, delete the existing one, if any
9248 newEventCache = viewProcessor.filter.updateChild(oldEventCache, childKey, ChildrenNode.EMPTY_NODE, pathPopFront(path), source, accumulator);
9249 }
9250 else {
9251 newEventCache = oldEventCache;
9252 }
9253 if (newEventCache.isEmpty() &&
9254 viewCache.serverCache.isFullyInitialized()) {
9255 // We might have reverted all child writes. Maybe the old event was a leaf node
9256 complete = writeTreeRefCalcCompleteEventCache(writesCache, viewCacheGetCompleteServerSnap(viewCache));
9257 if (complete.isLeafNode()) {
9258 newEventCache = viewProcessor.filter.updateFullNode(newEventCache, complete, accumulator);
9259 }
9260 }
9261 }
9262 complete =
9263 viewCache.serverCache.isFullyInitialized() ||
9264 writeTreeRefShadowingWrite(writesCache, newEmptyPath()) != null;
9265 return viewCacheUpdateEventSnap(viewCache, newEventCache, complete, viewProcessor.filter.filtersNodes());
9266 }
9267}
9268
9269/**
9270 * @license
9271 * Copyright 2017 Google LLC
9272 *
9273 * Licensed under the Apache License, Version 2.0 (the "License");
9274 * you may not use this file except in compliance with the License.
9275 * You may obtain a copy of the License at
9276 *
9277 * http://www.apache.org/licenses/LICENSE-2.0
9278 *
9279 * Unless required by applicable law or agreed to in writing, software
9280 * distributed under the License is distributed on an "AS IS" BASIS,
9281 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9282 * See the License for the specific language governing permissions and
9283 * limitations under the License.
9284 */
9285/**
9286 * A view represents a specific location and query that has 1 or more event registrations.
9287 *
9288 * It does several things:
9289 * - Maintains the list of event registrations for this location/query.
9290 * - Maintains a cache of the data visible for this location/query.
9291 * - Applies new operations (via applyOperation), updates the cache, and based on the event
9292 * registrations returns the set of events to be raised.
9293 */
9294class View {
9295 constructor(query_, initialViewCache) {
9296 this.query_ = query_;
9297 this.eventRegistrations_ = [];
9298 const params = this.query_._queryParams;
9299 const indexFilter = new IndexedFilter(params.getIndex());
9300 const filter = queryParamsGetNodeFilter(params);
9301 this.processor_ = newViewProcessor(filter);
9302 const initialServerCache = initialViewCache.serverCache;
9303 const initialEventCache = initialViewCache.eventCache;
9304 // Don't filter server node with other filter than index, wait for tagged listen
9305 const serverSnap = indexFilter.updateFullNode(ChildrenNode.EMPTY_NODE, initialServerCache.getNode(), null);
9306 const eventSnap = filter.updateFullNode(ChildrenNode.EMPTY_NODE, initialEventCache.getNode(), null);
9307 const newServerCache = new CacheNode(serverSnap, initialServerCache.isFullyInitialized(), indexFilter.filtersNodes());
9308 const newEventCache = new CacheNode(eventSnap, initialEventCache.isFullyInitialized(), filter.filtersNodes());
9309 this.viewCache_ = newViewCache(newEventCache, newServerCache);
9310 this.eventGenerator_ = new EventGenerator(this.query_);
9311 }
9312 get query() {
9313 return this.query_;
9314 }
9315}
9316function viewGetServerCache(view) {
9317 return view.viewCache_.serverCache.getNode();
9318}
9319function viewGetCompleteNode(view) {
9320 return viewCacheGetCompleteEventSnap(view.viewCache_);
9321}
9322function viewGetCompleteServerCache(view, path) {
9323 const cache = viewCacheGetCompleteServerSnap(view.viewCache_);
9324 if (cache) {
9325 // If this isn't a "loadsAllData" view, then cache isn't actually a complete cache and
9326 // we need to see if it contains the child we're interested in.
9327 if (view.query._queryParams.loadsAllData() ||
9328 (!pathIsEmpty(path) &&
9329 !cache.getImmediateChild(pathGetFront(path)).isEmpty())) {
9330 return cache.getChild(path);
9331 }
9332 }
9333 return null;
9334}
9335function viewIsEmpty(view) {
9336 return view.eventRegistrations_.length === 0;
9337}
9338function viewAddEventRegistration(view, eventRegistration) {
9339 view.eventRegistrations_.push(eventRegistration);
9340}
9341/**
9342 * @param eventRegistration - If null, remove all callbacks.
9343 * @param cancelError - If a cancelError is provided, appropriate cancel events will be returned.
9344 * @returns Cancel events, if cancelError was provided.
9345 */
9346function viewRemoveEventRegistration(view, eventRegistration, cancelError) {
9347 const cancelEvents = [];
9348 if (cancelError) {
9349 assert(eventRegistration == null, 'A cancel should cancel all event registrations.');
9350 const path = view.query._path;
9351 view.eventRegistrations_.forEach(registration => {
9352 const maybeEvent = registration.createCancelEvent(cancelError, path);
9353 if (maybeEvent) {
9354 cancelEvents.push(maybeEvent);
9355 }
9356 });
9357 }
9358 if (eventRegistration) {
9359 let remaining = [];
9360 for (let i = 0; i < view.eventRegistrations_.length; ++i) {
9361 const existing = view.eventRegistrations_[i];
9362 if (!existing.matches(eventRegistration)) {
9363 remaining.push(existing);
9364 }
9365 else if (eventRegistration.hasAnyCallback()) {
9366 // We're removing just this one
9367 remaining = remaining.concat(view.eventRegistrations_.slice(i + 1));
9368 break;
9369 }
9370 }
9371 view.eventRegistrations_ = remaining;
9372 }
9373 else {
9374 view.eventRegistrations_ = [];
9375 }
9376 return cancelEvents;
9377}
9378/**
9379 * Applies the given Operation, updates our cache, and returns the appropriate events.
9380 */
9381function viewApplyOperation(view, operation, writesCache, completeServerCache) {
9382 if (operation.type === OperationType.MERGE &&
9383 operation.source.queryId !== null) {
9384 assert(viewCacheGetCompleteServerSnap(view.viewCache_), 'We should always have a full cache before handling merges');
9385 assert(viewCacheGetCompleteEventSnap(view.viewCache_), 'Missing event cache, even though we have a server cache');
9386 }
9387 const oldViewCache = view.viewCache_;
9388 const result = viewProcessorApplyOperation(view.processor_, oldViewCache, operation, writesCache, completeServerCache);
9389 viewProcessorAssertIndexed(view.processor_, result.viewCache);
9390 assert(result.viewCache.serverCache.isFullyInitialized() ||
9391 !oldViewCache.serverCache.isFullyInitialized(), 'Once a server snap is complete, it should never go back');
9392 view.viewCache_ = result.viewCache;
9393 return viewGenerateEventsForChanges_(view, result.changes, result.viewCache.eventCache.getNode(), null);
9394}
9395function viewGetInitialEvents(view, registration) {
9396 const eventSnap = view.viewCache_.eventCache;
9397 const initialChanges = [];
9398 if (!eventSnap.getNode().isLeafNode()) {
9399 const eventNode = eventSnap.getNode();
9400 eventNode.forEachChild(PRIORITY_INDEX, (key, childNode) => {
9401 initialChanges.push(changeChildAdded(key, childNode));
9402 });
9403 }
9404 if (eventSnap.isFullyInitialized()) {
9405 initialChanges.push(changeValue(eventSnap.getNode()));
9406 }
9407 return viewGenerateEventsForChanges_(view, initialChanges, eventSnap.getNode(), registration);
9408}
9409function viewGenerateEventsForChanges_(view, changes, eventCache, eventRegistration) {
9410 const registrations = eventRegistration
9411 ? [eventRegistration]
9412 : view.eventRegistrations_;
9413 return eventGeneratorGenerateEventsForChanges(view.eventGenerator_, changes, eventCache, registrations);
9414}
9415
9416/**
9417 * @license
9418 * Copyright 2017 Google LLC
9419 *
9420 * Licensed under the Apache License, Version 2.0 (the "License");
9421 * you may not use this file except in compliance with the License.
9422 * You may obtain a copy of the License at
9423 *
9424 * http://www.apache.org/licenses/LICENSE-2.0
9425 *
9426 * Unless required by applicable law or agreed to in writing, software
9427 * distributed under the License is distributed on an "AS IS" BASIS,
9428 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9429 * See the License for the specific language governing permissions and
9430 * limitations under the License.
9431 */
9432let referenceConstructor$1;
9433/**
9434 * SyncPoint represents a single location in a SyncTree with 1 or more event registrations, meaning we need to
9435 * maintain 1 or more Views at this location to cache server data and raise appropriate events for server changes
9436 * and user writes (set, transaction, update).
9437 *
9438 * It's responsible for:
9439 * - Maintaining the set of 1 or more views necessary at this location (a SyncPoint with 0 views should be removed).
9440 * - Proxying user / server operations to the views as appropriate (i.e. applyServerOverwrite,
9441 * applyUserOverwrite, etc.)
9442 */
9443class SyncPoint {
9444 constructor() {
9445 /**
9446 * The Views being tracked at this location in the tree, stored as a map where the key is a
9447 * queryId and the value is the View for that query.
9448 *
9449 * NOTE: This list will be quite small (usually 1, but perhaps 2 or 3; any more is an odd use case).
9450 */
9451 this.views = new Map();
9452 }
9453}
9454function syncPointSetReferenceConstructor(val) {
9455 assert(!referenceConstructor$1, '__referenceConstructor has already been defined');
9456 referenceConstructor$1 = val;
9457}
9458function syncPointGetReferenceConstructor() {
9459 assert(referenceConstructor$1, 'Reference.ts has not been loaded');
9460 return referenceConstructor$1;
9461}
9462function syncPointIsEmpty(syncPoint) {
9463 return syncPoint.views.size === 0;
9464}
9465function syncPointApplyOperation(syncPoint, operation, writesCache, optCompleteServerCache) {
9466 const queryId = operation.source.queryId;
9467 if (queryId !== null) {
9468 const view = syncPoint.views.get(queryId);
9469 assert(view != null, 'SyncTree gave us an op for an invalid query.');
9470 return viewApplyOperation(view, operation, writesCache, optCompleteServerCache);
9471 }
9472 else {
9473 let events = [];
9474 for (const view of syncPoint.views.values()) {
9475 events = events.concat(viewApplyOperation(view, operation, writesCache, optCompleteServerCache));
9476 }
9477 return events;
9478 }
9479}
9480/**
9481 * Get a view for the specified query.
9482 *
9483 * @param query - The query to return a view for
9484 * @param writesCache
9485 * @param serverCache
9486 * @param serverCacheComplete
9487 * @returns Events to raise.
9488 */
9489function syncPointGetView(syncPoint, query, writesCache, serverCache, serverCacheComplete) {
9490 const queryId = query._queryIdentifier;
9491 const view = syncPoint.views.get(queryId);
9492 if (!view) {
9493 // TODO: make writesCache take flag for complete server node
9494 let eventCache = writeTreeRefCalcCompleteEventCache(writesCache, serverCacheComplete ? serverCache : null);
9495 let eventCacheComplete = false;
9496 if (eventCache) {
9497 eventCacheComplete = true;
9498 }
9499 else if (serverCache instanceof ChildrenNode) {
9500 eventCache = writeTreeRefCalcCompleteEventChildren(writesCache, serverCache);
9501 eventCacheComplete = false;
9502 }
9503 else {
9504 eventCache = ChildrenNode.EMPTY_NODE;
9505 eventCacheComplete = false;
9506 }
9507 const viewCache = newViewCache(new CacheNode(eventCache, eventCacheComplete, false), new CacheNode(serverCache, serverCacheComplete, false));
9508 return new View(query, viewCache);
9509 }
9510 return view;
9511}
9512/**
9513 * Add an event callback for the specified query.
9514 *
9515 * @param query
9516 * @param eventRegistration
9517 * @param writesCache
9518 * @param serverCache - Complete server cache, if we have it.
9519 * @param serverCacheComplete
9520 * @returns Events to raise.
9521 */
9522function syncPointAddEventRegistration(syncPoint, query, eventRegistration, writesCache, serverCache, serverCacheComplete) {
9523 const view = syncPointGetView(syncPoint, query, writesCache, serverCache, serverCacheComplete);
9524 if (!syncPoint.views.has(query._queryIdentifier)) {
9525 syncPoint.views.set(query._queryIdentifier, view);
9526 }
9527 // This is guaranteed to exist now, we just created anything that was missing
9528 viewAddEventRegistration(view, eventRegistration);
9529 return viewGetInitialEvents(view, eventRegistration);
9530}
9531/**
9532 * Remove event callback(s). Return cancelEvents if a cancelError is specified.
9533 *
9534 * If query is the default query, we'll check all views for the specified eventRegistration.
9535 * If eventRegistration is null, we'll remove all callbacks for the specified view(s).
9536 *
9537 * @param eventRegistration - If null, remove all callbacks.
9538 * @param cancelError - If a cancelError is provided, appropriate cancel events will be returned.
9539 * @returns removed queries and any cancel events
9540 */
9541function syncPointRemoveEventRegistration(syncPoint, query, eventRegistration, cancelError) {
9542 const queryId = query._queryIdentifier;
9543 const removed = [];
9544 let cancelEvents = [];
9545 const hadCompleteView = syncPointHasCompleteView(syncPoint);
9546 if (queryId === 'default') {
9547 // When you do ref.off(...), we search all views for the registration to remove.
9548 for (const [viewQueryId, view] of syncPoint.views.entries()) {
9549 cancelEvents = cancelEvents.concat(viewRemoveEventRegistration(view, eventRegistration, cancelError));
9550 if (viewIsEmpty(view)) {
9551 syncPoint.views.delete(viewQueryId);
9552 // We'll deal with complete views later.
9553 if (!view.query._queryParams.loadsAllData()) {
9554 removed.push(view.query);
9555 }
9556 }
9557 }
9558 }
9559 else {
9560 // remove the callback from the specific view.
9561 const view = syncPoint.views.get(queryId);
9562 if (view) {
9563 cancelEvents = cancelEvents.concat(viewRemoveEventRegistration(view, eventRegistration, cancelError));
9564 if (viewIsEmpty(view)) {
9565 syncPoint.views.delete(queryId);
9566 // We'll deal with complete views later.
9567 if (!view.query._queryParams.loadsAllData()) {
9568 removed.push(view.query);
9569 }
9570 }
9571 }
9572 }
9573 if (hadCompleteView && !syncPointHasCompleteView(syncPoint)) {
9574 // We removed our last complete view.
9575 removed.push(new (syncPointGetReferenceConstructor())(query._repo, query._path));
9576 }
9577 return { removed, events: cancelEvents };
9578}
9579function syncPointGetQueryViews(syncPoint) {
9580 const result = [];
9581 for (const view of syncPoint.views.values()) {
9582 if (!view.query._queryParams.loadsAllData()) {
9583 result.push(view);
9584 }
9585 }
9586 return result;
9587}
9588/**
9589 * @param path - The path to the desired complete snapshot
9590 * @returns A complete cache, if it exists
9591 */
9592function syncPointGetCompleteServerCache(syncPoint, path) {
9593 let serverCache = null;
9594 for (const view of syncPoint.views.values()) {
9595 serverCache = serverCache || viewGetCompleteServerCache(view, path);
9596 }
9597 return serverCache;
9598}
9599function syncPointViewForQuery(syncPoint, query) {
9600 const params = query._queryParams;
9601 if (params.loadsAllData()) {
9602 return syncPointGetCompleteView(syncPoint);
9603 }
9604 else {
9605 const queryId = query._queryIdentifier;
9606 return syncPoint.views.get(queryId);
9607 }
9608}
9609function syncPointViewExistsForQuery(syncPoint, query) {
9610 return syncPointViewForQuery(syncPoint, query) != null;
9611}
9612function syncPointHasCompleteView(syncPoint) {
9613 return syncPointGetCompleteView(syncPoint) != null;
9614}
9615function syncPointGetCompleteView(syncPoint) {
9616 for (const view of syncPoint.views.values()) {
9617 if (view.query._queryParams.loadsAllData()) {
9618 return view;
9619 }
9620 }
9621 return null;
9622}
9623
9624/**
9625 * @license
9626 * Copyright 2017 Google LLC
9627 *
9628 * Licensed under the Apache License, Version 2.0 (the "License");
9629 * you may not use this file except in compliance with the License.
9630 * You may obtain a copy of the License at
9631 *
9632 * http://www.apache.org/licenses/LICENSE-2.0
9633 *
9634 * Unless required by applicable law or agreed to in writing, software
9635 * distributed under the License is distributed on an "AS IS" BASIS,
9636 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9637 * See the License for the specific language governing permissions and
9638 * limitations under the License.
9639 */
9640let referenceConstructor;
9641function syncTreeSetReferenceConstructor(val) {
9642 assert(!referenceConstructor, '__referenceConstructor has already been defined');
9643 referenceConstructor = val;
9644}
9645function syncTreeGetReferenceConstructor() {
9646 assert(referenceConstructor, 'Reference.ts has not been loaded');
9647 return referenceConstructor;
9648}
9649/**
9650 * Static tracker for next query tag.
9651 */
9652let syncTreeNextQueryTag_ = 1;
9653/**
9654 * SyncTree is the central class for managing event callback registration, data caching, views
9655 * (query processing), and event generation. There are typically two SyncTree instances for
9656 * each Repo, one for the normal Firebase data, and one for the .info data.
9657 *
9658 * It has a number of responsibilities, including:
9659 * - Tracking all user event callbacks (registered via addEventRegistration() and removeEventRegistration()).
9660 * - Applying and caching data changes for user set(), transaction(), and update() calls
9661 * (applyUserOverwrite(), applyUserMerge()).
9662 * - Applying and caching data changes for server data changes (applyServerOverwrite(),
9663 * applyServerMerge()).
9664 * - Generating user-facing events for server and user changes (all of the apply* methods
9665 * return the set of events that need to be raised as a result).
9666 * - Maintaining the appropriate set of server listens to ensure we are always subscribed
9667 * to the correct set of paths and queries to satisfy the current set of user event
9668 * callbacks (listens are started/stopped using the provided listenProvider).
9669 *
9670 * NOTE: Although SyncTree tracks event callbacks and calculates events to raise, the actual
9671 * events are returned to the caller rather than raised synchronously.
9672 *
9673 */
9674class SyncTree {
9675 /**
9676 * @param listenProvider_ - Used by SyncTree to start / stop listening
9677 * to server data.
9678 */
9679 constructor(listenProvider_) {
9680 this.listenProvider_ = listenProvider_;
9681 /**
9682 * Tree of SyncPoints. There's a SyncPoint at any location that has 1 or more views.
9683 */
9684 this.syncPointTree_ = new ImmutableTree(null);
9685 /**
9686 * A tree of all pending user writes (user-initiated set()'s, transaction()'s, update()'s, etc.).
9687 */
9688 this.pendingWriteTree_ = newWriteTree();
9689 this.tagToQueryMap = new Map();
9690 this.queryToTagMap = new Map();
9691 }
9692}
9693/**
9694 * Apply the data changes for a user-generated set() or transaction() call.
9695 *
9696 * @returns Events to raise.
9697 */
9698function syncTreeApplyUserOverwrite(syncTree, path, newData, writeId, visible) {
9699 // Record pending write.
9700 writeTreeAddOverwrite(syncTree.pendingWriteTree_, path, newData, writeId, visible);
9701 if (!visible) {
9702 return [];
9703 }
9704 else {
9705 return syncTreeApplyOperationToSyncPoints_(syncTree, new Overwrite(newOperationSourceUser(), path, newData));
9706 }
9707}
9708/**
9709 * Apply the data from a user-generated update() call
9710 *
9711 * @returns Events to raise.
9712 */
9713function syncTreeApplyUserMerge(syncTree, path, changedChildren, writeId) {
9714 // Record pending merge.
9715 writeTreeAddMerge(syncTree.pendingWriteTree_, path, changedChildren, writeId);
9716 const changeTree = ImmutableTree.fromObject(changedChildren);
9717 return syncTreeApplyOperationToSyncPoints_(syncTree, new Merge(newOperationSourceUser(), path, changeTree));
9718}
9719/**
9720 * Acknowledge a pending user write that was previously registered with applyUserOverwrite() or applyUserMerge().
9721 *
9722 * @param revert - True if the given write failed and needs to be reverted
9723 * @returns Events to raise.
9724 */
9725function syncTreeAckUserWrite(syncTree, writeId, revert = false) {
9726 const write = writeTreeGetWrite(syncTree.pendingWriteTree_, writeId);
9727 const needToReevaluate = writeTreeRemoveWrite(syncTree.pendingWriteTree_, writeId);
9728 if (!needToReevaluate) {
9729 return [];
9730 }
9731 else {
9732 let affectedTree = new ImmutableTree(null);
9733 if (write.snap != null) {
9734 // overwrite
9735 affectedTree = affectedTree.set(newEmptyPath(), true);
9736 }
9737 else {
9738 each(write.children, (pathString) => {
9739 affectedTree = affectedTree.set(new Path(pathString), true);
9740 });
9741 }
9742 return syncTreeApplyOperationToSyncPoints_(syncTree, new AckUserWrite(write.path, affectedTree, revert));
9743 }
9744}
9745/**
9746 * Apply new server data for the specified path..
9747 *
9748 * @returns Events to raise.
9749 */
9750function syncTreeApplyServerOverwrite(syncTree, path, newData) {
9751 return syncTreeApplyOperationToSyncPoints_(syncTree, new Overwrite(newOperationSourceServer(), path, newData));
9752}
9753/**
9754 * Apply new server data to be merged in at the specified path.
9755 *
9756 * @returns Events to raise.
9757 */
9758function syncTreeApplyServerMerge(syncTree, path, changedChildren) {
9759 const changeTree = ImmutableTree.fromObject(changedChildren);
9760 return syncTreeApplyOperationToSyncPoints_(syncTree, new Merge(newOperationSourceServer(), path, changeTree));
9761}
9762/**
9763 * Apply a listen complete for a query
9764 *
9765 * @returns Events to raise.
9766 */
9767function syncTreeApplyListenComplete(syncTree, path) {
9768 return syncTreeApplyOperationToSyncPoints_(syncTree, new ListenComplete(newOperationSourceServer(), path));
9769}
9770/**
9771 * Apply a listen complete for a tagged query
9772 *
9773 * @returns Events to raise.
9774 */
9775function syncTreeApplyTaggedListenComplete(syncTree, path, tag) {
9776 const queryKey = syncTreeQueryKeyForTag_(syncTree, tag);
9777 if (queryKey) {
9778 const r = syncTreeParseQueryKey_(queryKey);
9779 const queryPath = r.path, queryId = r.queryId;
9780 const relativePath = newRelativePath(queryPath, path);
9781 const op = new ListenComplete(newOperationSourceServerTaggedQuery(queryId), relativePath);
9782 return syncTreeApplyTaggedOperation_(syncTree, queryPath, op);
9783 }
9784 else {
9785 // We've already removed the query. No big deal, ignore the update
9786 return [];
9787 }
9788}
9789/**
9790 * Remove event callback(s).
9791 *
9792 * If query is the default query, we'll check all queries for the specified eventRegistration.
9793 * If eventRegistration is null, we'll remove all callbacks for the specified query/queries.
9794 *
9795 * @param eventRegistration - If null, all callbacks are removed.
9796 * @param cancelError - If a cancelError is provided, appropriate cancel events will be returned.
9797 * @returns Cancel events, if cancelError was provided.
9798 */
9799function syncTreeRemoveEventRegistration(syncTree, query, eventRegistration, cancelError) {
9800 // Find the syncPoint first. Then deal with whether or not it has matching listeners
9801 const path = query._path;
9802 const maybeSyncPoint = syncTree.syncPointTree_.get(path);
9803 let cancelEvents = [];
9804 // A removal on a default query affects all queries at that location. A removal on an indexed query, even one without
9805 // other query constraints, does *not* affect all queries at that location. So this check must be for 'default', and
9806 // not loadsAllData().
9807 if (maybeSyncPoint &&
9808 (query._queryIdentifier === 'default' ||
9809 syncPointViewExistsForQuery(maybeSyncPoint, query))) {
9810 const removedAndEvents = syncPointRemoveEventRegistration(maybeSyncPoint, query, eventRegistration, cancelError);
9811 if (syncPointIsEmpty(maybeSyncPoint)) {
9812 syncTree.syncPointTree_ = syncTree.syncPointTree_.remove(path);
9813 }
9814 const removed = removedAndEvents.removed;
9815 cancelEvents = removedAndEvents.events;
9816 // We may have just removed one of many listeners and can short-circuit this whole process
9817 // We may also not have removed a default listener, in which case all of the descendant listeners should already be
9818 // properly set up.
9819 //
9820 // Since indexed queries can shadow if they don't have other query constraints, check for loadsAllData(), instead of
9821 // queryId === 'default'
9822 const removingDefault = -1 !==
9823 removed.findIndex(query => {
9824 return query._queryParams.loadsAllData();
9825 });
9826 const covered = syncTree.syncPointTree_.findOnPath(path, (relativePath, parentSyncPoint) => syncPointHasCompleteView(parentSyncPoint));
9827 if (removingDefault && !covered) {
9828 const subtree = syncTree.syncPointTree_.subtree(path);
9829 // There are potentially child listeners. Determine what if any listens we need to send before executing the
9830 // removal
9831 if (!subtree.isEmpty()) {
9832 // We need to fold over our subtree and collect the listeners to send
9833 const newViews = syncTreeCollectDistinctViewsForSubTree_(subtree);
9834 // Ok, we've collected all the listens we need. Set them up.
9835 for (let i = 0; i < newViews.length; ++i) {
9836 const view = newViews[i], newQuery = view.query;
9837 const listener = syncTreeCreateListenerForView_(syncTree, view);
9838 syncTree.listenProvider_.startListening(syncTreeQueryForListening_(newQuery), syncTreeTagForQuery_(syncTree, newQuery), listener.hashFn, listener.onComplete);
9839 }
9840 }
9841 }
9842 // If we removed anything and we're not covered by a higher up listen, we need to stop listening on this query
9843 // The above block has us covered in terms of making sure we're set up on listens lower in the tree.
9844 // Also, note that if we have a cancelError, it's already been removed at the provider level.
9845 if (!covered && removed.length > 0 && !cancelError) {
9846 // If we removed a default, then we weren't listening on any of the other queries here. Just cancel the one
9847 // default. Otherwise, we need to iterate through and cancel each individual query
9848 if (removingDefault) {
9849 // We don't tag default listeners
9850 const defaultTag = null;
9851 syncTree.listenProvider_.stopListening(syncTreeQueryForListening_(query), defaultTag);
9852 }
9853 else {
9854 removed.forEach((queryToRemove) => {
9855 const tagToRemove = syncTree.queryToTagMap.get(syncTreeMakeQueryKey_(queryToRemove));
9856 syncTree.listenProvider_.stopListening(syncTreeQueryForListening_(queryToRemove), tagToRemove);
9857 });
9858 }
9859 }
9860 // Now, clear all of the tags we're tracking for the removed listens
9861 syncTreeRemoveTags_(syncTree, removed);
9862 }
9863 return cancelEvents;
9864}
9865/**
9866 * This function was added to support non-listener queries,
9867 * specifically for use in repoGetValue. It sets up all the same
9868 * local cache data-structures (SyncPoint + View) that are
9869 * needed for listeners without installing an event registration.
9870 * If `query` is not `loadsAllData`, it will also provision a tag for
9871 * the query so that query results can be merged into the sync
9872 * tree using existing logic for tagged listener queries.
9873 *
9874 * @param syncTree - Synctree to add the query to.
9875 * @param query - Query to register
9876 * @returns tag as a string if query is not a default query, null if query is not.
9877 */
9878function syncTreeRegisterQuery(syncTree, query) {
9879 const { syncPoint, serverCache, writesCache, serverCacheComplete } = syncTreeRegisterSyncPoint(query, syncTree);
9880 const view = syncPointGetView(syncPoint, query, writesCache, serverCache, serverCacheComplete);
9881 if (!syncPoint.views.has(query._queryIdentifier)) {
9882 syncPoint.views.set(query._queryIdentifier, view);
9883 }
9884 if (!query._queryParams.loadsAllData()) {
9885 return syncTreeTagForQuery_(syncTree, query);
9886 }
9887 return null;
9888}
9889/**
9890 * Apply new server data for the specified tagged query.
9891 *
9892 * @returns Events to raise.
9893 */
9894function syncTreeApplyTaggedQueryOverwrite(syncTree, path, snap, tag) {
9895 const queryKey = syncTreeQueryKeyForTag_(syncTree, tag);
9896 if (queryKey != null) {
9897 const r = syncTreeParseQueryKey_(queryKey);
9898 const queryPath = r.path, queryId = r.queryId;
9899 const relativePath = newRelativePath(queryPath, path);
9900 const op = new Overwrite(newOperationSourceServerTaggedQuery(queryId), relativePath, snap);
9901 return syncTreeApplyTaggedOperation_(syncTree, queryPath, op);
9902 }
9903 else {
9904 // Query must have been removed already
9905 return [];
9906 }
9907}
9908/**
9909 * Apply server data to be merged in for the specified tagged query.
9910 *
9911 * @returns Events to raise.
9912 */
9913function syncTreeApplyTaggedQueryMerge(syncTree, path, changedChildren, tag) {
9914 const queryKey = syncTreeQueryKeyForTag_(syncTree, tag);
9915 if (queryKey) {
9916 const r = syncTreeParseQueryKey_(queryKey);
9917 const queryPath = r.path, queryId = r.queryId;
9918 const relativePath = newRelativePath(queryPath, path);
9919 const changeTree = ImmutableTree.fromObject(changedChildren);
9920 const op = new Merge(newOperationSourceServerTaggedQuery(queryId), relativePath, changeTree);
9921 return syncTreeApplyTaggedOperation_(syncTree, queryPath, op);
9922 }
9923 else {
9924 // We've already removed the query. No big deal, ignore the update
9925 return [];
9926 }
9927}
9928/**
9929 * Creates a new syncpoint for a query and creates a tag if the view doesn't exist.
9930 * Extracted from addEventRegistration to allow `repoGetValue` to properly set up the SyncTree
9931 * without actually listening on a query.
9932 */
9933function syncTreeRegisterSyncPoint(query, syncTree) {
9934 const path = query._path;
9935 let serverCache = null;
9936 let foundAncestorDefaultView = false;
9937 // Any covering writes will necessarily be at the root, so really all we need to find is the server cache.
9938 // Consider optimizing this once there's a better understanding of what actual behavior will be.
9939 syncTree.syncPointTree_.foreachOnPath(path, (pathToSyncPoint, sp) => {
9940 const relativePath = newRelativePath(pathToSyncPoint, path);
9941 serverCache =
9942 serverCache || syncPointGetCompleteServerCache(sp, relativePath);
9943 foundAncestorDefaultView =
9944 foundAncestorDefaultView || syncPointHasCompleteView(sp);
9945 });
9946 let syncPoint = syncTree.syncPointTree_.get(path);
9947 if (!syncPoint) {
9948 syncPoint = new SyncPoint();
9949 syncTree.syncPointTree_ = syncTree.syncPointTree_.set(path, syncPoint);
9950 }
9951 else {
9952 foundAncestorDefaultView =
9953 foundAncestorDefaultView || syncPointHasCompleteView(syncPoint);
9954 serverCache =
9955 serverCache || syncPointGetCompleteServerCache(syncPoint, newEmptyPath());
9956 }
9957 let serverCacheComplete;
9958 if (serverCache != null) {
9959 serverCacheComplete = true;
9960 }
9961 else {
9962 serverCacheComplete = false;
9963 serverCache = ChildrenNode.EMPTY_NODE;
9964 const subtree = syncTree.syncPointTree_.subtree(path);
9965 subtree.foreachChild((childName, childSyncPoint) => {
9966 const completeCache = syncPointGetCompleteServerCache(childSyncPoint, newEmptyPath());
9967 if (completeCache) {
9968 serverCache = serverCache.updateImmediateChild(childName, completeCache);
9969 }
9970 });
9971 }
9972 const viewAlreadyExists = syncPointViewExistsForQuery(syncPoint, query);
9973 if (!viewAlreadyExists && !query._queryParams.loadsAllData()) {
9974 // We need to track a tag for this query
9975 const queryKey = syncTreeMakeQueryKey_(query);
9976 assert(!syncTree.queryToTagMap.has(queryKey), 'View does not exist, but we have a tag');
9977 const tag = syncTreeGetNextQueryTag_();
9978 syncTree.queryToTagMap.set(queryKey, tag);
9979 syncTree.tagToQueryMap.set(tag, queryKey);
9980 }
9981 const writesCache = writeTreeChildWrites(syncTree.pendingWriteTree_, path);
9982 return {
9983 syncPoint,
9984 writesCache,
9985 serverCache,
9986 serverCacheComplete,
9987 foundAncestorDefaultView,
9988 viewAlreadyExists
9989 };
9990}
9991/**
9992 * Add an event callback for the specified query.
9993 *
9994 * @returns Events to raise.
9995 */
9996function syncTreeAddEventRegistration(syncTree, query, eventRegistration) {
9997 const { syncPoint, serverCache, writesCache, serverCacheComplete, viewAlreadyExists, foundAncestorDefaultView } = syncTreeRegisterSyncPoint(query, syncTree);
9998 let events = syncPointAddEventRegistration(syncPoint, query, eventRegistration, writesCache, serverCache, serverCacheComplete);
9999 if (!viewAlreadyExists && !foundAncestorDefaultView) {
10000 const view = syncPointViewForQuery(syncPoint, query);
10001 events = events.concat(syncTreeSetupListener_(syncTree, query, view));
10002 }
10003 return events;
10004}
10005/**
10006 * Returns a complete cache, if we have one, of the data at a particular path. If the location does not have a
10007 * listener above it, we will get a false "null". This shouldn't be a problem because transactions will always
10008 * have a listener above, and atomic operations would correctly show a jitter of <increment value> ->
10009 * <incremented total> as the write is applied locally and then acknowledged at the server.
10010 *
10011 * Note: this method will *include* hidden writes from transaction with applyLocally set to false.
10012 *
10013 * @param path - The path to the data we want
10014 * @param writeIdsToExclude - A specific set to be excluded
10015 */
10016function syncTreeCalcCompleteEventCache(syncTree, path, writeIdsToExclude) {
10017 const includeHiddenSets = true;
10018 const writeTree = syncTree.pendingWriteTree_;
10019 const serverCache = syncTree.syncPointTree_.findOnPath(path, (pathSoFar, syncPoint) => {
10020 const relativePath = newRelativePath(pathSoFar, path);
10021 const serverCache = syncPointGetCompleteServerCache(syncPoint, relativePath);
10022 if (serverCache) {
10023 return serverCache;
10024 }
10025 });
10026 return writeTreeCalcCompleteEventCache(writeTree, path, serverCache, writeIdsToExclude, includeHiddenSets);
10027}
10028function syncTreeGetServerValue(syncTree, query) {
10029 const path = query._path;
10030 let serverCache = null;
10031 // Any covering writes will necessarily be at the root, so really all we need to find is the server cache.
10032 // Consider optimizing this once there's a better understanding of what actual behavior will be.
10033 syncTree.syncPointTree_.foreachOnPath(path, (pathToSyncPoint, sp) => {
10034 const relativePath = newRelativePath(pathToSyncPoint, path);
10035 serverCache =
10036 serverCache || syncPointGetCompleteServerCache(sp, relativePath);
10037 });
10038 let syncPoint = syncTree.syncPointTree_.get(path);
10039 if (!syncPoint) {
10040 syncPoint = new SyncPoint();
10041 syncTree.syncPointTree_ = syncTree.syncPointTree_.set(path, syncPoint);
10042 }
10043 else {
10044 serverCache =
10045 serverCache || syncPointGetCompleteServerCache(syncPoint, newEmptyPath());
10046 }
10047 const serverCacheComplete = serverCache != null;
10048 const serverCacheNode = serverCacheComplete
10049 ? new CacheNode(serverCache, true, false)
10050 : null;
10051 const writesCache = writeTreeChildWrites(syncTree.pendingWriteTree_, query._path);
10052 const view = syncPointGetView(syncPoint, query, writesCache, serverCacheComplete ? serverCacheNode.getNode() : ChildrenNode.EMPTY_NODE, serverCacheComplete);
10053 return viewGetCompleteNode(view);
10054}
10055/**
10056 * A helper method that visits all descendant and ancestor SyncPoints, applying the operation.
10057 *
10058 * NOTES:
10059 * - Descendant SyncPoints will be visited first (since we raise events depth-first).
10060 *
10061 * - We call applyOperation() on each SyncPoint passing three things:
10062 * 1. A version of the Operation that has been made relative to the SyncPoint location.
10063 * 2. A WriteTreeRef of any writes we have cached at the SyncPoint location.
10064 * 3. A snapshot Node with cached server data, if we have it.
10065 *
10066 * - We concatenate all of the events returned by each SyncPoint and return the result.
10067 */
10068function syncTreeApplyOperationToSyncPoints_(syncTree, operation) {
10069 return syncTreeApplyOperationHelper_(operation, syncTree.syncPointTree_,
10070 /*serverCache=*/ null, writeTreeChildWrites(syncTree.pendingWriteTree_, newEmptyPath()));
10071}
10072/**
10073 * Recursive helper for applyOperationToSyncPoints_
10074 */
10075function syncTreeApplyOperationHelper_(operation, syncPointTree, serverCache, writesCache) {
10076 if (pathIsEmpty(operation.path)) {
10077 return syncTreeApplyOperationDescendantsHelper_(operation, syncPointTree, serverCache, writesCache);
10078 }
10079 else {
10080 const syncPoint = syncPointTree.get(newEmptyPath());
10081 // If we don't have cached server data, see if we can get it from this SyncPoint.
10082 if (serverCache == null && syncPoint != null) {
10083 serverCache = syncPointGetCompleteServerCache(syncPoint, newEmptyPath());
10084 }
10085 let events = [];
10086 const childName = pathGetFront(operation.path);
10087 const childOperation = operation.operationForChild(childName);
10088 const childTree = syncPointTree.children.get(childName);
10089 if (childTree && childOperation) {
10090 const childServerCache = serverCache
10091 ? serverCache.getImmediateChild(childName)
10092 : null;
10093 const childWritesCache = writeTreeRefChild(writesCache, childName);
10094 events = events.concat(syncTreeApplyOperationHelper_(childOperation, childTree, childServerCache, childWritesCache));
10095 }
10096 if (syncPoint) {
10097 events = events.concat(syncPointApplyOperation(syncPoint, operation, writesCache, serverCache));
10098 }
10099 return events;
10100 }
10101}
10102/**
10103 * Recursive helper for applyOperationToSyncPoints_
10104 */
10105function syncTreeApplyOperationDescendantsHelper_(operation, syncPointTree, serverCache, writesCache) {
10106 const syncPoint = syncPointTree.get(newEmptyPath());
10107 // If we don't have cached server data, see if we can get it from this SyncPoint.
10108 if (serverCache == null && syncPoint != null) {
10109 serverCache = syncPointGetCompleteServerCache(syncPoint, newEmptyPath());
10110 }
10111 let events = [];
10112 syncPointTree.children.inorderTraversal((childName, childTree) => {
10113 const childServerCache = serverCache
10114 ? serverCache.getImmediateChild(childName)
10115 : null;
10116 const childWritesCache = writeTreeRefChild(writesCache, childName);
10117 const childOperation = operation.operationForChild(childName);
10118 if (childOperation) {
10119 events = events.concat(syncTreeApplyOperationDescendantsHelper_(childOperation, childTree, childServerCache, childWritesCache));
10120 }
10121 });
10122 if (syncPoint) {
10123 events = events.concat(syncPointApplyOperation(syncPoint, operation, writesCache, serverCache));
10124 }
10125 return events;
10126}
10127function syncTreeCreateListenerForView_(syncTree, view) {
10128 const query = view.query;
10129 const tag = syncTreeTagForQuery_(syncTree, query);
10130 return {
10131 hashFn: () => {
10132 const cache = viewGetServerCache(view) || ChildrenNode.EMPTY_NODE;
10133 return cache.hash();
10134 },
10135 onComplete: (status) => {
10136 if (status === 'ok') {
10137 if (tag) {
10138 return syncTreeApplyTaggedListenComplete(syncTree, query._path, tag);
10139 }
10140 else {
10141 return syncTreeApplyListenComplete(syncTree, query._path);
10142 }
10143 }
10144 else {
10145 // If a listen failed, kill all of the listeners here, not just the one that triggered the error.
10146 // Note that this may need to be scoped to just this listener if we change permissions on filtered children
10147 const error = errorForServerCode(status, query);
10148 return syncTreeRemoveEventRegistration(syncTree, query,
10149 /*eventRegistration*/ null, error);
10150 }
10151 }
10152 };
10153}
10154/**
10155 * Return the tag associated with the given query.
10156 */
10157function syncTreeTagForQuery_(syncTree, query) {
10158 const queryKey = syncTreeMakeQueryKey_(query);
10159 return syncTree.queryToTagMap.get(queryKey);
10160}
10161/**
10162 * Given a query, computes a "queryKey" suitable for use in our queryToTagMap_.
10163 */
10164function syncTreeMakeQueryKey_(query) {
10165 return query._path.toString() + '$' + query._queryIdentifier;
10166}
10167/**
10168 * Return the query associated with the given tag, if we have one
10169 */
10170function syncTreeQueryKeyForTag_(syncTree, tag) {
10171 return syncTree.tagToQueryMap.get(tag);
10172}
10173/**
10174 * Given a queryKey (created by makeQueryKey), parse it back into a path and queryId.
10175 */
10176function syncTreeParseQueryKey_(queryKey) {
10177 const splitIndex = queryKey.indexOf('$');
10178 assert(splitIndex !== -1 && splitIndex < queryKey.length - 1, 'Bad queryKey.');
10179 return {
10180 queryId: queryKey.substr(splitIndex + 1),
10181 path: new Path(queryKey.substr(0, splitIndex))
10182 };
10183}
10184/**
10185 * A helper method to apply tagged operations
10186 */
10187function syncTreeApplyTaggedOperation_(syncTree, queryPath, operation) {
10188 const syncPoint = syncTree.syncPointTree_.get(queryPath);
10189 assert(syncPoint, "Missing sync point for query tag that we're tracking");
10190 const writesCache = writeTreeChildWrites(syncTree.pendingWriteTree_, queryPath);
10191 return syncPointApplyOperation(syncPoint, operation, writesCache, null);
10192}
10193/**
10194 * This collapses multiple unfiltered views into a single view, since we only need a single
10195 * listener for them.
10196 */
10197function syncTreeCollectDistinctViewsForSubTree_(subtree) {
10198 return subtree.fold((relativePath, maybeChildSyncPoint, childMap) => {
10199 if (maybeChildSyncPoint && syncPointHasCompleteView(maybeChildSyncPoint)) {
10200 const completeView = syncPointGetCompleteView(maybeChildSyncPoint);
10201 return [completeView];
10202 }
10203 else {
10204 // No complete view here, flatten any deeper listens into an array
10205 let views = [];
10206 if (maybeChildSyncPoint) {
10207 views = syncPointGetQueryViews(maybeChildSyncPoint);
10208 }
10209 each(childMap, (_key, childViews) => {
10210 views = views.concat(childViews);
10211 });
10212 return views;
10213 }
10214 });
10215}
10216/**
10217 * Normalizes a query to a query we send the server for listening
10218 *
10219 * @returns The normalized query
10220 */
10221function syncTreeQueryForListening_(query) {
10222 if (query._queryParams.loadsAllData() && !query._queryParams.isDefault()) {
10223 // We treat queries that load all data as default queries
10224 // Cast is necessary because ref() technically returns Firebase which is actually fb.api.Firebase which inherits
10225 // from Query
10226 return new (syncTreeGetReferenceConstructor())(query._repo, query._path);
10227 }
10228 else {
10229 return query;
10230 }
10231}
10232function syncTreeRemoveTags_(syncTree, queries) {
10233 for (let j = 0; j < queries.length; ++j) {
10234 const removedQuery = queries[j];
10235 if (!removedQuery._queryParams.loadsAllData()) {
10236 // We should have a tag for this
10237 const removedQueryKey = syncTreeMakeQueryKey_(removedQuery);
10238 const removedQueryTag = syncTree.queryToTagMap.get(removedQueryKey);
10239 syncTree.queryToTagMap.delete(removedQueryKey);
10240 syncTree.tagToQueryMap.delete(removedQueryTag);
10241 }
10242 }
10243}
10244/**
10245 * Static accessor for query tags.
10246 */
10247function syncTreeGetNextQueryTag_() {
10248 return syncTreeNextQueryTag_++;
10249}
10250/**
10251 * For a given new listen, manage the de-duplication of outstanding subscriptions.
10252 *
10253 * @returns This method can return events to support synchronous data sources
10254 */
10255function syncTreeSetupListener_(syncTree, query, view) {
10256 const path = query._path;
10257 const tag = syncTreeTagForQuery_(syncTree, query);
10258 const listener = syncTreeCreateListenerForView_(syncTree, view);
10259 const events = syncTree.listenProvider_.startListening(syncTreeQueryForListening_(query), tag, listener.hashFn, listener.onComplete);
10260 const subtree = syncTree.syncPointTree_.subtree(path);
10261 // The root of this subtree has our query. We're here because we definitely need to send a listen for that, but we
10262 // may need to shadow other listens as well.
10263 if (tag) {
10264 assert(!syncPointHasCompleteView(subtree.value), "If we're adding a query, it shouldn't be shadowed");
10265 }
10266 else {
10267 // Shadow everything at or below this location, this is a default listener.
10268 const queriesToStop = subtree.fold((relativePath, maybeChildSyncPoint, childMap) => {
10269 if (!pathIsEmpty(relativePath) &&
10270 maybeChildSyncPoint &&
10271 syncPointHasCompleteView(maybeChildSyncPoint)) {
10272 return [syncPointGetCompleteView(maybeChildSyncPoint).query];
10273 }
10274 else {
10275 // No default listener here, flatten any deeper queries into an array
10276 let queries = [];
10277 if (maybeChildSyncPoint) {
10278 queries = queries.concat(syncPointGetQueryViews(maybeChildSyncPoint).map(view => view.query));
10279 }
10280 each(childMap, (_key, childQueries) => {
10281 queries = queries.concat(childQueries);
10282 });
10283 return queries;
10284 }
10285 });
10286 for (let i = 0; i < queriesToStop.length; ++i) {
10287 const queryToStop = queriesToStop[i];
10288 syncTree.listenProvider_.stopListening(syncTreeQueryForListening_(queryToStop), syncTreeTagForQuery_(syncTree, queryToStop));
10289 }
10290 }
10291 return events;
10292}
10293
10294/**
10295 * @license
10296 * Copyright 2017 Google LLC
10297 *
10298 * Licensed under the Apache License, Version 2.0 (the "License");
10299 * you may not use this file except in compliance with the License.
10300 * You may obtain a copy of the License at
10301 *
10302 * http://www.apache.org/licenses/LICENSE-2.0
10303 *
10304 * Unless required by applicable law or agreed to in writing, software
10305 * distributed under the License is distributed on an "AS IS" BASIS,
10306 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10307 * See the License for the specific language governing permissions and
10308 * limitations under the License.
10309 */
10310class ExistingValueProvider {
10311 constructor(node_) {
10312 this.node_ = node_;
10313 }
10314 getImmediateChild(childName) {
10315 const child = this.node_.getImmediateChild(childName);
10316 return new ExistingValueProvider(child);
10317 }
10318 node() {
10319 return this.node_;
10320 }
10321}
10322class DeferredValueProvider {
10323 constructor(syncTree, path) {
10324 this.syncTree_ = syncTree;
10325 this.path_ = path;
10326 }
10327 getImmediateChild(childName) {
10328 const childPath = pathChild(this.path_, childName);
10329 return new DeferredValueProvider(this.syncTree_, childPath);
10330 }
10331 node() {
10332 return syncTreeCalcCompleteEventCache(this.syncTree_, this.path_);
10333 }
10334}
10335/**
10336 * Generate placeholders for deferred values.
10337 */
10338const generateWithValues = function (values) {
10339 values = values || {};
10340 values['timestamp'] = values['timestamp'] || new Date().getTime();
10341 return values;
10342};
10343/**
10344 * Value to use when firing local events. When writing server values, fire
10345 * local events with an approximate value, otherwise return value as-is.
10346 */
10347const resolveDeferredLeafValue = function (value, existingVal, serverValues) {
10348 if (!value || typeof value !== 'object') {
10349 return value;
10350 }
10351 assert('.sv' in value, 'Unexpected leaf node or priority contents');
10352 if (typeof value['.sv'] === 'string') {
10353 return resolveScalarDeferredValue(value['.sv'], existingVal, serverValues);
10354 }
10355 else if (typeof value['.sv'] === 'object') {
10356 return resolveComplexDeferredValue(value['.sv'], existingVal);
10357 }
10358 else {
10359 assert(false, 'Unexpected server value: ' + JSON.stringify(value, null, 2));
10360 }
10361};
10362const resolveScalarDeferredValue = function (op, existing, serverValues) {
10363 switch (op) {
10364 case 'timestamp':
10365 return serverValues['timestamp'];
10366 default:
10367 assert(false, 'Unexpected server value: ' + op);
10368 }
10369};
10370const resolveComplexDeferredValue = function (op, existing, unused) {
10371 if (!op.hasOwnProperty('increment')) {
10372 assert(false, 'Unexpected server value: ' + JSON.stringify(op, null, 2));
10373 }
10374 const delta = op['increment'];
10375 if (typeof delta !== 'number') {
10376 assert(false, 'Unexpected increment value: ' + delta);
10377 }
10378 const existingNode = existing.node();
10379 assert(existingNode !== null && typeof existingNode !== 'undefined', 'Expected ChildrenNode.EMPTY_NODE for nulls');
10380 // Incrementing a non-number sets the value to the incremented amount
10381 if (!existingNode.isLeafNode()) {
10382 return delta;
10383 }
10384 const leaf = existingNode;
10385 const existingVal = leaf.getValue();
10386 if (typeof existingVal !== 'number') {
10387 return delta;
10388 }
10389 // No need to do over/underflow arithmetic here because JS only handles floats under the covers
10390 return existingVal + delta;
10391};
10392/**
10393 * Recursively replace all deferred values and priorities in the tree with the
10394 * specified generated replacement values.
10395 * @param path - path to which write is relative
10396 * @param node - new data written at path
10397 * @param syncTree - current data
10398 */
10399const resolveDeferredValueTree = function (path, node, syncTree, serverValues) {
10400 return resolveDeferredValue(node, new DeferredValueProvider(syncTree, path), serverValues);
10401};
10402/**
10403 * Recursively replace all deferred values and priorities in the node with the
10404 * specified generated replacement values. If there are no server values in the node,
10405 * it'll be returned as-is.
10406 */
10407const resolveDeferredValueSnapshot = function (node, existing, serverValues) {
10408 return resolveDeferredValue(node, new ExistingValueProvider(existing), serverValues);
10409};
10410function resolveDeferredValue(node, existingVal, serverValues) {
10411 const rawPri = node.getPriority().val();
10412 const priority = resolveDeferredLeafValue(rawPri, existingVal.getImmediateChild('.priority'), serverValues);
10413 let newNode;
10414 if (node.isLeafNode()) {
10415 const leafNode = node;
10416 const value = resolveDeferredLeafValue(leafNode.getValue(), existingVal, serverValues);
10417 if (value !== leafNode.getValue() ||
10418 priority !== leafNode.getPriority().val()) {
10419 return new LeafNode(value, nodeFromJSON(priority));
10420 }
10421 else {
10422 return node;
10423 }
10424 }
10425 else {
10426 const childrenNode = node;
10427 newNode = childrenNode;
10428 if (priority !== childrenNode.getPriority().val()) {
10429 newNode = newNode.updatePriority(new LeafNode(priority));
10430 }
10431 childrenNode.forEachChild(PRIORITY_INDEX, (childName, childNode) => {
10432 const newChildNode = resolveDeferredValue(childNode, existingVal.getImmediateChild(childName), serverValues);
10433 if (newChildNode !== childNode) {
10434 newNode = newNode.updateImmediateChild(childName, newChildNode);
10435 }
10436 });
10437 return newNode;
10438 }
10439}
10440
10441/**
10442 * @license
10443 * Copyright 2017 Google LLC
10444 *
10445 * Licensed under the Apache License, Version 2.0 (the "License");
10446 * you may not use this file except in compliance with the License.
10447 * You may obtain a copy of the License at
10448 *
10449 * http://www.apache.org/licenses/LICENSE-2.0
10450 *
10451 * Unless required by applicable law or agreed to in writing, software
10452 * distributed under the License is distributed on an "AS IS" BASIS,
10453 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10454 * See the License for the specific language governing permissions and
10455 * limitations under the License.
10456 */
10457/**
10458 * A light-weight tree, traversable by path. Nodes can have both values and children.
10459 * Nodes are not enumerated (by forEachChild) unless they have a value or non-empty
10460 * children.
10461 */
10462class Tree {
10463 /**
10464 * @param name - Optional name of the node.
10465 * @param parent - Optional parent node.
10466 * @param node - Optional node to wrap.
10467 */
10468 constructor(name = '', parent = null, node = { children: {}, childCount: 0 }) {
10469 this.name = name;
10470 this.parent = parent;
10471 this.node = node;
10472 }
10473}
10474/**
10475 * Returns a sub-Tree for the given path.
10476 *
10477 * @param pathObj - Path to look up.
10478 * @returns Tree for path.
10479 */
10480function treeSubTree(tree, pathObj) {
10481 // TODO: Require pathObj to be Path?
10482 let path = pathObj instanceof Path ? pathObj : new Path(pathObj);
10483 let child = tree, next = pathGetFront(path);
10484 while (next !== null) {
10485 const childNode = safeGet(child.node.children, next) || {
10486 children: {},
10487 childCount: 0
10488 };
10489 child = new Tree(next, child, childNode);
10490 path = pathPopFront(path);
10491 next = pathGetFront(path);
10492 }
10493 return child;
10494}
10495/**
10496 * Returns the data associated with this tree node.
10497 *
10498 * @returns The data or null if no data exists.
10499 */
10500function treeGetValue(tree) {
10501 return tree.node.value;
10502}
10503/**
10504 * Sets data to this tree node.
10505 *
10506 * @param value - Value to set.
10507 */
10508function treeSetValue(tree, value) {
10509 tree.node.value = value;
10510 treeUpdateParents(tree);
10511}
10512/**
10513 * @returns Whether the tree has any children.
10514 */
10515function treeHasChildren(tree) {
10516 return tree.node.childCount > 0;
10517}
10518/**
10519 * @returns Whethe rthe tree is empty (no value or children).
10520 */
10521function treeIsEmpty(tree) {
10522 return treeGetValue(tree) === undefined && !treeHasChildren(tree);
10523}
10524/**
10525 * Calls action for each child of this tree node.
10526 *
10527 * @param action - Action to be called for each child.
10528 */
10529function treeForEachChild(tree, action) {
10530 each(tree.node.children, (child, childTree) => {
10531 action(new Tree(child, tree, childTree));
10532 });
10533}
10534/**
10535 * Does a depth-first traversal of this node's descendants, calling action for each one.
10536 *
10537 * @param action - Action to be called for each child.
10538 * @param includeSelf - Whether to call action on this node as well. Defaults to
10539 * false.
10540 * @param childrenFirst - Whether to call action on children before calling it on
10541 * parent.
10542 */
10543function treeForEachDescendant(tree, action, includeSelf, childrenFirst) {
10544 if (includeSelf && !childrenFirst) {
10545 action(tree);
10546 }
10547 treeForEachChild(tree, child => {
10548 treeForEachDescendant(child, action, true, childrenFirst);
10549 });
10550 if (includeSelf && childrenFirst) {
10551 action(tree);
10552 }
10553}
10554/**
10555 * Calls action on each ancestor node.
10556 *
10557 * @param action - Action to be called on each parent; return
10558 * true to abort.
10559 * @param includeSelf - Whether to call action on this node as well.
10560 * @returns true if the action callback returned true.
10561 */
10562function treeForEachAncestor(tree, action, includeSelf) {
10563 let node = includeSelf ? tree : tree.parent;
10564 while (node !== null) {
10565 if (action(node)) {
10566 return true;
10567 }
10568 node = node.parent;
10569 }
10570 return false;
10571}
10572/**
10573 * @returns The path of this tree node, as a Path.
10574 */
10575function treeGetPath(tree) {
10576 return new Path(tree.parent === null
10577 ? tree.name
10578 : treeGetPath(tree.parent) + '/' + tree.name);
10579}
10580/**
10581 * Adds or removes this child from its parent based on whether it's empty or not.
10582 */
10583function treeUpdateParents(tree) {
10584 if (tree.parent !== null) {
10585 treeUpdateChild(tree.parent, tree.name, tree);
10586 }
10587}
10588/**
10589 * Adds or removes the passed child to this tree node, depending on whether it's empty.
10590 *
10591 * @param childName - The name of the child to update.
10592 * @param child - The child to update.
10593 */
10594function treeUpdateChild(tree, childName, child) {
10595 const childEmpty = treeIsEmpty(child);
10596 const childExists = contains(tree.node.children, childName);
10597 if (childEmpty && childExists) {
10598 delete tree.node.children[childName];
10599 tree.node.childCount--;
10600 treeUpdateParents(tree);
10601 }
10602 else if (!childEmpty && !childExists) {
10603 tree.node.children[childName] = child.node;
10604 tree.node.childCount++;
10605 treeUpdateParents(tree);
10606 }
10607}
10608
10609/**
10610 * @license
10611 * Copyright 2017 Google LLC
10612 *
10613 * Licensed under the Apache License, Version 2.0 (the "License");
10614 * you may not use this file except in compliance with the License.
10615 * You may obtain a copy of the License at
10616 *
10617 * http://www.apache.org/licenses/LICENSE-2.0
10618 *
10619 * Unless required by applicable law or agreed to in writing, software
10620 * distributed under the License is distributed on an "AS IS" BASIS,
10621 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10622 * See the License for the specific language governing permissions and
10623 * limitations under the License.
10624 */
10625/**
10626 * True for invalid Firebase keys
10627 */
10628const INVALID_KEY_REGEX_ = /[\[\].#$\/\u0000-\u001F\u007F]/;
10629/**
10630 * True for invalid Firebase paths.
10631 * Allows '/' in paths.
10632 */
10633const INVALID_PATH_REGEX_ = /[\[\].#$\u0000-\u001F\u007F]/;
10634/**
10635 * Maximum number of characters to allow in leaf value
10636 */
10637const MAX_LEAF_SIZE_ = 10 * 1024 * 1024;
10638const isValidKey = function (key) {
10639 return (typeof key === 'string' && key.length !== 0 && !INVALID_KEY_REGEX_.test(key));
10640};
10641const isValidPathString = function (pathString) {
10642 return (typeof pathString === 'string' &&
10643 pathString.length !== 0 &&
10644 !INVALID_PATH_REGEX_.test(pathString));
10645};
10646const isValidRootPathString = function (pathString) {
10647 if (pathString) {
10648 // Allow '/.info/' at the beginning.
10649 pathString = pathString.replace(/^\/*\.info(\/|$)/, '/');
10650 }
10651 return isValidPathString(pathString);
10652};
10653const isValidPriority = function (priority) {
10654 return (priority === null ||
10655 typeof priority === 'string' ||
10656 (typeof priority === 'number' && !isInvalidJSONNumber(priority)) ||
10657 (priority &&
10658 typeof priority === 'object' &&
10659 // eslint-disable-next-line @typescript-eslint/no-explicit-any
10660 contains(priority, '.sv')));
10661};
10662/**
10663 * Pre-validate a datum passed as an argument to Firebase function.
10664 */
10665const validateFirebaseDataArg = function (fnName, value, path, optional) {
10666 if (optional && value === undefined) {
10667 return;
10668 }
10669 validateFirebaseData(errorPrefix(fnName, 'value'), value, path);
10670};
10671/**
10672 * Validate a data object client-side before sending to server.
10673 */
10674const validateFirebaseData = function (errorPrefix, data, path_) {
10675 const path = path_ instanceof Path ? new ValidationPath(path_, errorPrefix) : path_;
10676 if (data === undefined) {
10677 throw new Error(errorPrefix + 'contains undefined ' + validationPathToErrorString(path));
10678 }
10679 if (typeof data === 'function') {
10680 throw new Error(errorPrefix +
10681 'contains a function ' +
10682 validationPathToErrorString(path) +
10683 ' with contents = ' +
10684 data.toString());
10685 }
10686 if (isInvalidJSONNumber(data)) {
10687 throw new Error(errorPrefix +
10688 'contains ' +
10689 data.toString() +
10690 ' ' +
10691 validationPathToErrorString(path));
10692 }
10693 // Check max leaf size, but try to avoid the utf8 conversion if we can.
10694 if (typeof data === 'string' &&
10695 data.length > MAX_LEAF_SIZE_ / 3 &&
10696 stringLength(data) > MAX_LEAF_SIZE_) {
10697 throw new Error(errorPrefix +
10698 'contains a string greater than ' +
10699 MAX_LEAF_SIZE_ +
10700 ' utf8 bytes ' +
10701 validationPathToErrorString(path) +
10702 " ('" +
10703 data.substring(0, 50) +
10704 "...')");
10705 }
10706 // TODO = Perf = Consider combining the recursive validation of keys into NodeFromJSON
10707 // to save extra walking of large objects.
10708 if (data && typeof data === 'object') {
10709 let hasDotValue = false;
10710 let hasActualChild = false;
10711 each(data, (key, value) => {
10712 if (key === '.value') {
10713 hasDotValue = true;
10714 }
10715 else if (key !== '.priority' && key !== '.sv') {
10716 hasActualChild = true;
10717 if (!isValidKey(key)) {
10718 throw new Error(errorPrefix +
10719 ' contains an invalid key (' +
10720 key +
10721 ') ' +
10722 validationPathToErrorString(path) +
10723 '. Keys must be non-empty strings ' +
10724 'and can\'t contain ".", "#", "$", "/", "[", or "]"');
10725 }
10726 }
10727 validationPathPush(path, key);
10728 validateFirebaseData(errorPrefix, value, path);
10729 validationPathPop(path);
10730 });
10731 if (hasDotValue && hasActualChild) {
10732 throw new Error(errorPrefix +
10733 ' contains ".value" child ' +
10734 validationPathToErrorString(path) +
10735 ' in addition to actual children.');
10736 }
10737 }
10738};
10739/**
10740 * Pre-validate paths passed in the firebase function.
10741 */
10742const validateFirebaseMergePaths = function (errorPrefix, mergePaths) {
10743 let i, curPath;
10744 for (i = 0; i < mergePaths.length; i++) {
10745 curPath = mergePaths[i];
10746 const keys = pathSlice(curPath);
10747 for (let j = 0; j < keys.length; j++) {
10748 if (keys[j] === '.priority' && j === keys.length - 1) ;
10749 else if (!isValidKey(keys[j])) {
10750 throw new Error(errorPrefix +
10751 'contains an invalid key (' +
10752 keys[j] +
10753 ') in path ' +
10754 curPath.toString() +
10755 '. Keys must be non-empty strings ' +
10756 'and can\'t contain ".", "#", "$", "/", "[", or "]"');
10757 }
10758 }
10759 }
10760 // Check that update keys are not descendants of each other.
10761 // We rely on the property that sorting guarantees that ancestors come
10762 // right before descendants.
10763 mergePaths.sort(pathCompare);
10764 let prevPath = null;
10765 for (i = 0; i < mergePaths.length; i++) {
10766 curPath = mergePaths[i];
10767 if (prevPath !== null && pathContains(prevPath, curPath)) {
10768 throw new Error(errorPrefix +
10769 'contains a path ' +
10770 prevPath.toString() +
10771 ' that is ancestor of another path ' +
10772 curPath.toString());
10773 }
10774 prevPath = curPath;
10775 }
10776};
10777/**
10778 * pre-validate an object passed as an argument to firebase function (
10779 * must be an object - e.g. for firebase.update()).
10780 */
10781const validateFirebaseMergeDataArg = function (fnName, data, path, optional) {
10782 if (optional && data === undefined) {
10783 return;
10784 }
10785 const errorPrefix$1 = errorPrefix(fnName, 'values');
10786 if (!(data && typeof data === 'object') || Array.isArray(data)) {
10787 throw new Error(errorPrefix$1 + ' must be an object containing the children to replace.');
10788 }
10789 const mergePaths = [];
10790 each(data, (key, value) => {
10791 const curPath = new Path(key);
10792 validateFirebaseData(errorPrefix$1, value, pathChild(path, curPath));
10793 if (pathGetBack(curPath) === '.priority') {
10794 if (!isValidPriority(value)) {
10795 throw new Error(errorPrefix$1 +
10796 "contains an invalid value for '" +
10797 curPath.toString() +
10798 "', which must be a valid " +
10799 'Firebase priority (a string, finite number, server value, or null).');
10800 }
10801 }
10802 mergePaths.push(curPath);
10803 });
10804 validateFirebaseMergePaths(errorPrefix$1, mergePaths);
10805};
10806const validatePriority = function (fnName, priority, optional) {
10807 if (optional && priority === undefined) {
10808 return;
10809 }
10810 if (isInvalidJSONNumber(priority)) {
10811 throw new Error(errorPrefix(fnName, 'priority') +
10812 'is ' +
10813 priority.toString() +
10814 ', but must be a valid Firebase priority (a string, finite number, ' +
10815 'server value, or null).');
10816 }
10817 // Special case to allow importing data with a .sv.
10818 if (!isValidPriority(priority)) {
10819 throw new Error(errorPrefix(fnName, 'priority') +
10820 'must be a valid Firebase priority ' +
10821 '(a string, finite number, server value, or null).');
10822 }
10823};
10824const validateKey = function (fnName, argumentName, key, optional) {
10825 if (optional && key === undefined) {
10826 return;
10827 }
10828 if (!isValidKey(key)) {
10829 throw new Error(errorPrefix(fnName, argumentName) +
10830 'was an invalid key = "' +
10831 key +
10832 '". Firebase keys must be non-empty strings and ' +
10833 'can\'t contain ".", "#", "$", "/", "[", or "]").');
10834 }
10835};
10836/**
10837 * @internal
10838 */
10839const validatePathString = function (fnName, argumentName, pathString, optional) {
10840 if (optional && pathString === undefined) {
10841 return;
10842 }
10843 if (!isValidPathString(pathString)) {
10844 throw new Error(errorPrefix(fnName, argumentName) +
10845 'was an invalid path = "' +
10846 pathString +
10847 '". Paths must be non-empty strings and ' +
10848 'can\'t contain ".", "#", "$", "[", or "]"');
10849 }
10850};
10851const validateRootPathString = function (fnName, argumentName, pathString, optional) {
10852 if (pathString) {
10853 // Allow '/.info/' at the beginning.
10854 pathString = pathString.replace(/^\/*\.info(\/|$)/, '/');
10855 }
10856 validatePathString(fnName, argumentName, pathString, optional);
10857};
10858/**
10859 * @internal
10860 */
10861const validateWritablePath = function (fnName, path) {
10862 if (pathGetFront(path) === '.info') {
10863 throw new Error(fnName + " failed = Can't modify data under /.info/");
10864 }
10865};
10866const validateUrl = function (fnName, parsedUrl) {
10867 // TODO = Validate server better.
10868 const pathString = parsedUrl.path.toString();
10869 if (!(typeof parsedUrl.repoInfo.host === 'string') ||
10870 parsedUrl.repoInfo.host.length === 0 ||
10871 (!isValidKey(parsedUrl.repoInfo.namespace) &&
10872 parsedUrl.repoInfo.host.split(':')[0] !== 'localhost') ||
10873 (pathString.length !== 0 && !isValidRootPathString(pathString))) {
10874 throw new Error(errorPrefix(fnName, 'url') +
10875 'must be a valid firebase URL and ' +
10876 'the path can\'t contain ".", "#", "$", "[", or "]".');
10877 }
10878};
10879
10880/**
10881 * @license
10882 * Copyright 2017 Google LLC
10883 *
10884 * Licensed under the Apache License, Version 2.0 (the "License");
10885 * you may not use this file except in compliance with the License.
10886 * You may obtain a copy of the License at
10887 *
10888 * http://www.apache.org/licenses/LICENSE-2.0
10889 *
10890 * Unless required by applicable law or agreed to in writing, software
10891 * distributed under the License is distributed on an "AS IS" BASIS,
10892 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10893 * See the License for the specific language governing permissions and
10894 * limitations under the License.
10895 */
10896/**
10897 * The event queue serves a few purposes:
10898 * 1. It ensures we maintain event order in the face of event callbacks doing operations that result in more
10899 * events being queued.
10900 * 2. raiseQueuedEvents() handles being called reentrantly nicely. That is, if in the course of raising events,
10901 * raiseQueuedEvents() is called again, the "inner" call will pick up raising events where the "outer" call
10902 * left off, ensuring that the events are still raised synchronously and in order.
10903 * 3. You can use raiseEventsAtPath and raiseEventsForChangedPath to ensure only relevant previously-queued
10904 * events are raised synchronously.
10905 *
10906 * NOTE: This can all go away if/when we move to async events.
10907 *
10908 */
10909class EventQueue {
10910 constructor() {
10911 this.eventLists_ = [];
10912 /**
10913 * Tracks recursion depth of raiseQueuedEvents_, for debugging purposes.
10914 */
10915 this.recursionDepth_ = 0;
10916 }
10917}
10918/**
10919 * @param eventDataList - The new events to queue.
10920 */
10921function eventQueueQueueEvents(eventQueue, eventDataList) {
10922 // We group events by path, storing them in a single EventList, to make it easier to skip over them quickly.
10923 let currList = null;
10924 for (let i = 0; i < eventDataList.length; i++) {
10925 const data = eventDataList[i];
10926 const path = data.getPath();
10927 if (currList !== null && !pathEquals(path, currList.path)) {
10928 eventQueue.eventLists_.push(currList);
10929 currList = null;
10930 }
10931 if (currList === null) {
10932 currList = { events: [], path };
10933 }
10934 currList.events.push(data);
10935 }
10936 if (currList) {
10937 eventQueue.eventLists_.push(currList);
10938 }
10939}
10940/**
10941 * Queues the specified events and synchronously raises all events (including previously queued ones)
10942 * for the specified path.
10943 *
10944 * It is assumed that the new events are all for the specified path.
10945 *
10946 * @param path - The path to raise events for.
10947 * @param eventDataList - The new events to raise.
10948 */
10949function eventQueueRaiseEventsAtPath(eventQueue, path, eventDataList) {
10950 eventQueueQueueEvents(eventQueue, eventDataList);
10951 eventQueueRaiseQueuedEventsMatchingPredicate(eventQueue, eventPath => pathEquals(eventPath, path));
10952}
10953/**
10954 * Queues the specified events and synchronously raises all events (including previously queued ones) for
10955 * locations related to the specified change path (i.e. all ancestors and descendants).
10956 *
10957 * It is assumed that the new events are all related (ancestor or descendant) to the specified path.
10958 *
10959 * @param changedPath - The path to raise events for.
10960 * @param eventDataList - The events to raise
10961 */
10962function eventQueueRaiseEventsForChangedPath(eventQueue, changedPath, eventDataList) {
10963 eventQueueQueueEvents(eventQueue, eventDataList);
10964 eventQueueRaiseQueuedEventsMatchingPredicate(eventQueue, eventPath => pathContains(eventPath, changedPath) ||
10965 pathContains(changedPath, eventPath));
10966}
10967function eventQueueRaiseQueuedEventsMatchingPredicate(eventQueue, predicate) {
10968 eventQueue.recursionDepth_++;
10969 let sentAll = true;
10970 for (let i = 0; i < eventQueue.eventLists_.length; i++) {
10971 const eventList = eventQueue.eventLists_[i];
10972 if (eventList) {
10973 const eventPath = eventList.path;
10974 if (predicate(eventPath)) {
10975 eventListRaise(eventQueue.eventLists_[i]);
10976 eventQueue.eventLists_[i] = null;
10977 }
10978 else {
10979 sentAll = false;
10980 }
10981 }
10982 }
10983 if (sentAll) {
10984 eventQueue.eventLists_ = [];
10985 }
10986 eventQueue.recursionDepth_--;
10987}
10988/**
10989 * Iterates through the list and raises each event
10990 */
10991function eventListRaise(eventList) {
10992 for (let i = 0; i < eventList.events.length; i++) {
10993 const eventData = eventList.events[i];
10994 if (eventData !== null) {
10995 eventList.events[i] = null;
10996 const eventFn = eventData.getEventRunner();
10997 if (logger) {
10998 log('event: ' + eventData.toString());
10999 }
11000 exceptionGuard(eventFn);
11001 }
11002 }
11003}
11004
11005/**
11006 * @license
11007 * Copyright 2017 Google LLC
11008 *
11009 * Licensed under the Apache License, Version 2.0 (the "License");
11010 * you may not use this file except in compliance with the License.
11011 * You may obtain a copy of the License at
11012 *
11013 * http://www.apache.org/licenses/LICENSE-2.0
11014 *
11015 * Unless required by applicable law or agreed to in writing, software
11016 * distributed under the License is distributed on an "AS IS" BASIS,
11017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11018 * See the License for the specific language governing permissions and
11019 * limitations under the License.
11020 */
11021const INTERRUPT_REASON = 'repo_interrupt';
11022/**
11023 * If a transaction does not succeed after 25 retries, we abort it. Among other
11024 * things this ensure that if there's ever a bug causing a mismatch between
11025 * client / server hashes for some data, we won't retry indefinitely.
11026 */
11027const MAX_TRANSACTION_RETRIES = 25;
11028/**
11029 * A connection to a single data repository.
11030 */
11031class Repo {
11032 constructor(repoInfo_, forceRestClient_, authTokenProvider_, appCheckProvider_) {
11033 this.repoInfo_ = repoInfo_;
11034 this.forceRestClient_ = forceRestClient_;
11035 this.authTokenProvider_ = authTokenProvider_;
11036 this.appCheckProvider_ = appCheckProvider_;
11037 this.dataUpdateCount = 0;
11038 this.statsListener_ = null;
11039 this.eventQueue_ = new EventQueue();
11040 this.nextWriteId_ = 1;
11041 this.interceptServerDataCallback_ = null;
11042 /** A list of data pieces and paths to be set when this client disconnects. */
11043 this.onDisconnect_ = newSparseSnapshotTree();
11044 /** Stores queues of outstanding transactions for Firebase locations. */
11045 this.transactionQueueTree_ = new Tree();
11046 // TODO: This should be @private but it's used by test_access.js and internal.js
11047 this.persistentConnection_ = null;
11048 // This key is intentionally not updated if RepoInfo is later changed or replaced
11049 this.key = this.repoInfo_.toURLString();
11050 }
11051 /**
11052 * @returns The URL corresponding to the root of this Firebase.
11053 */
11054 toString() {
11055 return ((this.repoInfo_.secure ? 'https://' : 'http://') + this.repoInfo_.host);
11056 }
11057}
11058function repoStart(repo, appId, authOverride) {
11059 repo.stats_ = statsManagerGetCollection(repo.repoInfo_);
11060 if (repo.forceRestClient_ || beingCrawled()) {
11061 repo.server_ = new ReadonlyRestClient(repo.repoInfo_, (pathString, data, isMerge, tag) => {
11062 repoOnDataUpdate(repo, pathString, data, isMerge, tag);
11063 }, repo.authTokenProvider_, repo.appCheckProvider_);
11064 // Minor hack: Fire onConnect immediately, since there's no actual connection.
11065 setTimeout(() => repoOnConnectStatus(repo, /* connectStatus= */ true), 0);
11066 }
11067 else {
11068 // Validate authOverride
11069 if (typeof authOverride !== 'undefined' && authOverride !== null) {
11070 if (typeof authOverride !== 'object') {
11071 throw new Error('Only objects are supported for option databaseAuthVariableOverride');
11072 }
11073 try {
11074 stringify(authOverride);
11075 }
11076 catch (e) {
11077 throw new Error('Invalid authOverride provided: ' + e);
11078 }
11079 }
11080 repo.persistentConnection_ = new PersistentConnection(repo.repoInfo_, appId, (pathString, data, isMerge, tag) => {
11081 repoOnDataUpdate(repo, pathString, data, isMerge, tag);
11082 }, (connectStatus) => {
11083 repoOnConnectStatus(repo, connectStatus);
11084 }, (updates) => {
11085 repoOnServerInfoUpdate(repo, updates);
11086 }, repo.authTokenProvider_, repo.appCheckProvider_, authOverride);
11087 repo.server_ = repo.persistentConnection_;
11088 }
11089 repo.authTokenProvider_.addTokenChangeListener(token => {
11090 repo.server_.refreshAuthToken(token);
11091 });
11092 repo.appCheckProvider_.addTokenChangeListener(result => {
11093 repo.server_.refreshAppCheckToken(result.token);
11094 });
11095 // In the case of multiple Repos for the same repoInfo (i.e. there are multiple Firebase.Contexts being used),
11096 // we only want to create one StatsReporter. As such, we'll report stats over the first Repo created.
11097 repo.statsReporter_ = statsManagerGetOrCreateReporter(repo.repoInfo_, () => new StatsReporter(repo.stats_, repo.server_));
11098 // Used for .info.
11099 repo.infoData_ = new SnapshotHolder();
11100 repo.infoSyncTree_ = new SyncTree({
11101 startListening: (query, tag, currentHashFn, onComplete) => {
11102 let infoEvents = [];
11103 const node = repo.infoData_.getNode(query._path);
11104 // This is possibly a hack, but we have different semantics for .info endpoints. We don't raise null events
11105 // on initial data...
11106 if (!node.isEmpty()) {
11107 infoEvents = syncTreeApplyServerOverwrite(repo.infoSyncTree_, query._path, node);
11108 setTimeout(() => {
11109 onComplete('ok');
11110 }, 0);
11111 }
11112 return infoEvents;
11113 },
11114 stopListening: () => { }
11115 });
11116 repoUpdateInfo(repo, 'connected', false);
11117 repo.serverSyncTree_ = new SyncTree({
11118 startListening: (query, tag, currentHashFn, onComplete) => {
11119 repo.server_.listen(query, currentHashFn, tag, (status, data) => {
11120 const events = onComplete(status, data);
11121 eventQueueRaiseEventsForChangedPath(repo.eventQueue_, query._path, events);
11122 });
11123 // No synchronous events for network-backed sync trees
11124 return [];
11125 },
11126 stopListening: (query, tag) => {
11127 repo.server_.unlisten(query, tag);
11128 }
11129 });
11130}
11131/**
11132 * @returns The time in milliseconds, taking the server offset into account if we have one.
11133 */
11134function repoServerTime(repo) {
11135 const offsetNode = repo.infoData_.getNode(new Path('.info/serverTimeOffset'));
11136 const offset = offsetNode.val() || 0;
11137 return new Date().getTime() + offset;
11138}
11139/**
11140 * Generate ServerValues using some variables from the repo object.
11141 */
11142function repoGenerateServerValues(repo) {
11143 return generateWithValues({
11144 timestamp: repoServerTime(repo)
11145 });
11146}
11147/**
11148 * Called by realtime when we get new messages from the server.
11149 */
11150function repoOnDataUpdate(repo, pathString, data, isMerge, tag) {
11151 // For testing.
11152 repo.dataUpdateCount++;
11153 const path = new Path(pathString);
11154 data = repo.interceptServerDataCallback_
11155 ? repo.interceptServerDataCallback_(pathString, data)
11156 : data;
11157 let events = [];
11158 if (tag) {
11159 if (isMerge) {
11160 const taggedChildren = map(data, (raw) => nodeFromJSON(raw));
11161 events = syncTreeApplyTaggedQueryMerge(repo.serverSyncTree_, path, taggedChildren, tag);
11162 }
11163 else {
11164 const taggedSnap = nodeFromJSON(data);
11165 events = syncTreeApplyTaggedQueryOverwrite(repo.serverSyncTree_, path, taggedSnap, tag);
11166 }
11167 }
11168 else if (isMerge) {
11169 const changedChildren = map(data, (raw) => nodeFromJSON(raw));
11170 events = syncTreeApplyServerMerge(repo.serverSyncTree_, path, changedChildren);
11171 }
11172 else {
11173 const snap = nodeFromJSON(data);
11174 events = syncTreeApplyServerOverwrite(repo.serverSyncTree_, path, snap);
11175 }
11176 let affectedPath = path;
11177 if (events.length > 0) {
11178 // Since we have a listener outstanding for each transaction, receiving any events
11179 // is a proxy for some change having occurred.
11180 affectedPath = repoRerunTransactions(repo, path);
11181 }
11182 eventQueueRaiseEventsForChangedPath(repo.eventQueue_, affectedPath, events);
11183}
11184function repoOnConnectStatus(repo, connectStatus) {
11185 repoUpdateInfo(repo, 'connected', connectStatus);
11186 if (connectStatus === false) {
11187 repoRunOnDisconnectEvents(repo);
11188 }
11189}
11190function repoOnServerInfoUpdate(repo, updates) {
11191 each(updates, (key, value) => {
11192 repoUpdateInfo(repo, key, value);
11193 });
11194}
11195function repoUpdateInfo(repo, pathString, value) {
11196 const path = new Path('/.info/' + pathString);
11197 const newNode = nodeFromJSON(value);
11198 repo.infoData_.updateSnapshot(path, newNode);
11199 const events = syncTreeApplyServerOverwrite(repo.infoSyncTree_, path, newNode);
11200 eventQueueRaiseEventsForChangedPath(repo.eventQueue_, path, events);
11201}
11202function repoGetNextWriteId(repo) {
11203 return repo.nextWriteId_++;
11204}
11205/**
11206 * The purpose of `getValue` is to return the latest known value
11207 * satisfying `query`.
11208 *
11209 * This method will first check for in-memory cached values
11210 * belonging to active listeners. If they are found, such values
11211 * are considered to be the most up-to-date.
11212 *
11213 * If the client is not connected, this method will try to
11214 * establish a connection and request the value for `query`. If
11215 * the client is not able to retrieve the query result, it reports
11216 * an error.
11217 *
11218 * @param query - The query to surface a value for.
11219 */
11220function repoGetValue(repo, query) {
11221 // Only active queries are cached. There is no persisted cache.
11222 const cached = syncTreeGetServerValue(repo.serverSyncTree_, query);
11223 if (cached != null) {
11224 return Promise.resolve(cached);
11225 }
11226 return repo.server_.get(query).then(payload => {
11227 const node = nodeFromJSON(payload).withIndex(query._queryParams.getIndex());
11228 // if this is a filtered query, then overwrite at path
11229 if (query._queryParams.loadsAllData()) {
11230 syncTreeApplyServerOverwrite(repo.serverSyncTree_, query._path, node);
11231 }
11232 else {
11233 // Simulate `syncTreeAddEventRegistration` without events/listener setup.
11234 // We do this (along with the syncTreeRemoveEventRegistration` below) so that
11235 // `repoGetValue` results have the same cache effects as initial listener(s)
11236 // updates.
11237 const tag = syncTreeRegisterQuery(repo.serverSyncTree_, query);
11238 syncTreeApplyTaggedQueryOverwrite(repo.serverSyncTree_, query._path, node, tag);
11239 // Call `syncTreeRemoveEventRegistration` with a null event registration, since there is none.
11240 // Note: The below code essentially unregisters the query and cleans up any views/syncpoints temporarily created above.
11241 }
11242 const cancels = syncTreeRemoveEventRegistration(repo.serverSyncTree_, query, null);
11243 if (cancels.length > 0) {
11244 repoLog(repo, 'unexpected cancel events in repoGetValue');
11245 }
11246 return node;
11247 }, err => {
11248 repoLog(repo, 'get for query ' + stringify(query) + ' failed: ' + err);
11249 return Promise.reject(new Error(err));
11250 });
11251}
11252function repoSetWithPriority(repo, path, newVal, newPriority, onComplete) {
11253 repoLog(repo, 'set', {
11254 path: path.toString(),
11255 value: newVal,
11256 priority: newPriority
11257 });
11258 // TODO: Optimize this behavior to either (a) store flag to skip resolving where possible and / or
11259 // (b) store unresolved paths on JSON parse
11260 const serverValues = repoGenerateServerValues(repo);
11261 const newNodeUnresolved = nodeFromJSON(newVal, newPriority);
11262 const existing = syncTreeCalcCompleteEventCache(repo.serverSyncTree_, path);
11263 const newNode = resolveDeferredValueSnapshot(newNodeUnresolved, existing, serverValues);
11264 const writeId = repoGetNextWriteId(repo);
11265 const events = syncTreeApplyUserOverwrite(repo.serverSyncTree_, path, newNode, writeId, true);
11266 eventQueueQueueEvents(repo.eventQueue_, events);
11267 repo.server_.put(path.toString(), newNodeUnresolved.val(/*export=*/ true), (status, errorReason) => {
11268 const success = status === 'ok';
11269 if (!success) {
11270 warn('set at ' + path + ' failed: ' + status);
11271 }
11272 const clearEvents = syncTreeAckUserWrite(repo.serverSyncTree_, writeId, !success);
11273 eventQueueRaiseEventsForChangedPath(repo.eventQueue_, path, clearEvents);
11274 repoCallOnCompleteCallback(repo, onComplete, status, errorReason);
11275 });
11276 const affectedPath = repoAbortTransactions(repo, path);
11277 repoRerunTransactions(repo, affectedPath);
11278 // We queued the events above, so just flush the queue here
11279 eventQueueRaiseEventsForChangedPath(repo.eventQueue_, affectedPath, []);
11280}
11281function repoUpdate(repo, path, childrenToMerge, onComplete) {
11282 repoLog(repo, 'update', { path: path.toString(), value: childrenToMerge });
11283 // Start with our existing data and merge each child into it.
11284 let empty = true;
11285 const serverValues = repoGenerateServerValues(repo);
11286 const changedChildren = {};
11287 each(childrenToMerge, (changedKey, changedValue) => {
11288 empty = false;
11289 changedChildren[changedKey] = resolveDeferredValueTree(pathChild(path, changedKey), nodeFromJSON(changedValue), repo.serverSyncTree_, serverValues);
11290 });
11291 if (!empty) {
11292 const writeId = repoGetNextWriteId(repo);
11293 const events = syncTreeApplyUserMerge(repo.serverSyncTree_, path, changedChildren, writeId);
11294 eventQueueQueueEvents(repo.eventQueue_, events);
11295 repo.server_.merge(path.toString(), childrenToMerge, (status, errorReason) => {
11296 const success = status === 'ok';
11297 if (!success) {
11298 warn('update at ' + path + ' failed: ' + status);
11299 }
11300 const clearEvents = syncTreeAckUserWrite(repo.serverSyncTree_, writeId, !success);
11301 const affectedPath = clearEvents.length > 0 ? repoRerunTransactions(repo, path) : path;
11302 eventQueueRaiseEventsForChangedPath(repo.eventQueue_, affectedPath, clearEvents);
11303 repoCallOnCompleteCallback(repo, onComplete, status, errorReason);
11304 });
11305 each(childrenToMerge, (changedPath) => {
11306 const affectedPath = repoAbortTransactions(repo, pathChild(path, changedPath));
11307 repoRerunTransactions(repo, affectedPath);
11308 });
11309 // We queued the events above, so just flush the queue here
11310 eventQueueRaiseEventsForChangedPath(repo.eventQueue_, path, []);
11311 }
11312 else {
11313 log("update() called with empty data. Don't do anything.");
11314 repoCallOnCompleteCallback(repo, onComplete, 'ok', undefined);
11315 }
11316}
11317/**
11318 * Applies all of the changes stored up in the onDisconnect_ tree.
11319 */
11320function repoRunOnDisconnectEvents(repo) {
11321 repoLog(repo, 'onDisconnectEvents');
11322 const serverValues = repoGenerateServerValues(repo);
11323 const resolvedOnDisconnectTree = newSparseSnapshotTree();
11324 sparseSnapshotTreeForEachTree(repo.onDisconnect_, newEmptyPath(), (path, node) => {
11325 const resolved = resolveDeferredValueTree(path, node, repo.serverSyncTree_, serverValues);
11326 sparseSnapshotTreeRemember(resolvedOnDisconnectTree, path, resolved);
11327 });
11328 let events = [];
11329 sparseSnapshotTreeForEachTree(resolvedOnDisconnectTree, newEmptyPath(), (path, snap) => {
11330 events = events.concat(syncTreeApplyServerOverwrite(repo.serverSyncTree_, path, snap));
11331 const affectedPath = repoAbortTransactions(repo, path);
11332 repoRerunTransactions(repo, affectedPath);
11333 });
11334 repo.onDisconnect_ = newSparseSnapshotTree();
11335 eventQueueRaiseEventsForChangedPath(repo.eventQueue_, newEmptyPath(), events);
11336}
11337function repoOnDisconnectCancel(repo, path, onComplete) {
11338 repo.server_.onDisconnectCancel(path.toString(), (status, errorReason) => {
11339 if (status === 'ok') {
11340 sparseSnapshotTreeForget(repo.onDisconnect_, path);
11341 }
11342 repoCallOnCompleteCallback(repo, onComplete, status, errorReason);
11343 });
11344}
11345function repoOnDisconnectSet(repo, path, value, onComplete) {
11346 const newNode = nodeFromJSON(value);
11347 repo.server_.onDisconnectPut(path.toString(), newNode.val(/*export=*/ true), (status, errorReason) => {
11348 if (status === 'ok') {
11349 sparseSnapshotTreeRemember(repo.onDisconnect_, path, newNode);
11350 }
11351 repoCallOnCompleteCallback(repo, onComplete, status, errorReason);
11352 });
11353}
11354function repoOnDisconnectSetWithPriority(repo, path, value, priority, onComplete) {
11355 const newNode = nodeFromJSON(value, priority);
11356 repo.server_.onDisconnectPut(path.toString(), newNode.val(/*export=*/ true), (status, errorReason) => {
11357 if (status === 'ok') {
11358 sparseSnapshotTreeRemember(repo.onDisconnect_, path, newNode);
11359 }
11360 repoCallOnCompleteCallback(repo, onComplete, status, errorReason);
11361 });
11362}
11363function repoOnDisconnectUpdate(repo, path, childrenToMerge, onComplete) {
11364 if (isEmpty(childrenToMerge)) {
11365 log("onDisconnect().update() called with empty data. Don't do anything.");
11366 repoCallOnCompleteCallback(repo, onComplete, 'ok', undefined);
11367 return;
11368 }
11369 repo.server_.onDisconnectMerge(path.toString(), childrenToMerge, (status, errorReason) => {
11370 if (status === 'ok') {
11371 each(childrenToMerge, (childName, childNode) => {
11372 const newChildNode = nodeFromJSON(childNode);
11373 sparseSnapshotTreeRemember(repo.onDisconnect_, pathChild(path, childName), newChildNode);
11374 });
11375 }
11376 repoCallOnCompleteCallback(repo, onComplete, status, errorReason);
11377 });
11378}
11379function repoAddEventCallbackForQuery(repo, query, eventRegistration) {
11380 let events;
11381 if (pathGetFront(query._path) === '.info') {
11382 events = syncTreeAddEventRegistration(repo.infoSyncTree_, query, eventRegistration);
11383 }
11384 else {
11385 events = syncTreeAddEventRegistration(repo.serverSyncTree_, query, eventRegistration);
11386 }
11387 eventQueueRaiseEventsAtPath(repo.eventQueue_, query._path, events);
11388}
11389function repoRemoveEventCallbackForQuery(repo, query, eventRegistration) {
11390 // These are guaranteed not to raise events, since we're not passing in a cancelError. However, we can future-proof
11391 // a little bit by handling the return values anyways.
11392 let events;
11393 if (pathGetFront(query._path) === '.info') {
11394 events = syncTreeRemoveEventRegistration(repo.infoSyncTree_, query, eventRegistration);
11395 }
11396 else {
11397 events = syncTreeRemoveEventRegistration(repo.serverSyncTree_, query, eventRegistration);
11398 }
11399 eventQueueRaiseEventsAtPath(repo.eventQueue_, query._path, events);
11400}
11401function repoInterrupt(repo) {
11402 if (repo.persistentConnection_) {
11403 repo.persistentConnection_.interrupt(INTERRUPT_REASON);
11404 }
11405}
11406function repoResume(repo) {
11407 if (repo.persistentConnection_) {
11408 repo.persistentConnection_.resume(INTERRUPT_REASON);
11409 }
11410}
11411function repoLog(repo, ...varArgs) {
11412 let prefix = '';
11413 if (repo.persistentConnection_) {
11414 prefix = repo.persistentConnection_.id + ':';
11415 }
11416 log(prefix, ...varArgs);
11417}
11418function repoCallOnCompleteCallback(repo, callback, status, errorReason) {
11419 if (callback) {
11420 exceptionGuard(() => {
11421 if (status === 'ok') {
11422 callback(null);
11423 }
11424 else {
11425 const code = (status || 'error').toUpperCase();
11426 let message = code;
11427 if (errorReason) {
11428 message += ': ' + errorReason;
11429 }
11430 const error = new Error(message);
11431 // eslint-disable-next-line @typescript-eslint/no-explicit-any
11432 error.code = code;
11433 callback(error);
11434 }
11435 });
11436 }
11437}
11438/**
11439 * Creates a new transaction, adds it to the transactions we're tracking, and
11440 * sends it to the server if possible.
11441 *
11442 * @param path - Path at which to do transaction.
11443 * @param transactionUpdate - Update callback.
11444 * @param onComplete - Completion callback.
11445 * @param unwatcher - Function that will be called when the transaction no longer
11446 * need data updates for `path`.
11447 * @param applyLocally - Whether or not to make intermediate results visible
11448 */
11449function repoStartTransaction(repo, path, transactionUpdate, onComplete, unwatcher, applyLocally) {
11450 repoLog(repo, 'transaction on ' + path);
11451 // Initialize transaction.
11452 const transaction = {
11453 path,
11454 update: transactionUpdate,
11455 onComplete,
11456 // One of TransactionStatus enums.
11457 status: null,
11458 // Used when combining transactions at different locations to figure out
11459 // which one goes first.
11460 order: LUIDGenerator(),
11461 // Whether to raise local events for this transaction.
11462 applyLocally,
11463 // Count of how many times we've retried the transaction.
11464 retryCount: 0,
11465 // Function to call to clean up our .on() listener.
11466 unwatcher,
11467 // Stores why a transaction was aborted.
11468 abortReason: null,
11469 currentWriteId: null,
11470 currentInputSnapshot: null,
11471 currentOutputSnapshotRaw: null,
11472 currentOutputSnapshotResolved: null
11473 };
11474 // Run transaction initially.
11475 const currentState = repoGetLatestState(repo, path, undefined);
11476 transaction.currentInputSnapshot = currentState;
11477 const newVal = transaction.update(currentState.val());
11478 if (newVal === undefined) {
11479 // Abort transaction.
11480 transaction.unwatcher();
11481 transaction.currentOutputSnapshotRaw = null;
11482 transaction.currentOutputSnapshotResolved = null;
11483 if (transaction.onComplete) {
11484 transaction.onComplete(null, false, transaction.currentInputSnapshot);
11485 }
11486 }
11487 else {
11488 validateFirebaseData('transaction failed: Data returned ', newVal, transaction.path);
11489 // Mark as run and add to our queue.
11490 transaction.status = 0 /* RUN */;
11491 const queueNode = treeSubTree(repo.transactionQueueTree_, path);
11492 const nodeQueue = treeGetValue(queueNode) || [];
11493 nodeQueue.push(transaction);
11494 treeSetValue(queueNode, nodeQueue);
11495 // Update visibleData and raise events
11496 // Note: We intentionally raise events after updating all of our
11497 // transaction state, since the user could start new transactions from the
11498 // event callbacks.
11499 let priorityForNode;
11500 if (typeof newVal === 'object' &&
11501 newVal !== null &&
11502 contains(newVal, '.priority')) {
11503 // eslint-disable-next-line @typescript-eslint/no-explicit-any
11504 priorityForNode = safeGet(newVal, '.priority');
11505 assert(isValidPriority(priorityForNode), 'Invalid priority returned by transaction. ' +
11506 'Priority must be a valid string, finite number, server value, or null.');
11507 }
11508 else {
11509 const currentNode = syncTreeCalcCompleteEventCache(repo.serverSyncTree_, path) ||
11510 ChildrenNode.EMPTY_NODE;
11511 priorityForNode = currentNode.getPriority().val();
11512 }
11513 const serverValues = repoGenerateServerValues(repo);
11514 const newNodeUnresolved = nodeFromJSON(newVal, priorityForNode);
11515 const newNode = resolveDeferredValueSnapshot(newNodeUnresolved, currentState, serverValues);
11516 transaction.currentOutputSnapshotRaw = newNodeUnresolved;
11517 transaction.currentOutputSnapshotResolved = newNode;
11518 transaction.currentWriteId = repoGetNextWriteId(repo);
11519 const events = syncTreeApplyUserOverwrite(repo.serverSyncTree_, path, newNode, transaction.currentWriteId, transaction.applyLocally);
11520 eventQueueRaiseEventsForChangedPath(repo.eventQueue_, path, events);
11521 repoSendReadyTransactions(repo, repo.transactionQueueTree_);
11522 }
11523}
11524/**
11525 * @param excludeSets - A specific set to exclude
11526 */
11527function repoGetLatestState(repo, path, excludeSets) {
11528 return (syncTreeCalcCompleteEventCache(repo.serverSyncTree_, path, excludeSets) ||
11529 ChildrenNode.EMPTY_NODE);
11530}
11531/**
11532 * Sends any already-run transactions that aren't waiting for outstanding
11533 * transactions to complete.
11534 *
11535 * Externally it's called with no arguments, but it calls itself recursively
11536 * with a particular transactionQueueTree node to recurse through the tree.
11537 *
11538 * @param node - transactionQueueTree node to start at.
11539 */
11540function repoSendReadyTransactions(repo, node = repo.transactionQueueTree_) {
11541 // Before recursing, make sure any completed transactions are removed.
11542 if (!node) {
11543 repoPruneCompletedTransactionsBelowNode(repo, node);
11544 }
11545 if (treeGetValue(node)) {
11546 const queue = repoBuildTransactionQueue(repo, node);
11547 assert(queue.length > 0, 'Sending zero length transaction queue');
11548 const allRun = queue.every((transaction) => transaction.status === 0 /* RUN */);
11549 // If they're all run (and not sent), we can send them. Else, we must wait.
11550 if (allRun) {
11551 repoSendTransactionQueue(repo, treeGetPath(node), queue);
11552 }
11553 }
11554 else if (treeHasChildren(node)) {
11555 treeForEachChild(node, childNode => {
11556 repoSendReadyTransactions(repo, childNode);
11557 });
11558 }
11559}
11560/**
11561 * Given a list of run transactions, send them to the server and then handle
11562 * the result (success or failure).
11563 *
11564 * @param path - The location of the queue.
11565 * @param queue - Queue of transactions under the specified location.
11566 */
11567function repoSendTransactionQueue(repo, path, queue) {
11568 // Mark transactions as sent and increment retry count!
11569 const setsToIgnore = queue.map(txn => {
11570 return txn.currentWriteId;
11571 });
11572 const latestState = repoGetLatestState(repo, path, setsToIgnore);
11573 let snapToSend = latestState;
11574 const latestHash = latestState.hash();
11575 for (let i = 0; i < queue.length; i++) {
11576 const txn = queue[i];
11577 assert(txn.status === 0 /* RUN */, 'tryToSendTransactionQueue_: items in queue should all be run.');
11578 txn.status = 1 /* SENT */;
11579 txn.retryCount++;
11580 const relativePath = newRelativePath(path, txn.path);
11581 // If we've gotten to this point, the output snapshot must be defined.
11582 snapToSend = snapToSend.updateChild(relativePath /** @type {!Node} */, txn.currentOutputSnapshotRaw);
11583 }
11584 const dataToSend = snapToSend.val(true);
11585 const pathToSend = path;
11586 // Send the put.
11587 repo.server_.put(pathToSend.toString(), dataToSend, (status) => {
11588 repoLog(repo, 'transaction put response', {
11589 path: pathToSend.toString(),
11590 status
11591 });
11592 let events = [];
11593 if (status === 'ok') {
11594 // Queue up the callbacks and fire them after cleaning up all of our
11595 // transaction state, since the callback could trigger more
11596 // transactions or sets.
11597 const callbacks = [];
11598 for (let i = 0; i < queue.length; i++) {
11599 queue[i].status = 2 /* COMPLETED */;
11600 events = events.concat(syncTreeAckUserWrite(repo.serverSyncTree_, queue[i].currentWriteId));
11601 if (queue[i].onComplete) {
11602 // We never unset the output snapshot, and given that this
11603 // transaction is complete, it should be set
11604 callbacks.push(() => queue[i].onComplete(null, true, queue[i].currentOutputSnapshotResolved));
11605 }
11606 queue[i].unwatcher();
11607 }
11608 // Now remove the completed transactions.
11609 repoPruneCompletedTransactionsBelowNode(repo, treeSubTree(repo.transactionQueueTree_, path));
11610 // There may be pending transactions that we can now send.
11611 repoSendReadyTransactions(repo, repo.transactionQueueTree_);
11612 eventQueueRaiseEventsForChangedPath(repo.eventQueue_, path, events);
11613 // Finally, trigger onComplete callbacks.
11614 for (let i = 0; i < callbacks.length; i++) {
11615 exceptionGuard(callbacks[i]);
11616 }
11617 }
11618 else {
11619 // transactions are no longer sent. Update their status appropriately.
11620 if (status === 'datastale') {
11621 for (let i = 0; i < queue.length; i++) {
11622 if (queue[i].status === 3 /* SENT_NEEDS_ABORT */) {
11623 queue[i].status = 4 /* NEEDS_ABORT */;
11624 }
11625 else {
11626 queue[i].status = 0 /* RUN */;
11627 }
11628 }
11629 }
11630 else {
11631 warn('transaction at ' + pathToSend.toString() + ' failed: ' + status);
11632 for (let i = 0; i < queue.length; i++) {
11633 queue[i].status = 4 /* NEEDS_ABORT */;
11634 queue[i].abortReason = status;
11635 }
11636 }
11637 repoRerunTransactions(repo, path);
11638 }
11639 }, latestHash);
11640}
11641/**
11642 * Finds all transactions dependent on the data at changedPath and reruns them.
11643 *
11644 * Should be called any time cached data changes.
11645 *
11646 * Return the highest path that was affected by rerunning transactions. This
11647 * is the path at which events need to be raised for.
11648 *
11649 * @param changedPath - The path in mergedData that changed.
11650 * @returns The rootmost path that was affected by rerunning transactions.
11651 */
11652function repoRerunTransactions(repo, changedPath) {
11653 const rootMostTransactionNode = repoGetAncestorTransactionNode(repo, changedPath);
11654 const path = treeGetPath(rootMostTransactionNode);
11655 const queue = repoBuildTransactionQueue(repo, rootMostTransactionNode);
11656 repoRerunTransactionQueue(repo, queue, path);
11657 return path;
11658}
11659/**
11660 * Does all the work of rerunning transactions (as well as cleans up aborted
11661 * transactions and whatnot).
11662 *
11663 * @param queue - The queue of transactions to run.
11664 * @param path - The path the queue is for.
11665 */
11666function repoRerunTransactionQueue(repo, queue, path) {
11667 if (queue.length === 0) {
11668 return; // Nothing to do!
11669 }
11670 // Queue up the callbacks and fire them after cleaning up all of our
11671 // transaction state, since the callback could trigger more transactions or
11672 // sets.
11673 const callbacks = [];
11674 let events = [];
11675 // Ignore all of the sets we're going to re-run.
11676 const txnsToRerun = queue.filter(q => {
11677 return q.status === 0 /* RUN */;
11678 });
11679 const setsToIgnore = txnsToRerun.map(q => {
11680 return q.currentWriteId;
11681 });
11682 for (let i = 0; i < queue.length; i++) {
11683 const transaction = queue[i];
11684 const relativePath = newRelativePath(path, transaction.path);
11685 let abortTransaction = false, abortReason;
11686 assert(relativePath !== null, 'rerunTransactionsUnderNode_: relativePath should not be null.');
11687 if (transaction.status === 4 /* NEEDS_ABORT */) {
11688 abortTransaction = true;
11689 abortReason = transaction.abortReason;
11690 events = events.concat(syncTreeAckUserWrite(repo.serverSyncTree_, transaction.currentWriteId, true));
11691 }
11692 else if (transaction.status === 0 /* RUN */) {
11693 if (transaction.retryCount >= MAX_TRANSACTION_RETRIES) {
11694 abortTransaction = true;
11695 abortReason = 'maxretry';
11696 events = events.concat(syncTreeAckUserWrite(repo.serverSyncTree_, transaction.currentWriteId, true));
11697 }
11698 else {
11699 // This code reruns a transaction
11700 const currentNode = repoGetLatestState(repo, transaction.path, setsToIgnore);
11701 transaction.currentInputSnapshot = currentNode;
11702 const newData = queue[i].update(currentNode.val());
11703 if (newData !== undefined) {
11704 validateFirebaseData('transaction failed: Data returned ', newData, transaction.path);
11705 let newDataNode = nodeFromJSON(newData);
11706 const hasExplicitPriority = typeof newData === 'object' &&
11707 newData != null &&
11708 contains(newData, '.priority');
11709 if (!hasExplicitPriority) {
11710 // Keep the old priority if there wasn't a priority explicitly specified.
11711 newDataNode = newDataNode.updatePriority(currentNode.getPriority());
11712 }
11713 const oldWriteId = transaction.currentWriteId;
11714 const serverValues = repoGenerateServerValues(repo);
11715 const newNodeResolved = resolveDeferredValueSnapshot(newDataNode, currentNode, serverValues);
11716 transaction.currentOutputSnapshotRaw = newDataNode;
11717 transaction.currentOutputSnapshotResolved = newNodeResolved;
11718 transaction.currentWriteId = repoGetNextWriteId(repo);
11719 // Mutates setsToIgnore in place
11720 setsToIgnore.splice(setsToIgnore.indexOf(oldWriteId), 1);
11721 events = events.concat(syncTreeApplyUserOverwrite(repo.serverSyncTree_, transaction.path, newNodeResolved, transaction.currentWriteId, transaction.applyLocally));
11722 events = events.concat(syncTreeAckUserWrite(repo.serverSyncTree_, oldWriteId, true));
11723 }
11724 else {
11725 abortTransaction = true;
11726 abortReason = 'nodata';
11727 events = events.concat(syncTreeAckUserWrite(repo.serverSyncTree_, transaction.currentWriteId, true));
11728 }
11729 }
11730 }
11731 eventQueueRaiseEventsForChangedPath(repo.eventQueue_, path, events);
11732 events = [];
11733 if (abortTransaction) {
11734 // Abort.
11735 queue[i].status = 2 /* COMPLETED */;
11736 // Removing a listener can trigger pruning which can muck with
11737 // mergedData/visibleData (as it prunes data). So defer the unwatcher
11738 // until we're done.
11739 (function (unwatcher) {
11740 setTimeout(unwatcher, Math.floor(0));
11741 })(queue[i].unwatcher);
11742 if (queue[i].onComplete) {
11743 if (abortReason === 'nodata') {
11744 callbacks.push(() => queue[i].onComplete(null, false, queue[i].currentInputSnapshot));
11745 }
11746 else {
11747 callbacks.push(() => queue[i].onComplete(new Error(abortReason), false, null));
11748 }
11749 }
11750 }
11751 }
11752 // Clean up completed transactions.
11753 repoPruneCompletedTransactionsBelowNode(repo, repo.transactionQueueTree_);
11754 // Now fire callbacks, now that we're in a good, known state.
11755 for (let i = 0; i < callbacks.length; i++) {
11756 exceptionGuard(callbacks[i]);
11757 }
11758 // Try to send the transaction result to the server.
11759 repoSendReadyTransactions(repo, repo.transactionQueueTree_);
11760}
11761/**
11762 * Returns the rootmost ancestor node of the specified path that has a pending
11763 * transaction on it, or just returns the node for the given path if there are
11764 * no pending transactions on any ancestor.
11765 *
11766 * @param path - The location to start at.
11767 * @returns The rootmost node with a transaction.
11768 */
11769function repoGetAncestorTransactionNode(repo, path) {
11770 let front;
11771 // Start at the root and walk deeper into the tree towards path until we
11772 // find a node with pending transactions.
11773 let transactionNode = repo.transactionQueueTree_;
11774 front = pathGetFront(path);
11775 while (front !== null && treeGetValue(transactionNode) === undefined) {
11776 transactionNode = treeSubTree(transactionNode, front);
11777 path = pathPopFront(path);
11778 front = pathGetFront(path);
11779 }
11780 return transactionNode;
11781}
11782/**
11783 * Builds the queue of all transactions at or below the specified
11784 * transactionNode.
11785 *
11786 * @param transactionNode
11787 * @returns The generated queue.
11788 */
11789function repoBuildTransactionQueue(repo, transactionNode) {
11790 // Walk any child transaction queues and aggregate them into a single queue.
11791 const transactionQueue = [];
11792 repoAggregateTransactionQueuesForNode(repo, transactionNode, transactionQueue);
11793 // Sort them by the order the transactions were created.
11794 transactionQueue.sort((a, b) => a.order - b.order);
11795 return transactionQueue;
11796}
11797function repoAggregateTransactionQueuesForNode(repo, node, queue) {
11798 const nodeQueue = treeGetValue(node);
11799 if (nodeQueue) {
11800 for (let i = 0; i < nodeQueue.length; i++) {
11801 queue.push(nodeQueue[i]);
11802 }
11803 }
11804 treeForEachChild(node, child => {
11805 repoAggregateTransactionQueuesForNode(repo, child, queue);
11806 });
11807}
11808/**
11809 * Remove COMPLETED transactions at or below this node in the transactionQueueTree_.
11810 */
11811function repoPruneCompletedTransactionsBelowNode(repo, node) {
11812 const queue = treeGetValue(node);
11813 if (queue) {
11814 let to = 0;
11815 for (let from = 0; from < queue.length; from++) {
11816 if (queue[from].status !== 2 /* COMPLETED */) {
11817 queue[to] = queue[from];
11818 to++;
11819 }
11820 }
11821 queue.length = to;
11822 treeSetValue(node, queue.length > 0 ? queue : undefined);
11823 }
11824 treeForEachChild(node, childNode => {
11825 repoPruneCompletedTransactionsBelowNode(repo, childNode);
11826 });
11827}
11828/**
11829 * Aborts all transactions on ancestors or descendants of the specified path.
11830 * Called when doing a set() or update() since we consider them incompatible
11831 * with transactions.
11832 *
11833 * @param path - Path for which we want to abort related transactions.
11834 */
11835function repoAbortTransactions(repo, path) {
11836 const affectedPath = treeGetPath(repoGetAncestorTransactionNode(repo, path));
11837 const transactionNode = treeSubTree(repo.transactionQueueTree_, path);
11838 treeForEachAncestor(transactionNode, (node) => {
11839 repoAbortTransactionsOnNode(repo, node);
11840 });
11841 repoAbortTransactionsOnNode(repo, transactionNode);
11842 treeForEachDescendant(transactionNode, (node) => {
11843 repoAbortTransactionsOnNode(repo, node);
11844 });
11845 return affectedPath;
11846}
11847/**
11848 * Abort transactions stored in this transaction queue node.
11849 *
11850 * @param node - Node to abort transactions for.
11851 */
11852function repoAbortTransactionsOnNode(repo, node) {
11853 const queue = treeGetValue(node);
11854 if (queue) {
11855 // Queue up the callbacks and fire them after cleaning up all of our
11856 // transaction state, since the callback could trigger more transactions
11857 // or sets.
11858 const callbacks = [];
11859 // Go through queue. Any already-sent transactions must be marked for
11860 // abort, while the unsent ones can be immediately aborted and removed.
11861 let events = [];
11862 let lastSent = -1;
11863 for (let i = 0; i < queue.length; i++) {
11864 if (queue[i].status === 3 /* SENT_NEEDS_ABORT */) ;
11865 else if (queue[i].status === 1 /* SENT */) {
11866 assert(lastSent === i - 1, 'All SENT items should be at beginning of queue.');
11867 lastSent = i;
11868 // Mark transaction for abort when it comes back.
11869 queue[i].status = 3 /* SENT_NEEDS_ABORT */;
11870 queue[i].abortReason = 'set';
11871 }
11872 else {
11873 assert(queue[i].status === 0 /* RUN */, 'Unexpected transaction status in abort');
11874 // We can abort it immediately.
11875 queue[i].unwatcher();
11876 events = events.concat(syncTreeAckUserWrite(repo.serverSyncTree_, queue[i].currentWriteId, true));
11877 if (queue[i].onComplete) {
11878 callbacks.push(queue[i].onComplete.bind(null, new Error('set'), false, null));
11879 }
11880 }
11881 }
11882 if (lastSent === -1) {
11883 // We're not waiting for any sent transactions. We can clear the queue.
11884 treeSetValue(node, undefined);
11885 }
11886 else {
11887 // Remove the transactions we aborted.
11888 queue.length = lastSent + 1;
11889 }
11890 // Now fire the callbacks.
11891 eventQueueRaiseEventsForChangedPath(repo.eventQueue_, treeGetPath(node), events);
11892 for (let i = 0; i < callbacks.length; i++) {
11893 exceptionGuard(callbacks[i]);
11894 }
11895 }
11896}
11897
11898/**
11899 * @license
11900 * Copyright 2017 Google LLC
11901 *
11902 * Licensed under the Apache License, Version 2.0 (the "License");
11903 * you may not use this file except in compliance with the License.
11904 * You may obtain a copy of the License at
11905 *
11906 * http://www.apache.org/licenses/LICENSE-2.0
11907 *
11908 * Unless required by applicable law or agreed to in writing, software
11909 * distributed under the License is distributed on an "AS IS" BASIS,
11910 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11911 * See the License for the specific language governing permissions and
11912 * limitations under the License.
11913 */
11914function decodePath(pathString) {
11915 let pathStringDecoded = '';
11916 const pieces = pathString.split('/');
11917 for (let i = 0; i < pieces.length; i++) {
11918 if (pieces[i].length > 0) {
11919 let piece = pieces[i];
11920 try {
11921 piece = decodeURIComponent(piece.replace(/\+/g, ' '));
11922 }
11923 catch (e) { }
11924 pathStringDecoded += '/' + piece;
11925 }
11926 }
11927 return pathStringDecoded;
11928}
11929/**
11930 * @returns key value hash
11931 */
11932function decodeQuery(queryString) {
11933 const results = {};
11934 if (queryString.charAt(0) === '?') {
11935 queryString = queryString.substring(1);
11936 }
11937 for (const segment of queryString.split('&')) {
11938 if (segment.length === 0) {
11939 continue;
11940 }
11941 const kv = segment.split('=');
11942 if (kv.length === 2) {
11943 results[decodeURIComponent(kv[0])] = decodeURIComponent(kv[1]);
11944 }
11945 else {
11946 warn(`Invalid query segment '${segment}' in query '${queryString}'`);
11947 }
11948 }
11949 return results;
11950}
11951const parseRepoInfo = function (dataURL, nodeAdmin) {
11952 const parsedUrl = parseDatabaseURL(dataURL), namespace = parsedUrl.namespace;
11953 if (parsedUrl.domain === 'firebase.com') {
11954 fatal(parsedUrl.host +
11955 ' is no longer supported. ' +
11956 'Please use <YOUR FIREBASE>.firebaseio.com instead');
11957 }
11958 // Catch common error of uninitialized namespace value.
11959 if ((!namespace || namespace === 'undefined') &&
11960 parsedUrl.domain !== 'localhost') {
11961 fatal('Cannot parse Firebase url. Please use https://<YOUR FIREBASE>.firebaseio.com');
11962 }
11963 if (!parsedUrl.secure) {
11964 warnIfPageIsSecure();
11965 }
11966 const webSocketOnly = parsedUrl.scheme === 'ws' || parsedUrl.scheme === 'wss';
11967 return {
11968 repoInfo: new RepoInfo(parsedUrl.host, parsedUrl.secure, namespace, webSocketOnly, nodeAdmin,
11969 /*persistenceKey=*/ '',
11970 /*includeNamespaceInQueryParams=*/ namespace !== parsedUrl.subdomain),
11971 path: new Path(parsedUrl.pathString)
11972 };
11973};
11974const parseDatabaseURL = function (dataURL) {
11975 // Default to empty strings in the event of a malformed string.
11976 let host = '', domain = '', subdomain = '', pathString = '', namespace = '';
11977 // Always default to SSL, unless otherwise specified.
11978 let secure = true, scheme = 'https', port = 443;
11979 // Don't do any validation here. The caller is responsible for validating the result of parsing.
11980 if (typeof dataURL === 'string') {
11981 // Parse scheme.
11982 let colonInd = dataURL.indexOf('//');
11983 if (colonInd >= 0) {
11984 scheme = dataURL.substring(0, colonInd - 1);
11985 dataURL = dataURL.substring(colonInd + 2);
11986 }
11987 // Parse host, path, and query string.
11988 let slashInd = dataURL.indexOf('/');
11989 if (slashInd === -1) {
11990 slashInd = dataURL.length;
11991 }
11992 let questionMarkInd = dataURL.indexOf('?');
11993 if (questionMarkInd === -1) {
11994 questionMarkInd = dataURL.length;
11995 }
11996 host = dataURL.substring(0, Math.min(slashInd, questionMarkInd));
11997 if (slashInd < questionMarkInd) {
11998 // For pathString, questionMarkInd will always come after slashInd
11999 pathString = decodePath(dataURL.substring(slashInd, questionMarkInd));
12000 }
12001 const queryParams = decodeQuery(dataURL.substring(Math.min(dataURL.length, questionMarkInd)));
12002 // If we have a port, use scheme for determining if it's secure.
12003 colonInd = host.indexOf(':');
12004 if (colonInd >= 0) {
12005 secure = scheme === 'https' || scheme === 'wss';
12006 port = parseInt(host.substring(colonInd + 1), 10);
12007 }
12008 else {
12009 colonInd = host.length;
12010 }
12011 const hostWithoutPort = host.slice(0, colonInd);
12012 if (hostWithoutPort.toLowerCase() === 'localhost') {
12013 domain = 'localhost';
12014 }
12015 else if (hostWithoutPort.split('.').length <= 2) {
12016 domain = hostWithoutPort;
12017 }
12018 else {
12019 // Interpret the subdomain of a 3 or more component URL as the namespace name.
12020 const dotInd = host.indexOf('.');
12021 subdomain = host.substring(0, dotInd).toLowerCase();
12022 domain = host.substring(dotInd + 1);
12023 // Normalize namespaces to lowercase to share storage / connection.
12024 namespace = subdomain;
12025 }
12026 // Always treat the value of the `ns` as the namespace name if it is present.
12027 if ('ns' in queryParams) {
12028 namespace = queryParams['ns'];
12029 }
12030 }
12031 return {
12032 host,
12033 port,
12034 domain,
12035 subdomain,
12036 secure,
12037 scheme,
12038 pathString,
12039 namespace
12040 };
12041};
12042
12043/**
12044 * @license
12045 * Copyright 2017 Google LLC
12046 *
12047 * Licensed under the Apache License, Version 2.0 (the "License");
12048 * you may not use this file except in compliance with the License.
12049 * You may obtain a copy of the License at
12050 *
12051 * http://www.apache.org/licenses/LICENSE-2.0
12052 *
12053 * Unless required by applicable law or agreed to in writing, software
12054 * distributed under the License is distributed on an "AS IS" BASIS,
12055 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12056 * See the License for the specific language governing permissions and
12057 * limitations under the License.
12058 */
12059/**
12060 * Encapsulates the data needed to raise an event
12061 */
12062class DataEvent {
12063 /**
12064 * @param eventType - One of: value, child_added, child_changed, child_moved, child_removed
12065 * @param eventRegistration - The function to call to with the event data. User provided
12066 * @param snapshot - The data backing the event
12067 * @param prevName - Optional, the name of the previous child for child_* events.
12068 */
12069 constructor(eventType, eventRegistration, snapshot, prevName) {
12070 this.eventType = eventType;
12071 this.eventRegistration = eventRegistration;
12072 this.snapshot = snapshot;
12073 this.prevName = prevName;
12074 }
12075 getPath() {
12076 const ref = this.snapshot.ref;
12077 if (this.eventType === 'value') {
12078 return ref._path;
12079 }
12080 else {
12081 return ref.parent._path;
12082 }
12083 }
12084 getEventType() {
12085 return this.eventType;
12086 }
12087 getEventRunner() {
12088 return this.eventRegistration.getEventRunner(this);
12089 }
12090 toString() {
12091 return (this.getPath().toString() +
12092 ':' +
12093 this.eventType +
12094 ':' +
12095 stringify(this.snapshot.exportVal()));
12096 }
12097}
12098class CancelEvent {
12099 constructor(eventRegistration, error, path) {
12100 this.eventRegistration = eventRegistration;
12101 this.error = error;
12102 this.path = path;
12103 }
12104 getPath() {
12105 return this.path;
12106 }
12107 getEventType() {
12108 return 'cancel';
12109 }
12110 getEventRunner() {
12111 return this.eventRegistration.getEventRunner(this);
12112 }
12113 toString() {
12114 return this.path.toString() + ':cancel';
12115 }
12116}
12117
12118/**
12119 * @license
12120 * Copyright 2017 Google LLC
12121 *
12122 * Licensed under the Apache License, Version 2.0 (the "License");
12123 * you may not use this file except in compliance with the License.
12124 * You may obtain a copy of the License at
12125 *
12126 * http://www.apache.org/licenses/LICENSE-2.0
12127 *
12128 * Unless required by applicable law or agreed to in writing, software
12129 * distributed under the License is distributed on an "AS IS" BASIS,
12130 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12131 * See the License for the specific language governing permissions and
12132 * limitations under the License.
12133 */
12134/**
12135 * A wrapper class that converts events from the database@exp SDK to the legacy
12136 * Database SDK. Events are not converted directly as event registration relies
12137 * on reference comparison of the original user callback (see `matches()`) and
12138 * relies on equality of the legacy SDK's `context` object.
12139 */
12140class CallbackContext {
12141 constructor(snapshotCallback, cancelCallback) {
12142 this.snapshotCallback = snapshotCallback;
12143 this.cancelCallback = cancelCallback;
12144 }
12145 onValue(expDataSnapshot, previousChildName) {
12146 this.snapshotCallback.call(null, expDataSnapshot, previousChildName);
12147 }
12148 onCancel(error) {
12149 assert(this.hasCancelCallback, 'Raising a cancel event on a listener with no cancel callback');
12150 return this.cancelCallback.call(null, error);
12151 }
12152 get hasCancelCallback() {
12153 return !!this.cancelCallback;
12154 }
12155 matches(other) {
12156 return (this.snapshotCallback === other.snapshotCallback ||
12157 (this.snapshotCallback.userCallback !== undefined &&
12158 this.snapshotCallback.userCallback ===
12159 other.snapshotCallback.userCallback &&
12160 this.snapshotCallback.context === other.snapshotCallback.context));
12161 }
12162}
12163
12164/**
12165 * @license
12166 * Copyright 2021 Google LLC
12167 *
12168 * Licensed under the Apache License, Version 2.0 (the "License");
12169 * you may not use this file except in compliance with the License.
12170 * You may obtain a copy of the License at
12171 *
12172 * http://www.apache.org/licenses/LICENSE-2.0
12173 *
12174 * Unless required by applicable law or agreed to in writing, software
12175 * distributed under the License is distributed on an "AS IS" BASIS,
12176 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12177 * See the License for the specific language governing permissions and
12178 * limitations under the License.
12179 */
12180/**
12181 * The `onDisconnect` class allows you to write or clear data when your client
12182 * disconnects from the Database server. These updates occur whether your
12183 * client disconnects cleanly or not, so you can rely on them to clean up data
12184 * even if a connection is dropped or a client crashes.
12185 *
12186 * The `onDisconnect` class is most commonly used to manage presence in
12187 * applications where it is useful to detect how many clients are connected and
12188 * when other clients disconnect. See
12189 * {@link https://firebase.google.com/docs/database/web/offline-capabilities | Enabling Offline Capabilities in JavaScript}
12190 * for more information.
12191 *
12192 * To avoid problems when a connection is dropped before the requests can be
12193 * transferred to the Database server, these functions should be called before
12194 * writing any data.
12195 *
12196 * Note that `onDisconnect` operations are only triggered once. If you want an
12197 * operation to occur each time a disconnect occurs, you'll need to re-establish
12198 * the `onDisconnect` operations each time you reconnect.
12199 */
12200class OnDisconnect {
12201 /** @hideconstructor */
12202 constructor(_repo, _path) {
12203 this._repo = _repo;
12204 this._path = _path;
12205 }
12206 /**
12207 * Cancels all previously queued `onDisconnect()` set or update events for this
12208 * location and all children.
12209 *
12210 * If a write has been queued for this location via a `set()` or `update()` at a
12211 * parent location, the write at this location will be canceled, though writes
12212 * to sibling locations will still occur.
12213 *
12214 * @returns Resolves when synchronization to the server is complete.
12215 */
12216 cancel() {
12217 const deferred = new Deferred();
12218 repoOnDisconnectCancel(this._repo, this._path, deferred.wrapCallback(() => { }));
12219 return deferred.promise;
12220 }
12221 /**
12222 * Ensures the data at this location is deleted when the client is disconnected
12223 * (due to closing the browser, navigating to a new page, or network issues).
12224 *
12225 * @returns Resolves when synchronization to the server is complete.
12226 */
12227 remove() {
12228 validateWritablePath('OnDisconnect.remove', this._path);
12229 const deferred = new Deferred();
12230 repoOnDisconnectSet(this._repo, this._path, null, deferred.wrapCallback(() => { }));
12231 return deferred.promise;
12232 }
12233 /**
12234 * Ensures the data at this location is set to the specified value when the
12235 * client is disconnected (due to closing the browser, navigating to a new page,
12236 * or network issues).
12237 *
12238 * `set()` is especially useful for implementing "presence" systems, where a
12239 * value should be changed or cleared when a user disconnects so that they
12240 * appear "offline" to other users. See
12241 * {@link https://firebase.google.com/docs/database/web/offline-capabilities | Enabling Offline Capabilities in JavaScript}
12242 * for more information.
12243 *
12244 * Note that `onDisconnect` operations are only triggered once. If you want an
12245 * operation to occur each time a disconnect occurs, you'll need to re-establish
12246 * the `onDisconnect` operations each time.
12247 *
12248 * @param value - The value to be written to this location on disconnect (can
12249 * be an object, array, string, number, boolean, or null).
12250 * @returns Resolves when synchronization to the Database is complete.
12251 */
12252 set(value) {
12253 validateWritablePath('OnDisconnect.set', this._path);
12254 validateFirebaseDataArg('OnDisconnect.set', value, this._path, false);
12255 const deferred = new Deferred();
12256 repoOnDisconnectSet(this._repo, this._path, value, deferred.wrapCallback(() => { }));
12257 return deferred.promise;
12258 }
12259 /**
12260 * Ensures the data at this location is set to the specified value and priority
12261 * when the client is disconnected (due to closing the browser, navigating to a
12262 * new page, or network issues).
12263 *
12264 * @param value - The value to be written to this location on disconnect (can
12265 * be an object, array, string, number, boolean, or null).
12266 * @param priority - The priority to be written (string, number, or null).
12267 * @returns Resolves when synchronization to the Database is complete.
12268 */
12269 setWithPriority(value, priority) {
12270 validateWritablePath('OnDisconnect.setWithPriority', this._path);
12271 validateFirebaseDataArg('OnDisconnect.setWithPriority', value, this._path, false);
12272 validatePriority('OnDisconnect.setWithPriority', priority, false);
12273 const deferred = new Deferred();
12274 repoOnDisconnectSetWithPriority(this._repo, this._path, value, priority, deferred.wrapCallback(() => { }));
12275 return deferred.promise;
12276 }
12277 /**
12278 * Writes multiple values at this location when the client is disconnected (due
12279 * to closing the browser, navigating to a new page, or network issues).
12280 *
12281 * The `values` argument contains multiple property-value pairs that will be
12282 * written to the Database together. Each child property can either be a simple
12283 * property (for example, "name") or a relative path (for example, "name/first")
12284 * from the current location to the data to update.
12285 *
12286 * As opposed to the `set()` method, `update()` can be use to selectively update
12287 * only the referenced properties at the current location (instead of replacing
12288 * all the child properties at the current location).
12289 *
12290 * @param values - Object containing multiple values.
12291 * @returns Resolves when synchronization to the Database is complete.
12292 */
12293 update(values) {
12294 validateWritablePath('OnDisconnect.update', this._path);
12295 validateFirebaseMergeDataArg('OnDisconnect.update', values, this._path, false);
12296 const deferred = new Deferred();
12297 repoOnDisconnectUpdate(this._repo, this._path, values, deferred.wrapCallback(() => { }));
12298 return deferred.promise;
12299 }
12300}
12301
12302/**
12303 * @license
12304 * Copyright 2020 Google LLC
12305 *
12306 * Licensed under the Apache License, Version 2.0 (the "License");
12307 * you may not use this file except in compliance with the License.
12308 * You may obtain a copy of the License at
12309 *
12310 * http://www.apache.org/licenses/LICENSE-2.0
12311 *
12312 * Unless required by applicable law or agreed to in writing, software
12313 * distributed under the License is distributed on an "AS IS" BASIS,
12314 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12315 * See the License for the specific language governing permissions and
12316 * limitations under the License.
12317 */
12318/**
12319 * @internal
12320 */
12321class QueryImpl {
12322 /**
12323 * @hideconstructor
12324 */
12325 constructor(_repo, _path, _queryParams, _orderByCalled) {
12326 this._repo = _repo;
12327 this._path = _path;
12328 this._queryParams = _queryParams;
12329 this._orderByCalled = _orderByCalled;
12330 }
12331 get key() {
12332 if (pathIsEmpty(this._path)) {
12333 return null;
12334 }
12335 else {
12336 return pathGetBack(this._path);
12337 }
12338 }
12339 get ref() {
12340 return new ReferenceImpl(this._repo, this._path);
12341 }
12342 get _queryIdentifier() {
12343 const obj = queryParamsGetQueryObject(this._queryParams);
12344 const id = ObjectToUniqueKey(obj);
12345 return id === '{}' ? 'default' : id;
12346 }
12347 /**
12348 * An object representation of the query parameters used by this Query.
12349 */
12350 get _queryObject() {
12351 return queryParamsGetQueryObject(this._queryParams);
12352 }
12353 isEqual(other) {
12354 other = getModularInstance(other);
12355 if (!(other instanceof QueryImpl)) {
12356 return false;
12357 }
12358 const sameRepo = this._repo === other._repo;
12359 const samePath = pathEquals(this._path, other._path);
12360 const sameQueryIdentifier = this._queryIdentifier === other._queryIdentifier;
12361 return sameRepo && samePath && sameQueryIdentifier;
12362 }
12363 toJSON() {
12364 return this.toString();
12365 }
12366 toString() {
12367 return this._repo.toString() + pathToUrlEncodedString(this._path);
12368 }
12369}
12370/**
12371 * Validates that no other order by call has been made
12372 */
12373function validateNoPreviousOrderByCall(query, fnName) {
12374 if (query._orderByCalled === true) {
12375 throw new Error(fnName + ": You can't combine multiple orderBy calls.");
12376 }
12377}
12378/**
12379 * Validates start/end values for queries.
12380 */
12381function validateQueryEndpoints(params) {
12382 let startNode = null;
12383 let endNode = null;
12384 if (params.hasStart()) {
12385 startNode = params.getIndexStartValue();
12386 }
12387 if (params.hasEnd()) {
12388 endNode = params.getIndexEndValue();
12389 }
12390 if (params.getIndex() === KEY_INDEX) {
12391 const tooManyArgsError = 'Query: When ordering by key, you may only pass one argument to ' +
12392 'startAt(), endAt(), or equalTo().';
12393 const wrongArgTypeError = 'Query: When ordering by key, the argument passed to startAt(), startAfter(), ' +
12394 'endAt(), endBefore(), or equalTo() must be a string.';
12395 if (params.hasStart()) {
12396 const startName = params.getIndexStartName();
12397 if (startName !== MIN_NAME) {
12398 throw new Error(tooManyArgsError);
12399 }
12400 else if (typeof startNode !== 'string') {
12401 throw new Error(wrongArgTypeError);
12402 }
12403 }
12404 if (params.hasEnd()) {
12405 const endName = params.getIndexEndName();
12406 if (endName !== MAX_NAME) {
12407 throw new Error(tooManyArgsError);
12408 }
12409 else if (typeof endNode !== 'string') {
12410 throw new Error(wrongArgTypeError);
12411 }
12412 }
12413 }
12414 else if (params.getIndex() === PRIORITY_INDEX) {
12415 if ((startNode != null && !isValidPriority(startNode)) ||
12416 (endNode != null && !isValidPriority(endNode))) {
12417 throw new Error('Query: When ordering by priority, the first argument passed to startAt(), ' +
12418 'startAfter() endAt(), endBefore(), or equalTo() must be a valid priority value ' +
12419 '(null, a number, or a string).');
12420 }
12421 }
12422 else {
12423 assert(params.getIndex() instanceof PathIndex ||
12424 params.getIndex() === VALUE_INDEX, 'unknown index type.');
12425 if ((startNode != null && typeof startNode === 'object') ||
12426 (endNode != null && typeof endNode === 'object')) {
12427 throw new Error('Query: First argument passed to startAt(), startAfter(), endAt(), endBefore(), or ' +
12428 'equalTo() cannot be an object.');
12429 }
12430 }
12431}
12432/**
12433 * Validates that limit* has been called with the correct combination of parameters
12434 */
12435function validateLimit(params) {
12436 if (params.hasStart() &&
12437 params.hasEnd() &&
12438 params.hasLimit() &&
12439 !params.hasAnchoredLimit()) {
12440 throw new Error("Query: Can't combine startAt(), startAfter(), endAt(), endBefore(), and limit(). Use " +
12441 'limitToFirst() or limitToLast() instead.');
12442 }
12443}
12444/**
12445 * @internal
12446 */
12447class ReferenceImpl extends QueryImpl {
12448 /** @hideconstructor */
12449 constructor(repo, path) {
12450 super(repo, path, new QueryParams(), false);
12451 }
12452 get parent() {
12453 const parentPath = pathParent(this._path);
12454 return parentPath === null
12455 ? null
12456 : new ReferenceImpl(this._repo, parentPath);
12457 }
12458 get root() {
12459 let ref = this;
12460 while (ref.parent !== null) {
12461 ref = ref.parent;
12462 }
12463 return ref;
12464 }
12465}
12466/**
12467 * A `DataSnapshot` contains data from a Database location.
12468 *
12469 * Any time you read data from the Database, you receive the data as a
12470 * `DataSnapshot`. A `DataSnapshot` is passed to the event callbacks you attach
12471 * with `on()` or `once()`. You can extract the contents of the snapshot as a
12472 * JavaScript object by calling the `val()` method. Alternatively, you can
12473 * traverse into the snapshot by calling `child()` to return child snapshots
12474 * (which you could then call `val()` on).
12475 *
12476 * A `DataSnapshot` is an efficiently generated, immutable copy of the data at
12477 * a Database location. It cannot be modified and will never change (to modify
12478 * data, you always call the `set()` method on a `Reference` directly).
12479 */
12480class DataSnapshot {
12481 /**
12482 * @param _node - A SnapshotNode to wrap.
12483 * @param ref - The location this snapshot came from.
12484 * @param _index - The iteration order for this snapshot
12485 * @hideconstructor
12486 */
12487 constructor(_node,
12488 /**
12489 * The location of this DataSnapshot.
12490 */
12491 ref, _index) {
12492 this._node = _node;
12493 this.ref = ref;
12494 this._index = _index;
12495 }
12496 /**
12497 * Gets the priority value of the data in this `DataSnapshot`.
12498 *
12499 * Applications need not use priority but can order collections by
12500 * ordinary properties (see
12501 * {@link https://firebase.google.com/docs/database/web/lists-of-data#sorting_and_filtering_data |Sorting and filtering data}
12502 * ).
12503 */
12504 get priority() {
12505 // typecast here because we never return deferred values or internal priorities (MAX_PRIORITY)
12506 return this._node.getPriority().val();
12507 }
12508 /**
12509 * The key (last part of the path) of the location of this `DataSnapshot`.
12510 *
12511 * The last token in a Database location is considered its key. For example,
12512 * "ada" is the key for the /users/ada/ node. Accessing the key on any
12513 * `DataSnapshot` will return the key for the location that generated it.
12514 * However, accessing the key on the root URL of a Database will return
12515 * `null`.
12516 */
12517 get key() {
12518 return this.ref.key;
12519 }
12520 /** Returns the number of child properties of this `DataSnapshot`. */
12521 get size() {
12522 return this._node.numChildren();
12523 }
12524 /**
12525 * Gets another `DataSnapshot` for the location at the specified relative path.
12526 *
12527 * Passing a relative path to the `child()` method of a DataSnapshot returns
12528 * another `DataSnapshot` for the location at the specified relative path. The
12529 * relative path can either be a simple child name (for example, "ada") or a
12530 * deeper, slash-separated path (for example, "ada/name/first"). If the child
12531 * location has no data, an empty `DataSnapshot` (that is, a `DataSnapshot`
12532 * whose value is `null`) is returned.
12533 *
12534 * @param path - A relative path to the location of child data.
12535 */
12536 child(path) {
12537 const childPath = new Path(path);
12538 const childRef = child(this.ref, path);
12539 return new DataSnapshot(this._node.getChild(childPath), childRef, PRIORITY_INDEX);
12540 }
12541 /**
12542 * Returns true if this `DataSnapshot` contains any data. It is slightly more
12543 * efficient than using `snapshot.val() !== null`.
12544 */
12545 exists() {
12546 return !this._node.isEmpty();
12547 }
12548 /**
12549 * Exports the entire contents of the DataSnapshot as a JavaScript object.
12550 *
12551 * The `exportVal()` method is similar to `val()`, except priority information
12552 * is included (if available), making it suitable for backing up your data.
12553 *
12554 * @returns The DataSnapshot's contents as a JavaScript value (Object,
12555 * Array, string, number, boolean, or `null`).
12556 */
12557 // eslint-disable-next-line @typescript-eslint/no-explicit-any
12558 exportVal() {
12559 return this._node.val(true);
12560 }
12561 /**
12562 * Enumerates the top-level children in the `DataSnapshot`.
12563 *
12564 * Because of the way JavaScript objects work, the ordering of data in the
12565 * JavaScript object returned by `val()` is not guaranteed to match the
12566 * ordering on the server nor the ordering of `onChildAdded()` events. That is
12567 * where `forEach()` comes in handy. It guarantees the children of a
12568 * `DataSnapshot` will be iterated in their query order.
12569 *
12570 * If no explicit `orderBy*()` method is used, results are returned
12571 * ordered by key (unless priorities are used, in which case, results are
12572 * returned by priority).
12573 *
12574 * @param action - A function that will be called for each child DataSnapshot.
12575 * The callback can return true to cancel further enumeration.
12576 * @returns true if enumeration was canceled due to your callback returning
12577 * true.
12578 */
12579 forEach(action) {
12580 if (this._node.isLeafNode()) {
12581 return false;
12582 }
12583 const childrenNode = this._node;
12584 // Sanitize the return value to a boolean. ChildrenNode.forEachChild has a weird return type...
12585 return !!childrenNode.forEachChild(this._index, (key, node) => {
12586 return action(new DataSnapshot(node, child(this.ref, key), PRIORITY_INDEX));
12587 });
12588 }
12589 /**
12590 * Returns true if the specified child path has (non-null) data.
12591 *
12592 * @param path - A relative path to the location of a potential child.
12593 * @returns `true` if data exists at the specified child path; else
12594 * `false`.
12595 */
12596 hasChild(path) {
12597 const childPath = new Path(path);
12598 return !this._node.getChild(childPath).isEmpty();
12599 }
12600 /**
12601 * Returns whether or not the `DataSnapshot` has any non-`null` child
12602 * properties.
12603 *
12604 * You can use `hasChildren()` to determine if a `DataSnapshot` has any
12605 * children. If it does, you can enumerate them using `forEach()`. If it
12606 * doesn't, then either this snapshot contains a primitive value (which can be
12607 * retrieved with `val()`) or it is empty (in which case, `val()` will return
12608 * `null`).
12609 *
12610 * @returns true if this snapshot has any children; else false.
12611 */
12612 hasChildren() {
12613 if (this._node.isLeafNode()) {
12614 return false;
12615 }
12616 else {
12617 return !this._node.isEmpty();
12618 }
12619 }
12620 /**
12621 * Returns a JSON-serializable representation of this object.
12622 */
12623 toJSON() {
12624 return this.exportVal();
12625 }
12626 /**
12627 * Extracts a JavaScript value from a `DataSnapshot`.
12628 *
12629 * Depending on the data in a `DataSnapshot`, the `val()` method may return a
12630 * scalar type (string, number, or boolean), an array, or an object. It may
12631 * also return null, indicating that the `DataSnapshot` is empty (contains no
12632 * data).
12633 *
12634 * @returns The DataSnapshot's contents as a JavaScript value (Object,
12635 * Array, string, number, boolean, or `null`).
12636 */
12637 // eslint-disable-next-line @typescript-eslint/no-explicit-any
12638 val() {
12639 return this._node.val();
12640 }
12641}
12642/**
12643 *
12644 * Returns a `Reference` representing the location in the Database
12645 * corresponding to the provided path. If no path is provided, the `Reference`
12646 * will point to the root of the Database.
12647 *
12648 * @param db - The database instance to obtain a reference for.
12649 * @param path - Optional path representing the location the returned
12650 * `Reference` will point. If not provided, the returned `Reference` will
12651 * point to the root of the Database.
12652 * @returns If a path is provided, a `Reference`
12653 * pointing to the provided path. Otherwise, a `Reference` pointing to the
12654 * root of the Database.
12655 */
12656function ref(db, path) {
12657 db = getModularInstance(db);
12658 db._checkNotDeleted('ref');
12659 return path !== undefined ? child(db._root, path) : db._root;
12660}
12661/**
12662 * Returns a `Reference` representing the location in the Database
12663 * corresponding to the provided Firebase URL.
12664 *
12665 * An exception is thrown if the URL is not a valid Firebase Database URL or it
12666 * has a different domain than the current `Database` instance.
12667 *
12668 * Note that all query parameters (`orderBy`, `limitToLast`, etc.) are ignored
12669 * and are not applied to the returned `Reference`.
12670 *
12671 * @param db - The database instance to obtain a reference for.
12672 * @param url - The Firebase URL at which the returned `Reference` will
12673 * point.
12674 * @returns A `Reference` pointing to the provided
12675 * Firebase URL.
12676 */
12677function refFromURL(db, url) {
12678 db = getModularInstance(db);
12679 db._checkNotDeleted('refFromURL');
12680 const parsedURL = parseRepoInfo(url, db._repo.repoInfo_.nodeAdmin);
12681 validateUrl('refFromURL', parsedURL);
12682 const repoInfo = parsedURL.repoInfo;
12683 if (!db._repo.repoInfo_.isCustomHost() &&
12684 repoInfo.host !== db._repo.repoInfo_.host) {
12685 fatal('refFromURL' +
12686 ': Host name does not match the current database: ' +
12687 '(found ' +
12688 repoInfo.host +
12689 ' but expected ' +
12690 db._repo.repoInfo_.host +
12691 ')');
12692 }
12693 return ref(db, parsedURL.path.toString());
12694}
12695/**
12696 * Gets a `Reference` for the location at the specified relative path.
12697 *
12698 * The relative path can either be a simple child name (for example, "ada") or
12699 * a deeper slash-separated path (for example, "ada/name/first").
12700 *
12701 * @param parent - The parent location.
12702 * @param path - A relative path from this location to the desired child
12703 * location.
12704 * @returns The specified child location.
12705 */
12706function child(parent, path) {
12707 parent = getModularInstance(parent);
12708 if (pathGetFront(parent._path) === null) {
12709 validateRootPathString('child', 'path', path, false);
12710 }
12711 else {
12712 validatePathString('child', 'path', path, false);
12713 }
12714 return new ReferenceImpl(parent._repo, pathChild(parent._path, path));
12715}
12716/**
12717 * Returns an `OnDisconnect` object - see
12718 * {@link https://firebase.google.com/docs/database/web/offline-capabilities | Enabling Offline Capabilities in JavaScript}
12719 * for more information on how to use it.
12720 *
12721 * @param ref - The reference to add OnDisconnect triggers for.
12722 */
12723function onDisconnect(ref) {
12724 ref = getModularInstance(ref);
12725 return new OnDisconnect(ref._repo, ref._path);
12726}
12727/**
12728 * Generates a new child location using a unique key and returns its
12729 * `Reference`.
12730 *
12731 * This is the most common pattern for adding data to a collection of items.
12732 *
12733 * If you provide a value to `push()`, the value is written to the
12734 * generated location. If you don't pass a value, nothing is written to the
12735 * database and the child remains empty (but you can use the `Reference`
12736 * elsewhere).
12737 *
12738 * The unique keys generated by `push()` are ordered by the current time, so the
12739 * resulting list of items is chronologically sorted. The keys are also
12740 * designed to be unguessable (they contain 72 random bits of entropy).
12741 *
12742 * See {@link https://firebase.google.com/docs/database/web/lists-of-data#append_to_a_list_of_data | Append to a list of data}
12743 * </br>See {@link ttps://firebase.googleblog.com/2015/02/the-2120-ways-to-ensure-unique_68.html | The 2^120 Ways to Ensure Unique Identifiers}
12744 *
12745 * @param parent - The parent location.
12746 * @param value - Optional value to be written at the generated location.
12747 * @returns Combined `Promise` and `Reference`; resolves when write is complete,
12748 * but can be used immediately as the `Reference` to the child location.
12749 */
12750function push(parent, value) {
12751 parent = getModularInstance(parent);
12752 validateWritablePath('push', parent._path);
12753 validateFirebaseDataArg('push', value, parent._path, true);
12754 const now = repoServerTime(parent._repo);
12755 const name = nextPushId(now);
12756 // push() returns a ThennableReference whose promise is fulfilled with a
12757 // regular Reference. We use child() to create handles to two different
12758 // references. The first is turned into a ThennableReference below by adding
12759 // then() and catch() methods and is used as the return value of push(). The
12760 // second remains a regular Reference and is used as the fulfilled value of
12761 // the first ThennableReference.
12762 const thennablePushRef = child(parent, name);
12763 const pushRef = child(parent, name);
12764 let promise;
12765 if (value != null) {
12766 promise = set(pushRef, value).then(() => pushRef);
12767 }
12768 else {
12769 promise = Promise.resolve(pushRef);
12770 }
12771 thennablePushRef.then = promise.then.bind(promise);
12772 thennablePushRef.catch = promise.then.bind(promise, undefined);
12773 return thennablePushRef;
12774}
12775/**
12776 * Removes the data at this Database location.
12777 *
12778 * Any data at child locations will also be deleted.
12779 *
12780 * The effect of the remove will be visible immediately and the corresponding
12781 * event 'value' will be triggered. Synchronization of the remove to the
12782 * Firebase servers will also be started, and the returned Promise will resolve
12783 * when complete. If provided, the onComplete callback will be called
12784 * asynchronously after synchronization has finished.
12785 *
12786 * @param ref - The location to remove.
12787 * @returns Resolves when remove on server is complete.
12788 */
12789function remove(ref) {
12790 validateWritablePath('remove', ref._path);
12791 return set(ref, null);
12792}
12793/**
12794 * Writes data to this Database location.
12795 *
12796 * This will overwrite any data at this location and all child locations.
12797 *
12798 * The effect of the write will be visible immediately, and the corresponding
12799 * events ("value", "child_added", etc.) will be triggered. Synchronization of
12800 * the data to the Firebase servers will also be started, and the returned
12801 * Promise will resolve when complete. If provided, the `onComplete` callback
12802 * will be called asynchronously after synchronization has finished.
12803 *
12804 * Passing `null` for the new value is equivalent to calling `remove()`; namely,
12805 * all data at this location and all child locations will be deleted.
12806 *
12807 * `set()` will remove any priority stored at this location, so if priority is
12808 * meant to be preserved, you need to use `setWithPriority()` instead.
12809 *
12810 * Note that modifying data with `set()` will cancel any pending transactions
12811 * at that location, so extreme care should be taken if mixing `set()` and
12812 * `transaction()` to modify the same data.
12813 *
12814 * A single `set()` will generate a single "value" event at the location where
12815 * the `set()` was performed.
12816 *
12817 * @param ref - The location to write to.
12818 * @param value - The value to be written (string, number, boolean, object,
12819 * array, or null).
12820 * @returns Resolves when write to server is complete.
12821 */
12822function set(ref, value) {
12823 ref = getModularInstance(ref);
12824 validateWritablePath('set', ref._path);
12825 validateFirebaseDataArg('set', value, ref._path, false);
12826 const deferred = new Deferred();
12827 repoSetWithPriority(ref._repo, ref._path, value,
12828 /*priority=*/ null, deferred.wrapCallback(() => { }));
12829 return deferred.promise;
12830}
12831/**
12832 * Sets a priority for the data at this Database location.
12833 *
12834 * Applications need not use priority but can order collections by
12835 * ordinary properties (see
12836 * {@link https://firebase.google.com/docs/database/web/lists-of-data#sorting_and_filtering_data | Sorting and filtering data}
12837 * ).
12838 *
12839 * @param ref - The location to write to.
12840 * @param priority - The priority to be written (string, number, or null).
12841 * @returns Resolves when write to server is complete.
12842 */
12843function setPriority(ref, priority) {
12844 ref = getModularInstance(ref);
12845 validateWritablePath('setPriority', ref._path);
12846 validatePriority('setPriority', priority, false);
12847 const deferred = new Deferred();
12848 repoSetWithPriority(ref._repo, pathChild(ref._path, '.priority'), priority, null, deferred.wrapCallback(() => { }));
12849 return deferred.promise;
12850}
12851/**
12852 * Writes data the Database location. Like `set()` but also specifies the
12853 * priority for that data.
12854 *
12855 * Applications need not use priority but can order collections by
12856 * ordinary properties (see
12857 * {@link https://firebase.google.com/docs/database/web/lists-of-data#sorting_and_filtering_data | Sorting and filtering data}
12858 * ).
12859 *
12860 * @param ref - The location to write to.
12861 * @param value - The value to be written (string, number, boolean, object,
12862 * array, or null).
12863 * @param priority - The priority to be written (string, number, or null).
12864 * @returns Resolves when write to server is complete.
12865 */
12866function setWithPriority(ref, value, priority) {
12867 validateWritablePath('setWithPriority', ref._path);
12868 validateFirebaseDataArg('setWithPriority', value, ref._path, false);
12869 validatePriority('setWithPriority', priority, false);
12870 if (ref.key === '.length' || ref.key === '.keys') {
12871 throw 'setWithPriority failed: ' + ref.key + ' is a read-only object.';
12872 }
12873 const deferred = new Deferred();
12874 repoSetWithPriority(ref._repo, ref._path, value, priority, deferred.wrapCallback(() => { }));
12875 return deferred.promise;
12876}
12877/**
12878 * Writes multiple values to the Database at once.
12879 *
12880 * The `values` argument contains multiple property-value pairs that will be
12881 * written to the Database together. Each child property can either be a simple
12882 * property (for example, "name") or a relative path (for example,
12883 * "name/first") from the current location to the data to update.
12884 *
12885 * As opposed to the `set()` method, `update()` can be use to selectively update
12886 * only the referenced properties at the current location (instead of replacing
12887 * all the child properties at the current location).
12888 *
12889 * The effect of the write will be visible immediately, and the corresponding
12890 * events ('value', 'child_added', etc.) will be triggered. Synchronization of
12891 * the data to the Firebase servers will also be started, and the returned
12892 * Promise will resolve when complete. If provided, the `onComplete` callback
12893 * will be called asynchronously after synchronization has finished.
12894 *
12895 * A single `update()` will generate a single "value" event at the location
12896 * where the `update()` was performed, regardless of how many children were
12897 * modified.
12898 *
12899 * Note that modifying data with `update()` will cancel any pending
12900 * transactions at that location, so extreme care should be taken if mixing
12901 * `update()` and `transaction()` to modify the same data.
12902 *
12903 * Passing `null` to `update()` will remove the data at this location.
12904 *
12905 * See
12906 * {@link https://firebase.googleblog.com/2015/09/introducing-multi-location-updates-and_86.html | Introducing multi-location updates and more}.
12907 *
12908 * @param ref - The location to write to.
12909 * @param values - Object containing multiple values.
12910 * @returns Resolves when update on server is complete.
12911 */
12912function update(ref, values) {
12913 validateFirebaseMergeDataArg('update', values, ref._path, false);
12914 const deferred = new Deferred();
12915 repoUpdate(ref._repo, ref._path, values, deferred.wrapCallback(() => { }));
12916 return deferred.promise;
12917}
12918/**
12919 * Gets the most up-to-date result for this query.
12920 *
12921 * @param query - The query to run.
12922 * @returns A `Promise` which resolves to the resulting DataSnapshot if a value is
12923 * available, or rejects if the client is unable to return a value (e.g., if the
12924 * server is unreachable and there is nothing cached).
12925 */
12926function get(query) {
12927 query = getModularInstance(query);
12928 return repoGetValue(query._repo, query).then(node => {
12929 return new DataSnapshot(node, new ReferenceImpl(query._repo, query._path), query._queryParams.getIndex());
12930 });
12931}
12932/**
12933 * Represents registration for 'value' events.
12934 */
12935class ValueEventRegistration {
12936 constructor(callbackContext) {
12937 this.callbackContext = callbackContext;
12938 }
12939 respondsTo(eventType) {
12940 return eventType === 'value';
12941 }
12942 createEvent(change, query) {
12943 const index = query._queryParams.getIndex();
12944 return new DataEvent('value', this, new DataSnapshot(change.snapshotNode, new ReferenceImpl(query._repo, query._path), index));
12945 }
12946 getEventRunner(eventData) {
12947 if (eventData.getEventType() === 'cancel') {
12948 return () => this.callbackContext.onCancel(eventData.error);
12949 }
12950 else {
12951 return () => this.callbackContext.onValue(eventData.snapshot, null);
12952 }
12953 }
12954 createCancelEvent(error, path) {
12955 if (this.callbackContext.hasCancelCallback) {
12956 return new CancelEvent(this, error, path);
12957 }
12958 else {
12959 return null;
12960 }
12961 }
12962 matches(other) {
12963 if (!(other instanceof ValueEventRegistration)) {
12964 return false;
12965 }
12966 else if (!other.callbackContext || !this.callbackContext) {
12967 // If no callback specified, we consider it to match any callback.
12968 return true;
12969 }
12970 else {
12971 return other.callbackContext.matches(this.callbackContext);
12972 }
12973 }
12974 hasAnyCallback() {
12975 return this.callbackContext !== null;
12976 }
12977}
12978/**
12979 * Represents the registration of a child_x event.
12980 */
12981class ChildEventRegistration {
12982 constructor(eventType, callbackContext) {
12983 this.eventType = eventType;
12984 this.callbackContext = callbackContext;
12985 }
12986 respondsTo(eventType) {
12987 let eventToCheck = eventType === 'children_added' ? 'child_added' : eventType;
12988 eventToCheck =
12989 eventToCheck === 'children_removed' ? 'child_removed' : eventToCheck;
12990 return this.eventType === eventToCheck;
12991 }
12992 createCancelEvent(error, path) {
12993 if (this.callbackContext.hasCancelCallback) {
12994 return new CancelEvent(this, error, path);
12995 }
12996 else {
12997 return null;
12998 }
12999 }
13000 createEvent(change, query) {
13001 assert(change.childName != null, 'Child events should have a childName.');
13002 const childRef = child(new ReferenceImpl(query._repo, query._path), change.childName);
13003 const index = query._queryParams.getIndex();
13004 return new DataEvent(change.type, this, new DataSnapshot(change.snapshotNode, childRef, index), change.prevName);
13005 }
13006 getEventRunner(eventData) {
13007 if (eventData.getEventType() === 'cancel') {
13008 return () => this.callbackContext.onCancel(eventData.error);
13009 }
13010 else {
13011 return () => this.callbackContext.onValue(eventData.snapshot, eventData.prevName);
13012 }
13013 }
13014 matches(other) {
13015 if (other instanceof ChildEventRegistration) {
13016 return (this.eventType === other.eventType &&
13017 (!this.callbackContext ||
13018 !other.callbackContext ||
13019 this.callbackContext.matches(other.callbackContext)));
13020 }
13021 return false;
13022 }
13023 hasAnyCallback() {
13024 return !!this.callbackContext;
13025 }
13026}
13027function addEventListener(query, eventType, callback, cancelCallbackOrListenOptions, options) {
13028 let cancelCallback;
13029 if (typeof cancelCallbackOrListenOptions === 'object') {
13030 cancelCallback = undefined;
13031 options = cancelCallbackOrListenOptions;
13032 }
13033 if (typeof cancelCallbackOrListenOptions === 'function') {
13034 cancelCallback = cancelCallbackOrListenOptions;
13035 }
13036 if (options && options.onlyOnce) {
13037 const userCallback = callback;
13038 const onceCallback = (dataSnapshot, previousChildName) => {
13039 repoRemoveEventCallbackForQuery(query._repo, query, container);
13040 userCallback(dataSnapshot, previousChildName);
13041 };
13042 onceCallback.userCallback = callback.userCallback;
13043 onceCallback.context = callback.context;
13044 callback = onceCallback;
13045 }
13046 const callbackContext = new CallbackContext(callback, cancelCallback || undefined);
13047 const container = eventType === 'value'
13048 ? new ValueEventRegistration(callbackContext)
13049 : new ChildEventRegistration(eventType, callbackContext);
13050 repoAddEventCallbackForQuery(query._repo, query, container);
13051 return () => repoRemoveEventCallbackForQuery(query._repo, query, container);
13052}
13053function onValue(query, callback, cancelCallbackOrListenOptions, options) {
13054 return addEventListener(query, 'value', callback, cancelCallbackOrListenOptions, options);
13055}
13056function onChildAdded(query, callback, cancelCallbackOrListenOptions, options) {
13057 return addEventListener(query, 'child_added', callback, cancelCallbackOrListenOptions, options);
13058}
13059function onChildChanged(query, callback, cancelCallbackOrListenOptions, options) {
13060 return addEventListener(query, 'child_changed', callback, cancelCallbackOrListenOptions, options);
13061}
13062function onChildMoved(query, callback, cancelCallbackOrListenOptions, options) {
13063 return addEventListener(query, 'child_moved', callback, cancelCallbackOrListenOptions, options);
13064}
13065function onChildRemoved(query, callback, cancelCallbackOrListenOptions, options) {
13066 return addEventListener(query, 'child_removed', callback, cancelCallbackOrListenOptions, options);
13067}
13068/**
13069 * Detaches a callback previously attached with `on()`.
13070 *
13071 * Detach a callback previously attached with `on()`. Note that if `on()` was
13072 * called multiple times with the same eventType and callback, the callback
13073 * will be called multiple times for each event, and `off()` must be called
13074 * multiple times to remove the callback. Calling `off()` on a parent listener
13075 * will not automatically remove listeners registered on child nodes, `off()`
13076 * must also be called on any child listeners to remove the callback.
13077 *
13078 * If a callback is not specified, all callbacks for the specified eventType
13079 * will be removed. Similarly, if no eventType is specified, all callbacks
13080 * for the `Reference` will be removed.
13081 *
13082 * Individual listeners can also be removed by invoking their unsubscribe
13083 * callbacks.
13084 *
13085 * @param query - The query that the listener was registered with.
13086 * @param eventType - One of the following strings: "value", "child_added",
13087 * "child_changed", "child_removed", or "child_moved." If omitted, all callbacks
13088 * for the `Reference` will be removed.
13089 * @param callback - The callback function that was passed to `on()` or
13090 * `undefined` to remove all callbacks.
13091 */
13092function off(query, eventType, callback) {
13093 let container = null;
13094 const expCallback = callback ? new CallbackContext(callback) : null;
13095 if (eventType === 'value') {
13096 container = new ValueEventRegistration(expCallback);
13097 }
13098 else if (eventType) {
13099 container = new ChildEventRegistration(eventType, expCallback);
13100 }
13101 repoRemoveEventCallbackForQuery(query._repo, query, container);
13102}
13103/**
13104 * A `QueryConstraint` is used to narrow the set of documents returned by a
13105 * Database query. `QueryConstraint`s are created by invoking {@link endAt},
13106 * {@link endBefore}, {@link startAt}, {@link startAfter}, {@link
13107 * limitToFirst}, {@link limitToLast}, {@link orderByChild},
13108 * {@link orderByChild}, {@link orderByKey} , {@link orderByPriority} ,
13109 * {@link orderByValue} or {@link equalTo} and
13110 * can then be passed to {@link query} to create a new query instance that
13111 * also contains this `QueryConstraint`.
13112 */
13113class QueryConstraint {
13114}
13115class QueryEndAtConstraint extends QueryConstraint {
13116 constructor(_value, _key) {
13117 super();
13118 this._value = _value;
13119 this._key = _key;
13120 }
13121 _apply(query) {
13122 validateFirebaseDataArg('endAt', this._value, query._path, true);
13123 const newParams = queryParamsEndAt(query._queryParams, this._value, this._key);
13124 validateLimit(newParams);
13125 validateQueryEndpoints(newParams);
13126 if (query._queryParams.hasEnd()) {
13127 throw new Error('endAt: Starting point was already set (by another call to endAt, ' +
13128 'endBefore or equalTo).');
13129 }
13130 return new QueryImpl(query._repo, query._path, newParams, query._orderByCalled);
13131 }
13132}
13133/**
13134 * Creates a `QueryConstraint` with the specified ending point.
13135 *
13136 * Using `startAt()`, `startAfter()`, `endBefore()`, `endAt()` and `equalTo()`
13137 * allows you to choose arbitrary starting and ending points for your queries.
13138 *
13139 * The ending point is inclusive, so children with exactly the specified value
13140 * will be included in the query. The optional key argument can be used to
13141 * further limit the range of the query. If it is specified, then children that
13142 * have exactly the specified value must also have a key name less than or equal
13143 * to the specified key.
13144 *
13145 * You can read more about `endAt()` in
13146 * {@link https://firebase.google.com/docs/database/web/lists-of-data#filtering_data | Filtering data}.
13147 *
13148 * @param value - The value to end at. The argument type depends on which
13149 * `orderBy*()` function was used in this query. Specify a value that matches
13150 * the `orderBy*()` type. When used in combination with `orderByKey()`, the
13151 * value must be a string.
13152 * @param key - The child key to end at, among the children with the previously
13153 * specified priority. This argument is only allowed if ordering by child,
13154 * value, or priority.
13155 */
13156function endAt(value, key) {
13157 validateKey('endAt', 'key', key, true);
13158 return new QueryEndAtConstraint(value, key);
13159}
13160class QueryEndBeforeConstraint extends QueryConstraint {
13161 constructor(_value, _key) {
13162 super();
13163 this._value = _value;
13164 this._key = _key;
13165 }
13166 _apply(query) {
13167 validateFirebaseDataArg('endBefore', this._value, query._path, false);
13168 const newParams = queryParamsEndBefore(query._queryParams, this._value, this._key);
13169 validateLimit(newParams);
13170 validateQueryEndpoints(newParams);
13171 if (query._queryParams.hasEnd()) {
13172 throw new Error('endBefore: Starting point was already set (by another call to endAt, ' +
13173 'endBefore or equalTo).');
13174 }
13175 return new QueryImpl(query._repo, query._path, newParams, query._orderByCalled);
13176 }
13177}
13178/**
13179 * Creates a `QueryConstraint` with the specified ending point (exclusive).
13180 *
13181 * Using `startAt()`, `startAfter()`, `endBefore()`, `endAt()` and `equalTo()`
13182 * allows you to choose arbitrary starting and ending points for your queries.
13183 *
13184 * The ending point is exclusive. If only a value is provided, children
13185 * with a value less than the specified value will be included in the query.
13186 * If a key is specified, then children must have a value lesss than or equal
13187 * to the specified value and a a key name less than the specified key.
13188 *
13189 * @param value - The value to end before. The argument type depends on which
13190 * `orderBy*()` function was used in this query. Specify a value that matches
13191 * the `orderBy*()` type. When used in combination with `orderByKey()`, the
13192 * value must be a string.
13193 * @param key - The child key to end before, among the children with the
13194 * previously specified priority. This argument is only allowed if ordering by
13195 * child, value, or priority.
13196 */
13197function endBefore(value, key) {
13198 validateKey('endBefore', 'key', key, true);
13199 return new QueryEndBeforeConstraint(value, key);
13200}
13201class QueryStartAtConstraint extends QueryConstraint {
13202 constructor(_value, _key) {
13203 super();
13204 this._value = _value;
13205 this._key = _key;
13206 }
13207 _apply(query) {
13208 validateFirebaseDataArg('startAt', this._value, query._path, true);
13209 const newParams = queryParamsStartAt(query._queryParams, this._value, this._key);
13210 validateLimit(newParams);
13211 validateQueryEndpoints(newParams);
13212 if (query._queryParams.hasStart()) {
13213 throw new Error('startAt: Starting point was already set (by another call to startAt, ' +
13214 'startBefore or equalTo).');
13215 }
13216 return new QueryImpl(query._repo, query._path, newParams, query._orderByCalled);
13217 }
13218}
13219/**
13220 * Creates a `QueryConstraint` with the specified starting point.
13221 *
13222 * Using `startAt()`, `startAfter()`, `endBefore()`, `endAt()` and `equalTo()`
13223 * allows you to choose arbitrary starting and ending points for your queries.
13224 *
13225 * The starting point is inclusive, so children with exactly the specified value
13226 * will be included in the query. The optional key argument can be used to
13227 * further limit the range of the query. If it is specified, then children that
13228 * have exactly the specified value must also have a key name greater than or
13229 * equal to the specified key.
13230 *
13231 * You can read more about `startAt()` in
13232 * {@link https://firebase.google.com/docs/database/web/lists-of-data#filtering_data | Filtering data}.
13233 *
13234 * @param value - The value to start at. The argument type depends on which
13235 * `orderBy*()` function was used in this query. Specify a value that matches
13236 * the `orderBy*()` type. When used in combination with `orderByKey()`, the
13237 * value must be a string.
13238 * @param key - The child key to start at. This argument is only allowed if
13239 * ordering by child, value, or priority.
13240 */
13241function startAt(value = null, key) {
13242 validateKey('startAt', 'key', key, true);
13243 return new QueryStartAtConstraint(value, key);
13244}
13245class QueryStartAfterConstraint extends QueryConstraint {
13246 constructor(_value, _key) {
13247 super();
13248 this._value = _value;
13249 this._key = _key;
13250 }
13251 _apply(query) {
13252 validateFirebaseDataArg('startAfter', this._value, query._path, false);
13253 const newParams = queryParamsStartAfter(query._queryParams, this._value, this._key);
13254 validateLimit(newParams);
13255 validateQueryEndpoints(newParams);
13256 if (query._queryParams.hasStart()) {
13257 throw new Error('startAfter: Starting point was already set (by another call to startAt, ' +
13258 'startAfter, or equalTo).');
13259 }
13260 return new QueryImpl(query._repo, query._path, newParams, query._orderByCalled);
13261 }
13262}
13263/**
13264 * Creates a `QueryConstraint` with the specified starting point (exclusive).
13265 *
13266 * Using `startAt()`, `startAfter()`, `endBefore()`, `endAt()` and `equalTo()`
13267 * allows you to choose arbitrary starting and ending points for your queries.
13268 *
13269 * The starting point is exclusive. If only a value is provided, children
13270 * with a value greater than the specified value will be included in the query.
13271 * If a key is specified, then children must have a value greater than or equal
13272 * to the specified value and a a key name greater than the specified key.
13273 *
13274 * @param value - The value to start after. The argument type depends on which
13275 * `orderBy*()` function was used in this query. Specify a value that matches
13276 * the `orderBy*()` type. When used in combination with `orderByKey()`, the
13277 * value must be a string.
13278 * @param key - The child key to start after. This argument is only allowed if
13279 * ordering by child, value, or priority.
13280 */
13281function startAfter(value, key) {
13282 validateKey('startAfter', 'key', key, true);
13283 return new QueryStartAfterConstraint(value, key);
13284}
13285class QueryLimitToFirstConstraint extends QueryConstraint {
13286 constructor(_limit) {
13287 super();
13288 this._limit = _limit;
13289 }
13290 _apply(query) {
13291 if (query._queryParams.hasLimit()) {
13292 throw new Error('limitToFirst: Limit was already set (by another call to limitToFirst ' +
13293 'or limitToLast).');
13294 }
13295 return new QueryImpl(query._repo, query._path, queryParamsLimitToFirst(query._queryParams, this._limit), query._orderByCalled);
13296 }
13297}
13298/**
13299 * Creates a new `QueryConstraint` that if limited to the first specific number
13300 * of children.
13301 *
13302 * The `limitToFirst()` method is used to set a maximum number of children to be
13303 * synced for a given callback. If we set a limit of 100, we will initially only
13304 * receive up to 100 `child_added` events. If we have fewer than 100 messages
13305 * stored in our Database, a `child_added` event will fire for each message.
13306 * However, if we have over 100 messages, we will only receive a `child_added`
13307 * event for the first 100 ordered messages. As items change, we will receive
13308 * `child_removed` events for each item that drops out of the active list so
13309 * that the total number stays at 100.
13310 *
13311 * You can read more about `limitToFirst()` in
13312 * {@link https://firebase.google.com/docs/database/web/lists-of-data#filtering_data | Filtering data}.
13313 *
13314 * @param limit - The maximum number of nodes to include in this query.
13315 */
13316function limitToFirst(limit) {
13317 if (typeof limit !== 'number' || Math.floor(limit) !== limit || limit <= 0) {
13318 throw new Error('limitToFirst: First argument must be a positive integer.');
13319 }
13320 return new QueryLimitToFirstConstraint(limit);
13321}
13322class QueryLimitToLastConstraint extends QueryConstraint {
13323 constructor(_limit) {
13324 super();
13325 this._limit = _limit;
13326 }
13327 _apply(query) {
13328 if (query._queryParams.hasLimit()) {
13329 throw new Error('limitToLast: Limit was already set (by another call to limitToFirst ' +
13330 'or limitToLast).');
13331 }
13332 return new QueryImpl(query._repo, query._path, queryParamsLimitToLast(query._queryParams, this._limit), query._orderByCalled);
13333 }
13334}
13335/**
13336 * Creates a new `QueryConstraint` that is limited to return only the last
13337 * specified number of children.
13338 *
13339 * The `limitToLast()` method is used to set a maximum number of children to be
13340 * synced for a given callback. If we set a limit of 100, we will initially only
13341 * receive up to 100 `child_added` events. If we have fewer than 100 messages
13342 * stored in our Database, a `child_added` event will fire for each message.
13343 * However, if we have over 100 messages, we will only receive a `child_added`
13344 * event for the last 100 ordered messages. As items change, we will receive
13345 * `child_removed` events for each item that drops out of the active list so
13346 * that the total number stays at 100.
13347 *
13348 * You can read more about `limitToLast()` in
13349 * {@link https://firebase.google.com/docs/database/web/lists-of-data#filtering_data | Filtering data}.
13350 *
13351 * @param limit - The maximum number of nodes to include in this query.
13352 */
13353function limitToLast(limit) {
13354 if (typeof limit !== 'number' || Math.floor(limit) !== limit || limit <= 0) {
13355 throw new Error('limitToLast: First argument must be a positive integer.');
13356 }
13357 return new QueryLimitToLastConstraint(limit);
13358}
13359class QueryOrderByChildConstraint extends QueryConstraint {
13360 constructor(_path) {
13361 super();
13362 this._path = _path;
13363 }
13364 _apply(query) {
13365 validateNoPreviousOrderByCall(query, 'orderByChild');
13366 const parsedPath = new Path(this._path);
13367 if (pathIsEmpty(parsedPath)) {
13368 throw new Error('orderByChild: cannot pass in empty path. Use orderByValue() instead.');
13369 }
13370 const index = new PathIndex(parsedPath);
13371 const newParams = queryParamsOrderBy(query._queryParams, index);
13372 validateQueryEndpoints(newParams);
13373 return new QueryImpl(query._repo, query._path, newParams,
13374 /*orderByCalled=*/ true);
13375 }
13376}
13377/**
13378 * Creates a new `QueryConstraint` that orders by the specified child key.
13379 *
13380 * Queries can only order by one key at a time. Calling `orderByChild()`
13381 * multiple times on the same query is an error.
13382 *
13383 * Firebase queries allow you to order your data by any child key on the fly.
13384 * However, if you know in advance what your indexes will be, you can define
13385 * them via the .indexOn rule in your Security Rules for better performance. See
13386 * the{@link https://firebase.google.com/docs/database/security/indexing-data}
13387 * rule for more information.
13388 *
13389 * You can read more about `orderByChild()` in
13390 * {@link https://firebase.google.com/docs/database/web/lists-of-data#sort_data | Sort data}.
13391 *
13392 * @param path - The path to order by.
13393 */
13394function orderByChild(path) {
13395 if (path === '$key') {
13396 throw new Error('orderByChild: "$key" is invalid. Use orderByKey() instead.');
13397 }
13398 else if (path === '$priority') {
13399 throw new Error('orderByChild: "$priority" is invalid. Use orderByPriority() instead.');
13400 }
13401 else if (path === '$value') {
13402 throw new Error('orderByChild: "$value" is invalid. Use orderByValue() instead.');
13403 }
13404 validatePathString('orderByChild', 'path', path, false);
13405 return new QueryOrderByChildConstraint(path);
13406}
13407class QueryOrderByKeyConstraint extends QueryConstraint {
13408 _apply(query) {
13409 validateNoPreviousOrderByCall(query, 'orderByKey');
13410 const newParams = queryParamsOrderBy(query._queryParams, KEY_INDEX);
13411 validateQueryEndpoints(newParams);
13412 return new QueryImpl(query._repo, query._path, newParams,
13413 /*orderByCalled=*/ true);
13414 }
13415}
13416/**
13417 * Creates a new `QueryConstraint` that orders by the key.
13418 *
13419 * Sorts the results of a query by their (ascending) key values.
13420 *
13421 * You can read more about `orderByKey()` in
13422 * {@link https://firebase.google.com/docs/database/web/lists-of-data#sort_data | Sort data}.
13423 */
13424function orderByKey() {
13425 return new QueryOrderByKeyConstraint();
13426}
13427class QueryOrderByPriorityConstraint extends QueryConstraint {
13428 _apply(query) {
13429 validateNoPreviousOrderByCall(query, 'orderByPriority');
13430 const newParams = queryParamsOrderBy(query._queryParams, PRIORITY_INDEX);
13431 validateQueryEndpoints(newParams);
13432 return new QueryImpl(query._repo, query._path, newParams,
13433 /*orderByCalled=*/ true);
13434 }
13435}
13436/**
13437 * Creates a new `QueryConstraint` that orders by priority.
13438 *
13439 * Applications need not use priority but can order collections by
13440 * ordinary properties (see
13441 * {@link https://firebase.google.com/docs/database/web/lists-of-data#sort_data | Sort data}
13442 * for alternatives to priority.
13443 */
13444function orderByPriority() {
13445 return new QueryOrderByPriorityConstraint();
13446}
13447class QueryOrderByValueConstraint extends QueryConstraint {
13448 _apply(query) {
13449 validateNoPreviousOrderByCall(query, 'orderByValue');
13450 const newParams = queryParamsOrderBy(query._queryParams, VALUE_INDEX);
13451 validateQueryEndpoints(newParams);
13452 return new QueryImpl(query._repo, query._path, newParams,
13453 /*orderByCalled=*/ true);
13454 }
13455}
13456/**
13457 * Creates a new `QueryConstraint` that orders by value.
13458 *
13459 * If the children of a query are all scalar values (string, number, or
13460 * boolean), you can order the results by their (ascending) values.
13461 *
13462 * You can read more about `orderByValue()` in
13463 * {@link https://firebase.google.com/docs/database/web/lists-of-data#sort_data | Sort data}.
13464 */
13465function orderByValue() {
13466 return new QueryOrderByValueConstraint();
13467}
13468class QueryEqualToValueConstraint extends QueryConstraint {
13469 constructor(_value, _key) {
13470 super();
13471 this._value = _value;
13472 this._key = _key;
13473 }
13474 _apply(query) {
13475 validateFirebaseDataArg('equalTo', this._value, query._path, false);
13476 if (query._queryParams.hasStart()) {
13477 throw new Error('equalTo: Starting point was already set (by another call to startAt/startAfter or ' +
13478 'equalTo).');
13479 }
13480 if (query._queryParams.hasEnd()) {
13481 throw new Error('equalTo: Ending point was already set (by another call to endAt/endBefore or ' +
13482 'equalTo).');
13483 }
13484 return new QueryEndAtConstraint(this._value, this._key)._apply(new QueryStartAtConstraint(this._value, this._key)._apply(query));
13485 }
13486}
13487/**
13488 * Creates a `QueryConstraint` that includes children that match the specified
13489 * value.
13490 *
13491 * Using `startAt()`, `startAfter()`, `endBefore()`, `endAt()` and `equalTo()`
13492 * allows you to choose arbitrary starting and ending points for your queries.
13493 *
13494 * The optional key argument can be used to further limit the range of the
13495 * query. If it is specified, then children that have exactly the specified
13496 * value must also have exactly the specified key as their key name. This can be
13497 * used to filter result sets with many matches for the same value.
13498 *
13499 * You can read more about `equalTo()` in
13500 * {@link https://firebase.google.com/docs/database/web/lists-of-data#filtering_data | Filtering data}.
13501 *
13502 * @param value - The value to match for. The argument type depends on which
13503 * `orderBy*()` function was used in this query. Specify a value that matches
13504 * the `orderBy*()` type. When used in combination with `orderByKey()`, the
13505 * value must be a string.
13506 * @param key - The child key to start at, among the children with the
13507 * previously specified priority. This argument is only allowed if ordering by
13508 * child, value, or priority.
13509 */
13510function equalTo(value, key) {
13511 validateKey('equalTo', 'key', key, true);
13512 return new QueryEqualToValueConstraint(value, key);
13513}
13514/**
13515 * Creates a new immutable instance of `Query` that is extended to also include
13516 * additional query constraints.
13517 *
13518 * @param query - The Query instance to use as a base for the new constraints.
13519 * @param queryConstraints - The list of `QueryConstraint`s to apply.
13520 * @throws if any of the provided query constraints cannot be combined with the
13521 * existing or new constraints.
13522 */
13523function query(query, ...queryConstraints) {
13524 let queryImpl = getModularInstance(query);
13525 for (const constraint of queryConstraints) {
13526 queryImpl = constraint._apply(queryImpl);
13527 }
13528 return queryImpl;
13529}
13530/**
13531 * Define reference constructor in various modules
13532 *
13533 * We are doing this here to avoid several circular
13534 * dependency issues
13535 */
13536syncPointSetReferenceConstructor(ReferenceImpl);
13537syncTreeSetReferenceConstructor(ReferenceImpl);
13538
13539/**
13540 * @license
13541 * Copyright 2020 Google LLC
13542 *
13543 * Licensed under the Apache License, Version 2.0 (the "License");
13544 * you may not use this file except in compliance with the License.
13545 * You may obtain a copy of the License at
13546 *
13547 * http://www.apache.org/licenses/LICENSE-2.0
13548 *
13549 * Unless required by applicable law or agreed to in writing, software
13550 * distributed under the License is distributed on an "AS IS" BASIS,
13551 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13552 * See the License for the specific language governing permissions and
13553 * limitations under the License.
13554 */
13555/**
13556 * This variable is also defined in the firebase Node.js Admin SDK. Before
13557 * modifying this definition, consult the definition in:
13558 *
13559 * https://github.com/firebase/firebase-admin-node
13560 *
13561 * and make sure the two are consistent.
13562 */
13563const FIREBASE_DATABASE_EMULATOR_HOST_VAR = 'FIREBASE_DATABASE_EMULATOR_HOST';
13564/**
13565 * Creates and caches `Repo` instances.
13566 */
13567const repos = {};
13568/**
13569 * If true, any new `Repo` will be created to use `ReadonlyRestClient` (for testing purposes).
13570 */
13571let useRestClient = false;
13572/**
13573 * Update an existing `Repo` in place to point to a new host/port.
13574 */
13575function repoManagerApplyEmulatorSettings(repo, host, port, tokenProvider) {
13576 repo.repoInfo_ = new RepoInfo(`${host}:${port}`,
13577 /* secure= */ false, repo.repoInfo_.namespace, repo.repoInfo_.webSocketOnly, repo.repoInfo_.nodeAdmin, repo.repoInfo_.persistenceKey, repo.repoInfo_.includeNamespaceInQueryParams);
13578 if (tokenProvider) {
13579 repo.authTokenProvider_ = tokenProvider;
13580 }
13581}
13582/**
13583 * This function should only ever be called to CREATE a new database instance.
13584 * @internal
13585 */
13586function repoManagerDatabaseFromApp(app, authProvider, appCheckProvider, url, nodeAdmin) {
13587 let dbUrl = url || app.options.databaseURL;
13588 if (dbUrl === undefined) {
13589 if (!app.options.projectId) {
13590 fatal("Can't determine Firebase Database URL. Be sure to include " +
13591 ' a Project ID when calling firebase.initializeApp().');
13592 }
13593 log('Using default host for project ', app.options.projectId);
13594 dbUrl = `${app.options.projectId}-default-rtdb.firebaseio.com`;
13595 }
13596 let parsedUrl = parseRepoInfo(dbUrl, nodeAdmin);
13597 let repoInfo = parsedUrl.repoInfo;
13598 let isEmulator;
13599 let dbEmulatorHost = undefined;
13600 if (typeof process !== 'undefined' && process.env) {
13601 dbEmulatorHost = process.env[FIREBASE_DATABASE_EMULATOR_HOST_VAR];
13602 }
13603 if (dbEmulatorHost) {
13604 isEmulator = true;
13605 dbUrl = `http://${dbEmulatorHost}?ns=${repoInfo.namespace}`;
13606 parsedUrl = parseRepoInfo(dbUrl, nodeAdmin);
13607 repoInfo = parsedUrl.repoInfo;
13608 }
13609 else {
13610 isEmulator = !parsedUrl.repoInfo.secure;
13611 }
13612 const authTokenProvider = nodeAdmin && isEmulator
13613 ? new EmulatorTokenProvider(EmulatorTokenProvider.OWNER)
13614 : new FirebaseAuthTokenProvider(app.name, app.options, authProvider);
13615 validateUrl('Invalid Firebase Database URL', parsedUrl);
13616 if (!pathIsEmpty(parsedUrl.path)) {
13617 fatal('Database URL must point to the root of a Firebase Database ' +
13618 '(not including a child path).');
13619 }
13620 const repo = repoManagerCreateRepo(repoInfo, app, authTokenProvider, new AppCheckTokenProvider(app.name, appCheckProvider));
13621 return new Database(repo, app);
13622}
13623/**
13624 * Remove the repo and make sure it is disconnected.
13625 *
13626 */
13627function repoManagerDeleteRepo(repo, appName) {
13628 const appRepos = repos[appName];
13629 // This should never happen...
13630 if (!appRepos || appRepos[repo.key] !== repo) {
13631 fatal(`Database ${appName}(${repo.repoInfo_}) has already been deleted.`);
13632 }
13633 repoInterrupt(repo);
13634 delete appRepos[repo.key];
13635}
13636/**
13637 * Ensures a repo doesn't already exist and then creates one using the
13638 * provided app.
13639 *
13640 * @param repoInfo - The metadata about the Repo
13641 * @returns The Repo object for the specified server / repoName.
13642 */
13643function repoManagerCreateRepo(repoInfo, app, authTokenProvider, appCheckProvider) {
13644 let appRepos = repos[app.name];
13645 if (!appRepos) {
13646 appRepos = {};
13647 repos[app.name] = appRepos;
13648 }
13649 let repo = appRepos[repoInfo.toURLString()];
13650 if (repo) {
13651 fatal('Database initialized multiple times. Please make sure the format of the database URL matches with each database() call.');
13652 }
13653 repo = new Repo(repoInfo, useRestClient, authTokenProvider, appCheckProvider);
13654 appRepos[repoInfo.toURLString()] = repo;
13655 return repo;
13656}
13657/**
13658 * Forces us to use ReadonlyRestClient instead of PersistentConnection for new Repos.
13659 */
13660function repoManagerForceRestClient(forceRestClient) {
13661 useRestClient = forceRestClient;
13662}
13663/**
13664 * Class representing a Firebase Realtime Database.
13665 */
13666class Database {
13667 /** @hideconstructor */
13668 constructor(_repoInternal,
13669 /** The {@link @firebase/app#FirebaseApp} associated with this Realtime Database instance. */
13670 app) {
13671 this._repoInternal = _repoInternal;
13672 this.app = app;
13673 /** Represents a `Database` instance. */
13674 this['type'] = 'database';
13675 /** Track if the instance has been used (root or repo accessed) */
13676 this._instanceStarted = false;
13677 }
13678 get _repo() {
13679 if (!this._instanceStarted) {
13680 repoStart(this._repoInternal, this.app.options.appId, this.app.options['databaseAuthVariableOverride']);
13681 this._instanceStarted = true;
13682 }
13683 return this._repoInternal;
13684 }
13685 get _root() {
13686 if (!this._rootInternal) {
13687 this._rootInternal = new ReferenceImpl(this._repo, newEmptyPath());
13688 }
13689 return this._rootInternal;
13690 }
13691 _delete() {
13692 if (this._rootInternal !== null) {
13693 repoManagerDeleteRepo(this._repo, this.app.name);
13694 this._repoInternal = null;
13695 this._rootInternal = null;
13696 }
13697 return Promise.resolve();
13698 }
13699 _checkNotDeleted(apiName) {
13700 if (this._rootInternal === null) {
13701 fatal('Cannot call ' + apiName + ' on a deleted database.');
13702 }
13703 }
13704}
13705function checkTransportInit() {
13706 if (TransportManager.IS_TRANSPORT_INITIALIZED) {
13707 warn('Transport has already been initialized. Please call this function before calling ref or setting up a listener');
13708 }
13709}
13710/**
13711 * Force the use of websockets instead of longPolling.
13712 */
13713function forceWebSockets() {
13714 checkTransportInit();
13715 BrowserPollConnection.forceDisallow();
13716}
13717/**
13718 * Force the use of longPolling instead of websockets. This will be ignored if websocket protocol is used in databaseURL.
13719 */
13720function forceLongPolling() {
13721 checkTransportInit();
13722 WebSocketConnection.forceDisallow();
13723 BrowserPollConnection.forceAllow();
13724}
13725/**
13726 * Returns the instance of the Realtime Database SDK that is associated
13727 * with the provided {@link @firebase/app#FirebaseApp}. Initializes a new instance with
13728 * with default settings if no instance exists or if the existing instance uses
13729 * a custom database URL.
13730 *
13731 * @param app - The {@link @firebase/app#FirebaseApp} instance that the returned Realtime
13732 * Database instance is associated with.
13733 * @param url - The URL of the Realtime Database instance to connect to. If not
13734 * provided, the SDK connects to the default instance of the Firebase App.
13735 * @returns The `Database` instance of the provided app.
13736 */
13737function getDatabase(app = getApp(), url) {
13738 return _getProvider(app, 'database').getImmediate({
13739 identifier: url
13740 });
13741}
13742/**
13743 * Modify the provided instance to communicate with the Realtime Database
13744 * emulator.
13745 *
13746 * <p>Note: This method must be called before performing any other operation.
13747 *
13748 * @param db - The instance to modify.
13749 * @param host - The emulator host (ex: localhost)
13750 * @param port - The emulator port (ex: 8080)
13751 * @param options.mockUserToken - the mock auth token to use for unit testing Security Rules
13752 */
13753function connectDatabaseEmulator(db, host, port, options = {}) {
13754 db = getModularInstance(db);
13755 db._checkNotDeleted('useEmulator');
13756 if (db._instanceStarted) {
13757 fatal('Cannot call useEmulator() after instance has already been initialized.');
13758 }
13759 const repo = db._repoInternal;
13760 let tokenProvider = undefined;
13761 if (repo.repoInfo_.nodeAdmin) {
13762 if (options.mockUserToken) {
13763 fatal('mockUserToken is not supported by the Admin SDK. For client access with mock users, please use the "firebase" package instead of "firebase-admin".');
13764 }
13765 tokenProvider = new EmulatorTokenProvider(EmulatorTokenProvider.OWNER);
13766 }
13767 else if (options.mockUserToken) {
13768 const token = typeof options.mockUserToken === 'string'
13769 ? options.mockUserToken
13770 : createMockUserToken(options.mockUserToken, db.app.options.projectId);
13771 tokenProvider = new EmulatorTokenProvider(token);
13772 }
13773 // Modify the repo to apply emulator settings
13774 repoManagerApplyEmulatorSettings(repo, host, port, tokenProvider);
13775}
13776/**
13777 * Disconnects from the server (all Database operations will be completed
13778 * offline).
13779 *
13780 * The client automatically maintains a persistent connection to the Database
13781 * server, which will remain active indefinitely and reconnect when
13782 * disconnected. However, the `goOffline()` and `goOnline()` methods may be used
13783 * to control the client connection in cases where a persistent connection is
13784 * undesirable.
13785 *
13786 * While offline, the client will no longer receive data updates from the
13787 * Database. However, all Database operations performed locally will continue to
13788 * immediately fire events, allowing your application to continue behaving
13789 * normally. Additionally, each operation performed locally will automatically
13790 * be queued and retried upon reconnection to the Database server.
13791 *
13792 * To reconnect to the Database and begin receiving remote events, see
13793 * `goOnline()`.
13794 *
13795 * @param db - The instance to disconnect.
13796 */
13797function goOffline(db) {
13798 db = getModularInstance(db);
13799 db._checkNotDeleted('goOffline');
13800 repoInterrupt(db._repo);
13801}
13802/**
13803 * Reconnects to the server and synchronizes the offline Database state
13804 * with the server state.
13805 *
13806 * This method should be used after disabling the active connection with
13807 * `goOffline()`. Once reconnected, the client will transmit the proper data
13808 * and fire the appropriate events so that your client "catches up"
13809 * automatically.
13810 *
13811 * @param db - The instance to reconnect.
13812 */
13813function goOnline(db) {
13814 db = getModularInstance(db);
13815 db._checkNotDeleted('goOnline');
13816 repoResume(db._repo);
13817}
13818function enableLogging(logger, persistent) {
13819 enableLogging$1(logger, persistent);
13820}
13821
13822/**
13823 * @license
13824 * Copyright 2021 Google LLC
13825 *
13826 * Licensed under the Apache License, Version 2.0 (the "License");
13827 * you may not use this file except in compliance with the License.
13828 * You may obtain a copy of the License at
13829 *
13830 * http://www.apache.org/licenses/LICENSE-2.0
13831 *
13832 * Unless required by applicable law or agreed to in writing, software
13833 * distributed under the License is distributed on an "AS IS" BASIS,
13834 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13835 * See the License for the specific language governing permissions and
13836 * limitations under the License.
13837 */
13838function registerDatabase(variant) {
13839 setSDKVersion(SDK_VERSION$1);
13840 _registerComponent(new Component('database', (container, { instanceIdentifier: url }) => {
13841 const app = container.getProvider('app').getImmediate();
13842 const authProvider = container.getProvider('auth-internal');
13843 const appCheckProvider = container.getProvider('app-check-internal');
13844 return repoManagerDatabaseFromApp(app, authProvider, appCheckProvider, url);
13845 }, "PUBLIC" /* PUBLIC */).setMultipleInstances(true));
13846 registerVersion(name, version, variant);
13847 // BUILD_TARGET will be replaced by values like esm5, esm2017, cjs5, etc during the compilation
13848 registerVersion(name, version, 'esm2017');
13849}
13850
13851/**
13852 * @license
13853 * Copyright 2020 Google LLC
13854 *
13855 * Licensed under the Apache License, Version 2.0 (the "License");
13856 * you may not use this file except in compliance with the License.
13857 * You may obtain a copy of the License at
13858 *
13859 * http://www.apache.org/licenses/LICENSE-2.0
13860 *
13861 * Unless required by applicable law or agreed to in writing, software
13862 * distributed under the License is distributed on an "AS IS" BASIS,
13863 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13864 * See the License for the specific language governing permissions and
13865 * limitations under the License.
13866 */
13867const SERVER_TIMESTAMP = {
13868 '.sv': 'timestamp'
13869};
13870/**
13871 * Returns a placeholder value for auto-populating the current timestamp (time
13872 * since the Unix epoch, in milliseconds) as determined by the Firebase
13873 * servers.
13874 */
13875function serverTimestamp() {
13876 return SERVER_TIMESTAMP;
13877}
13878/**
13879 * Returns a placeholder value that can be used to atomically increment the
13880 * current database value by the provided delta.
13881 *
13882 * @param delta - the amount to modify the current value atomically.
13883 * @returns A placeholder value for modifying data atomically server-side.
13884 */
13885function increment(delta) {
13886 return {
13887 '.sv': {
13888 'increment': delta
13889 }
13890 };
13891}
13892
13893/**
13894 * @license
13895 * Copyright 2020 Google LLC
13896 *
13897 * Licensed under the Apache License, Version 2.0 (the "License");
13898 * you may not use this file except in compliance with the License.
13899 * You may obtain a copy of the License at
13900 *
13901 * http://www.apache.org/licenses/LICENSE-2.0
13902 *
13903 * Unless required by applicable law or agreed to in writing, software
13904 * distributed under the License is distributed on an "AS IS" BASIS,
13905 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13906 * See the License for the specific language governing permissions and
13907 * limitations under the License.
13908 */
13909/**
13910 * A type for the resolve value of {@link runTransaction}.
13911 */
13912class TransactionResult {
13913 /** @hideconstructor */
13914 constructor(
13915 /** Whether the transaction was successfully committed. */
13916 committed,
13917 /** The resulting data snapshot. */
13918 snapshot) {
13919 this.committed = committed;
13920 this.snapshot = snapshot;
13921 }
13922 /** Returns a JSON-serializable representation of this object. */
13923 toJSON() {
13924 return { committed: this.committed, snapshot: this.snapshot.toJSON() };
13925 }
13926}
13927/**
13928 * Atomically modifies the data at this location.
13929 *
13930 * Atomically modify the data at this location. Unlike a normal `set()`, which
13931 * just overwrites the data regardless of its previous value, `runTransaction()` is
13932 * used to modify the existing value to a new value, ensuring there are no
13933 * conflicts with other clients writing to the same location at the same time.
13934 *
13935 * To accomplish this, you pass `runTransaction()` an update function which is
13936 * used to transform the current value into a new value. If another client
13937 * writes to the location before your new value is successfully written, your
13938 * update function will be called again with the new current value, and the
13939 * write will be retried. This will happen repeatedly until your write succeeds
13940 * without conflict or you abort the transaction by not returning a value from
13941 * your update function.
13942 *
13943 * Note: Modifying data with `set()` will cancel any pending transactions at
13944 * that location, so extreme care should be taken if mixing `set()` and
13945 * `runTransaction()` to update the same data.
13946 *
13947 * Note: When using transactions with Security and Firebase Rules in place, be
13948 * aware that a client needs `.read` access in addition to `.write` access in
13949 * order to perform a transaction. This is because the client-side nature of
13950 * transactions requires the client to read the data in order to transactionally
13951 * update it.
13952 *
13953 * @param ref - The location to atomically modify.
13954 * @param transactionUpdate - A developer-supplied function which will be passed
13955 * the current data stored at this location (as a JavaScript object). The
13956 * function should return the new value it would like written (as a JavaScript
13957 * object). If `undefined` is returned (i.e. you return with no arguments) the
13958 * transaction will be aborted and the data at this location will not be
13959 * modified.
13960 * @param options - An options object to configure transactions.
13961 * @returns A `Promise` that can optionally be used instead of the `onComplete`
13962 * callback to handle success and failure.
13963 */
13964function runTransaction(ref,
13965// eslint-disable-next-line @typescript-eslint/no-explicit-any
13966transactionUpdate, options) {
13967 var _a;
13968 ref = getModularInstance(ref);
13969 validateWritablePath('Reference.transaction', ref._path);
13970 if (ref.key === '.length' || ref.key === '.keys') {
13971 throw ('Reference.transaction failed: ' + ref.key + ' is a read-only object.');
13972 }
13973 const applyLocally = (_a = options === null || options === void 0 ? void 0 : options.applyLocally) !== null && _a !== void 0 ? _a : true;
13974 const deferred = new Deferred();
13975 const promiseComplete = (error, committed, node) => {
13976 let dataSnapshot = null;
13977 if (error) {
13978 deferred.reject(error);
13979 }
13980 else {
13981 dataSnapshot = new DataSnapshot(node, new ReferenceImpl(ref._repo, ref._path), PRIORITY_INDEX);
13982 deferred.resolve(new TransactionResult(committed, dataSnapshot));
13983 }
13984 };
13985 // Add a watch to make sure we get server updates.
13986 const unwatcher = onValue(ref, () => { });
13987 repoStartTransaction(ref._repo, ref._path, transactionUpdate, promiseComplete, unwatcher, applyLocally);
13988 return deferred.promise;
13989}
13990
13991/**
13992 * @license
13993 * Copyright 2017 Google LLC
13994 *
13995 * Licensed under the Apache License, Version 2.0 (the "License");
13996 * you may not use this file except in compliance with the License.
13997 * You may obtain a copy of the License at
13998 *
13999 * http://www.apache.org/licenses/LICENSE-2.0
14000 *
14001 * Unless required by applicable law or agreed to in writing, software
14002 * distributed under the License is distributed on an "AS IS" BASIS,
14003 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14004 * See the License for the specific language governing permissions and
14005 * limitations under the License.
14006 */
14007PersistentConnection;
14008// eslint-disable-next-line @typescript-eslint/no-explicit-any
14009PersistentConnection.prototype.simpleListen = function (pathString, onComplete) {
14010 this.sendRequest('q', { p: pathString }, onComplete);
14011};
14012// eslint-disable-next-line @typescript-eslint/no-explicit-any
14013PersistentConnection.prototype.echo = function (data, onEcho) {
14014 this.sendRequest('echo', { d: data }, onEcho);
14015};
14016// RealTimeConnection properties that we use in tests.
14017Connection;
14018/**
14019 * @internal
14020 */
14021const hijackHash = function (newHash) {
14022 const oldPut = PersistentConnection.prototype.put;
14023 PersistentConnection.prototype.put = function (pathString, data, onComplete, hash) {
14024 if (hash !== undefined) {
14025 hash = newHash();
14026 }
14027 oldPut.call(this, pathString, data, onComplete, hash);
14028 };
14029 return function () {
14030 PersistentConnection.prototype.put = oldPut;
14031 };
14032};
14033RepoInfo;
14034/**
14035 * Forces the RepoManager to create Repos that use ReadonlyRestClient instead of PersistentConnection.
14036 * @internal
14037 */
14038const forceRestClient = function (forceRestClient) {
14039 repoManagerForceRestClient(forceRestClient);
14040};
14041
14042/**
14043 * @license
14044 * Copyright 2021 Google LLC
14045 *
14046 * Licensed under the Apache License, Version 2.0 (the "License");
14047 * you may not use this file except in compliance with the License.
14048 * You may obtain a copy of the License at
14049 *
14050 * http://www.apache.org/licenses/LICENSE-2.0
14051 *
14052 * Unless required by applicable law or agreed to in writing, software
14053 * distributed under the License is distributed on an "AS IS" BASIS,
14054 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14055 * See the License for the specific language governing permissions and
14056 * limitations under the License.
14057 */
14058setWebSocketImpl(Websocket.Client);
14059registerDatabase('node');
14060
14061export { DataSnapshot, Database, OnDisconnect, QueryConstraint, TransactionResult, QueryImpl as _QueryImpl, QueryParams as _QueryParams, ReferenceImpl as _ReferenceImpl, forceRestClient as _TEST_ACCESS_forceRestClient, hijackHash as _TEST_ACCESS_hijackHash, repoManagerDatabaseFromApp as _repoManagerDatabaseFromApp, setSDKVersion as _setSDKVersion, validatePathString as _validatePathString, validateWritablePath as _validateWritablePath, child, connectDatabaseEmulator, enableLogging, endAt, endBefore, equalTo, forceLongPolling, forceWebSockets, get, getDatabase, goOffline, goOnline, increment, limitToFirst, limitToLast, off, onChildAdded, onChildChanged, onChildMoved, onChildRemoved, onDisconnect, onValue, orderByChild, orderByKey, orderByPriority, orderByValue, push, query, ref, refFromURL, remove, runTransaction, serverTimestamp, set, setPriority, setWithPriority, startAfter, startAt, update };
14062//# sourceMappingURL=index.node.esm.js.map