UNPKG

562 kBJavaScriptView Raw
1import { getApp, _getProvider, SDK_VERSION as SDK_VERSION$1, _registerComponent, registerVersion } from '@firebase/app';
2import { Component } from '@firebase/component';
3import { 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';
4import { Logger, LogLevel } from '@firebase/logger';
5
6const name = "@firebase/database";
7const version = "0.13.4";
8
9/**
10 * @license
11 * Copyright 2019 Google LLC
12 *
13 * Licensed under the Apache License, Version 2.0 (the "License");
14 * you may not use this file except in compliance with the License.
15 * You may obtain a copy of the License at
16 *
17 * http://www.apache.org/licenses/LICENSE-2.0
18 *
19 * Unless required by applicable law or agreed to in writing, software
20 * distributed under the License is distributed on an "AS IS" BASIS,
21 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22 * See the License for the specific language governing permissions and
23 * limitations under the License.
24 */
25/** The semver (www.semver.org) version of the SDK. */
26let SDK_VERSION = '';
27/**
28 * SDK_VERSION should be set before any database instance is created
29 * @internal
30 */
31function setSDKVersion(version) {
32 SDK_VERSION = version;
33}
34
35/**
36 * @license
37 * Copyright 2017 Google LLC
38 *
39 * Licensed under the Apache License, Version 2.0 (the "License");
40 * you may not use this file except in compliance with the License.
41 * You may obtain a copy of the License at
42 *
43 * http://www.apache.org/licenses/LICENSE-2.0
44 *
45 * Unless required by applicable law or agreed to in writing, software
46 * distributed under the License is distributed on an "AS IS" BASIS,
47 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
48 * See the License for the specific language governing permissions and
49 * limitations under the License.
50 */
51/**
52 * Wraps a DOM Storage object and:
53 * - automatically encode objects as JSON strings before storing them to allow us to store arbitrary types.
54 * - prefixes names with "firebase:" to avoid collisions with app data.
55 *
56 * We automatically (see storage.js) create two such wrappers, one for sessionStorage,
57 * and one for localStorage.
58 *
59 */
60class DOMStorageWrapper {
61 /**
62 * @param domStorage_ - The underlying storage object (e.g. localStorage or sessionStorage)
63 */
64 constructor(domStorage_) {
65 this.domStorage_ = domStorage_;
66 // Use a prefix to avoid collisions with other stuff saved by the app.
67 this.prefix_ = 'firebase:';
68 }
69 /**
70 * @param key - The key to save the value under
71 * @param value - The value being stored, or null to remove the key.
72 */
73 set(key, value) {
74 if (value == null) {
75 this.domStorage_.removeItem(this.prefixedName_(key));
76 }
77 else {
78 this.domStorage_.setItem(this.prefixedName_(key), stringify(value));
79 }
80 }
81 /**
82 * @returns The value that was stored under this key, or null
83 */
84 get(key) {
85 const storedVal = this.domStorage_.getItem(this.prefixedName_(key));
86 if (storedVal == null) {
87 return null;
88 }
89 else {
90 return jsonEval(storedVal);
91 }
92 }
93 remove(key) {
94 this.domStorage_.removeItem(this.prefixedName_(key));
95 }
96 prefixedName_(name) {
97 return this.prefix_ + name;
98 }
99 toString() {
100 return this.domStorage_.toString();
101 }
102}
103
104/**
105 * @license
106 * Copyright 2017 Google LLC
107 *
108 * Licensed under the Apache License, Version 2.0 (the "License");
109 * you may not use this file except in compliance with the License.
110 * You may obtain a copy of the License at
111 *
112 * http://www.apache.org/licenses/LICENSE-2.0
113 *
114 * Unless required by applicable law or agreed to in writing, software
115 * distributed under the License is distributed on an "AS IS" BASIS,
116 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
117 * See the License for the specific language governing permissions and
118 * limitations under the License.
119 */
120/**
121 * An in-memory storage implementation that matches the API of DOMStorageWrapper
122 * (TODO: create interface for both to implement).
123 */
124class MemoryStorage {
125 constructor() {
126 this.cache_ = {};
127 this.isInMemoryStorage = true;
128 }
129 set(key, value) {
130 if (value == null) {
131 delete this.cache_[key];
132 }
133 else {
134 this.cache_[key] = value;
135 }
136 }
137 get(key) {
138 if (contains(this.cache_, key)) {
139 return this.cache_[key];
140 }
141 return null;
142 }
143 remove(key) {
144 delete this.cache_[key];
145 }
146}
147
148/**
149 * @license
150 * Copyright 2017 Google LLC
151 *
152 * Licensed under the Apache License, Version 2.0 (the "License");
153 * you may not use this file except in compliance with the License.
154 * You may obtain a copy of the License at
155 *
156 * http://www.apache.org/licenses/LICENSE-2.0
157 *
158 * Unless required by applicable law or agreed to in writing, software
159 * distributed under the License is distributed on an "AS IS" BASIS,
160 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
161 * See the License for the specific language governing permissions and
162 * limitations under the License.
163 */
164/**
165 * Helper to create a DOMStorageWrapper or else fall back to MemoryStorage.
166 * TODO: Once MemoryStorage and DOMStorageWrapper have a shared interface this method annotation should change
167 * to reflect this type
168 *
169 * @param domStorageName - Name of the underlying storage object
170 * (e.g. 'localStorage' or 'sessionStorage').
171 * @returns Turning off type information until a common interface is defined.
172 */
173const createStoragefor = function (domStorageName) {
174 try {
175 // NOTE: just accessing "localStorage" or "window['localStorage']" may throw a security exception,
176 // so it must be inside the try/catch.
177 if (typeof window !== 'undefined' &&
178 typeof window[domStorageName] !== 'undefined') {
179 // Need to test cache. Just because it's here doesn't mean it works
180 const domStorage = window[domStorageName];
181 domStorage.setItem('firebase:sentinel', 'cache');
182 domStorage.removeItem('firebase:sentinel');
183 return new DOMStorageWrapper(domStorage);
184 }
185 }
186 catch (e) { }
187 // Failed to create wrapper. Just return in-memory storage.
188 // TODO: log?
189 return new MemoryStorage();
190};
191/** A storage object that lasts across sessions */
192const PersistentStorage = createStoragefor('localStorage');
193/** A storage object that only lasts one session */
194const SessionStorage = createStoragefor('sessionStorage');
195
196/**
197 * @license
198 * Copyright 2017 Google LLC
199 *
200 * Licensed under the Apache License, Version 2.0 (the "License");
201 * you may not use this file except in compliance with the License.
202 * You may obtain a copy of the License at
203 *
204 * http://www.apache.org/licenses/LICENSE-2.0
205 *
206 * Unless required by applicable law or agreed to in writing, software
207 * distributed under the License is distributed on an "AS IS" BASIS,
208 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
209 * See the License for the specific language governing permissions and
210 * limitations under the License.
211 */
212const logClient = new Logger('@firebase/database');
213/**
214 * Returns a locally-unique ID (generated by just incrementing up from 0 each time its called).
215 */
216const LUIDGenerator = (function () {
217 let id = 1;
218 return function () {
219 return id++;
220 };
221})();
222/**
223 * Sha1 hash of the input string
224 * @param str - The string to hash
225 * @returns {!string} The resulting hash
226 */
227const sha1 = function (str) {
228 const utf8Bytes = stringToByteArray(str);
229 const sha1 = new Sha1();
230 sha1.update(utf8Bytes);
231 const sha1Bytes = sha1.digest();
232 return base64.encodeByteArray(sha1Bytes);
233};
234const buildLogMessage_ = function (...varArgs) {
235 let message = '';
236 for (let i = 0; i < varArgs.length; i++) {
237 const arg = varArgs[i];
238 if (Array.isArray(arg) ||
239 (arg &&
240 typeof arg === 'object' &&
241 // eslint-disable-next-line @typescript-eslint/no-explicit-any
242 typeof arg.length === 'number')) {
243 message += buildLogMessage_.apply(null, arg);
244 }
245 else if (typeof arg === 'object') {
246 message += stringify(arg);
247 }
248 else {
249 message += arg;
250 }
251 message += ' ';
252 }
253 return message;
254};
255/**
256 * Use this for all debug messages in Firebase.
257 */
258let logger = null;
259/**
260 * Flag to check for log availability on first log message
261 */
262let firstLog_ = true;
263/**
264 * The implementation of Firebase.enableLogging (defined here to break dependencies)
265 * @param logger_ - A flag to turn on logging, or a custom logger
266 * @param persistent - Whether or not to persist logging settings across refreshes
267 */
268const enableLogging$1 = function (logger_, persistent) {
269 assert(!persistent || logger_ === true || logger_ === false, "Can't turn on custom loggers persistently.");
270 if (logger_ === true) {
271 logClient.logLevel = LogLevel.VERBOSE;
272 logger = logClient.log.bind(logClient);
273 if (persistent) {
274 SessionStorage.set('logging_enabled', true);
275 }
276 }
277 else if (typeof logger_ === 'function') {
278 logger = logger_;
279 }
280 else {
281 logger = null;
282 SessionStorage.remove('logging_enabled');
283 }
284};
285const log = function (...varArgs) {
286 if (firstLog_ === true) {
287 firstLog_ = false;
288 if (logger === null && SessionStorage.get('logging_enabled') === true) {
289 enableLogging$1(true);
290 }
291 }
292 if (logger) {
293 const message = buildLogMessage_.apply(null, varArgs);
294 logger(message);
295 }
296};
297const logWrapper = function (prefix) {
298 return function (...varArgs) {
299 log(prefix, ...varArgs);
300 };
301};
302const error = function (...varArgs) {
303 const message = 'FIREBASE INTERNAL ERROR: ' + buildLogMessage_(...varArgs);
304 logClient.error(message);
305};
306const fatal = function (...varArgs) {
307 const message = `FIREBASE FATAL ERROR: ${buildLogMessage_(...varArgs)}`;
308 logClient.error(message);
309 throw new Error(message);
310};
311const warn = function (...varArgs) {
312 const message = 'FIREBASE WARNING: ' + buildLogMessage_(...varArgs);
313 logClient.warn(message);
314};
315/**
316 * Logs a warning if the containing page uses https. Called when a call to new Firebase
317 * does not use https.
318 */
319const warnIfPageIsSecure = function () {
320 // Be very careful accessing browser globals. Who knows what may or may not exist.
321 if (typeof window !== 'undefined' &&
322 window.location &&
323 window.location.protocol &&
324 window.location.protocol.indexOf('https:') !== -1) {
325 warn('Insecure Firebase access from a secure page. ' +
326 'Please use https in calls to new Firebase().');
327 }
328};
329/**
330 * Returns true if data is NaN, or +/- Infinity.
331 */
332const isInvalidJSONNumber = function (data) {
333 return (typeof data === 'number' &&
334 (data !== data || // NaN
335 data === Number.POSITIVE_INFINITY ||
336 data === Number.NEGATIVE_INFINITY));
337};
338const executeWhenDOMReady = function (fn) {
339 if (isNodeSdk() || document.readyState === 'complete') {
340 fn();
341 }
342 else {
343 // Modeled after jQuery. Try DOMContentLoaded and onreadystatechange (which
344 // fire before onload), but fall back to onload.
345 let called = false;
346 const wrappedFn = function () {
347 if (!document.body) {
348 setTimeout(wrappedFn, Math.floor(10));
349 return;
350 }
351 if (!called) {
352 called = true;
353 fn();
354 }
355 };
356 if (document.addEventListener) {
357 document.addEventListener('DOMContentLoaded', wrappedFn, false);
358 // fallback to onload.
359 window.addEventListener('load', wrappedFn, false);
360 // eslint-disable-next-line @typescript-eslint/no-explicit-any
361 }
362 else if (document.attachEvent) {
363 // IE.
364 // eslint-disable-next-line @typescript-eslint/no-explicit-any
365 document.attachEvent('onreadystatechange', () => {
366 if (document.readyState === 'complete') {
367 wrappedFn();
368 }
369 });
370 // fallback to onload.
371 // eslint-disable-next-line @typescript-eslint/no-explicit-any
372 window.attachEvent('onload', wrappedFn);
373 // jQuery has an extra hack for IE that we could employ (based on
374 // http://javascript.nwbox.com/IEContentLoaded/) But it looks really old.
375 // I'm hoping we don't need it.
376 }
377 }
378};
379/**
380 * Minimum key name. Invalid for actual data, used as a marker to sort before any valid names
381 */
382const MIN_NAME = '[MIN_NAME]';
383/**
384 * Maximum key name. Invalid for actual data, used as a marker to sort above any valid names
385 */
386const MAX_NAME = '[MAX_NAME]';
387/**
388 * Compares valid Firebase key names, plus min and max name
389 */
390const nameCompare = function (a, b) {
391 if (a === b) {
392 return 0;
393 }
394 else if (a === MIN_NAME || b === MAX_NAME) {
395 return -1;
396 }
397 else if (b === MIN_NAME || a === MAX_NAME) {
398 return 1;
399 }
400 else {
401 const aAsInt = tryParseInt(a), bAsInt = tryParseInt(b);
402 if (aAsInt !== null) {
403 if (bAsInt !== null) {
404 return aAsInt - bAsInt === 0 ? a.length - b.length : aAsInt - bAsInt;
405 }
406 else {
407 return -1;
408 }
409 }
410 else if (bAsInt !== null) {
411 return 1;
412 }
413 else {
414 return a < b ? -1 : 1;
415 }
416 }
417};
418/**
419 * @returns {!number} comparison result.
420 */
421const stringCompare = function (a, b) {
422 if (a === b) {
423 return 0;
424 }
425 else if (a < b) {
426 return -1;
427 }
428 else {
429 return 1;
430 }
431};
432const requireKey = function (key, obj) {
433 if (obj && key in obj) {
434 return obj[key];
435 }
436 else {
437 throw new Error('Missing required key (' + key + ') in object: ' + stringify(obj));
438 }
439};
440const ObjectToUniqueKey = function (obj) {
441 if (typeof obj !== 'object' || obj === null) {
442 return stringify(obj);
443 }
444 const keys = [];
445 // eslint-disable-next-line guard-for-in
446 for (const k in obj) {
447 keys.push(k);
448 }
449 // Export as json, but with the keys sorted.
450 keys.sort();
451 let key = '{';
452 for (let i = 0; i < keys.length; i++) {
453 if (i !== 0) {
454 key += ',';
455 }
456 key += stringify(keys[i]);
457 key += ':';
458 key += ObjectToUniqueKey(obj[keys[i]]);
459 }
460 key += '}';
461 return key;
462};
463/**
464 * Splits a string into a number of smaller segments of maximum size
465 * @param str - The string
466 * @param segsize - The maximum number of chars in the string.
467 * @returns The string, split into appropriately-sized chunks
468 */
469const splitStringBySize = function (str, segsize) {
470 const len = str.length;
471 if (len <= segsize) {
472 return [str];
473 }
474 const dataSegs = [];
475 for (let c = 0; c < len; c += segsize) {
476 if (c + segsize > len) {
477 dataSegs.push(str.substring(c, len));
478 }
479 else {
480 dataSegs.push(str.substring(c, c + segsize));
481 }
482 }
483 return dataSegs;
484};
485/**
486 * Apply a function to each (key, value) pair in an object or
487 * apply a function to each (index, value) pair in an array
488 * @param obj - The object or array to iterate over
489 * @param fn - The function to apply
490 */
491function each(obj, fn) {
492 for (const key in obj) {
493 if (obj.hasOwnProperty(key)) {
494 fn(key, obj[key]);
495 }
496 }
497}
498/**
499 * Borrowed from http://hg.secondlife.com/llsd/src/tip/js/typedarray.js (MIT License)
500 * I made one modification at the end and removed the NaN / Infinity
501 * handling (since it seemed broken [caused an overflow] and we don't need it). See MJL comments.
502 * @param v - A double
503 *
504 */
505const doubleToIEEE754String = function (v) {
506 assert(!isInvalidJSONNumber(v), 'Invalid JSON number'); // MJL
507 const ebits = 11, fbits = 52;
508 const bias = (1 << (ebits - 1)) - 1;
509 let s, e, f, ln, i;
510 // Compute sign, exponent, fraction
511 // Skip NaN / Infinity handling --MJL.
512 if (v === 0) {
513 e = 0;
514 f = 0;
515 s = 1 / v === -Infinity ? 1 : 0;
516 }
517 else {
518 s = v < 0;
519 v = Math.abs(v);
520 if (v >= Math.pow(2, 1 - bias)) {
521 // Normalized
522 ln = Math.min(Math.floor(Math.log(v) / Math.LN2), bias);
523 e = ln + bias;
524 f = Math.round(v * Math.pow(2, fbits - ln) - Math.pow(2, fbits));
525 }
526 else {
527 // Denormalized
528 e = 0;
529 f = Math.round(v / Math.pow(2, 1 - bias - fbits));
530 }
531 }
532 // Pack sign, exponent, fraction
533 const bits = [];
534 for (i = fbits; i; i -= 1) {
535 bits.push(f % 2 ? 1 : 0);
536 f = Math.floor(f / 2);
537 }
538 for (i = ebits; i; i -= 1) {
539 bits.push(e % 2 ? 1 : 0);
540 e = Math.floor(e / 2);
541 }
542 bits.push(s ? 1 : 0);
543 bits.reverse();
544 const str = bits.join('');
545 // Return the data as a hex string. --MJL
546 let hexByteString = '';
547 for (i = 0; i < 64; i += 8) {
548 let hexByte = parseInt(str.substr(i, 8), 2).toString(16);
549 if (hexByte.length === 1) {
550 hexByte = '0' + hexByte;
551 }
552 hexByteString = hexByteString + hexByte;
553 }
554 return hexByteString.toLowerCase();
555};
556/**
557 * Used to detect if we're in a Chrome content script (which executes in an
558 * isolated environment where long-polling doesn't work).
559 */
560const isChromeExtensionContentScript = function () {
561 return !!(typeof window === 'object' &&
562 window['chrome'] &&
563 window['chrome']['extension'] &&
564 !/^chrome/.test(window.location.href));
565};
566/**
567 * Used to detect if we're in a Windows 8 Store app.
568 */
569const isWindowsStoreApp = function () {
570 // Check for the presence of a couple WinRT globals
571 return typeof Windows === 'object' && typeof Windows.UI === 'object';
572};
573/**
574 * Converts a server error code to a Javascript Error
575 */
576function errorForServerCode(code, query) {
577 let reason = 'Unknown Error';
578 if (code === 'too_big') {
579 reason =
580 'The data requested exceeds the maximum size ' +
581 'that can be accessed with a single request.';
582 }
583 else if (code === 'permission_denied') {
584 reason = "Client doesn't have permission to access the desired data.";
585 }
586 else if (code === 'unavailable') {
587 reason = 'The service is unavailable';
588 }
589 const error = new Error(code + ' at ' + query._path.toString() + ': ' + reason);
590 // eslint-disable-next-line @typescript-eslint/no-explicit-any
591 error.code = code.toUpperCase();
592 return error;
593}
594/**
595 * Used to test for integer-looking strings
596 */
597const INTEGER_REGEXP_ = new RegExp('^-?(0*)\\d{1,10}$');
598/**
599 * For use in keys, the minimum possible 32-bit integer.
600 */
601const INTEGER_32_MIN = -2147483648;
602/**
603 * For use in kyes, the maximum possible 32-bit integer.
604 */
605const INTEGER_32_MAX = 2147483647;
606/**
607 * If the string contains a 32-bit integer, return it. Else return null.
608 */
609const tryParseInt = function (str) {
610 if (INTEGER_REGEXP_.test(str)) {
611 const intVal = Number(str);
612 if (intVal >= INTEGER_32_MIN && intVal <= INTEGER_32_MAX) {
613 return intVal;
614 }
615 }
616 return null;
617};
618/**
619 * Helper to run some code but catch any exceptions and re-throw them later.
620 * Useful for preventing user callbacks from breaking internal code.
621 *
622 * Re-throwing the exception from a setTimeout is a little evil, but it's very
623 * convenient (we don't have to try to figure out when is a safe point to
624 * re-throw it), and the behavior seems reasonable:
625 *
626 * * If you aren't pausing on exceptions, you get an error in the console with
627 * the correct stack trace.
628 * * If you're pausing on all exceptions, the debugger will pause on your
629 * exception and then again when we rethrow it.
630 * * If you're only pausing on uncaught exceptions, the debugger will only pause
631 * on us re-throwing it.
632 *
633 * @param fn - The code to guard.
634 */
635const exceptionGuard = function (fn) {
636 try {
637 fn();
638 }
639 catch (e) {
640 // Re-throw exception when it's safe.
641 setTimeout(() => {
642 // It used to be that "throw e" would result in a good console error with
643 // relevant context, but as of Chrome 39, you just get the firebase.js
644 // file/line number where we re-throw it, which is useless. So we log
645 // e.stack explicitly.
646 const stack = e.stack || '';
647 warn('Exception was thrown by user callback.', stack);
648 throw e;
649 }, Math.floor(0));
650 }
651};
652/**
653 * @returns {boolean} true if we think we're currently being crawled.
654 */
655const beingCrawled = function () {
656 const userAgent = (typeof window === 'object' &&
657 window['navigator'] &&
658 window['navigator']['userAgent']) ||
659 '';
660 // For now we whitelist the most popular crawlers. We should refine this to be the set of crawlers we
661 // believe to support JavaScript/AJAX rendering.
662 // NOTE: Google Webmaster Tools doesn't really belong, but their "This is how a visitor to your website
663 // would have seen the page" is flaky if we don't treat it as a crawler.
664 return (userAgent.search(/googlebot|google webmaster tools|bingbot|yahoo! slurp|baiduspider|yandexbot|duckduckbot/i) >= 0);
665};
666/**
667 * Same as setTimeout() except on Node.JS it will /not/ prevent the process from exiting.
668 *
669 * It is removed with clearTimeout() as normal.
670 *
671 * @param fn - Function to run.
672 * @param time - Milliseconds to wait before running.
673 * @returns The setTimeout() return value.
674 */
675const setTimeoutNonBlocking = function (fn, time) {
676 const timeout = setTimeout(fn, time);
677 // eslint-disable-next-line @typescript-eslint/no-explicit-any
678 if (typeof timeout === 'object' && timeout['unref']) {
679 // eslint-disable-next-line @typescript-eslint/no-explicit-any
680 timeout['unref']();
681 }
682 return timeout;
683};
684
685/**
686 * @license
687 * Copyright 2021 Google LLC
688 *
689 * Licensed under the Apache License, Version 2.0 (the "License");
690 * you may not use this file except in compliance with the License.
691 * You may obtain a copy of the License at
692 *
693 * http://www.apache.org/licenses/LICENSE-2.0
694 *
695 * Unless required by applicable law or agreed to in writing, software
696 * distributed under the License is distributed on an "AS IS" BASIS,
697 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
698 * See the License for the specific language governing permissions and
699 * limitations under the License.
700 */
701/**
702 * Abstraction around AppCheck's token fetching capabilities.
703 */
704class AppCheckTokenProvider {
705 constructor(appName_, appCheckProvider) {
706 this.appName_ = appName_;
707 this.appCheckProvider = appCheckProvider;
708 this.appCheck = appCheckProvider === null || appCheckProvider === void 0 ? void 0 : appCheckProvider.getImmediate({ optional: true });
709 if (!this.appCheck) {
710 appCheckProvider === null || appCheckProvider === void 0 ? void 0 : appCheckProvider.get().then(appCheck => (this.appCheck = appCheck));
711 }
712 }
713 getToken(forceRefresh) {
714 if (!this.appCheck) {
715 return new Promise((resolve, reject) => {
716 // Support delayed initialization of FirebaseAppCheck. This allows our
717 // customers to initialize the RTDB SDK before initializing Firebase
718 // AppCheck and ensures that all requests are authenticated if a token
719 // becomes available before the timoeout below expires.
720 setTimeout(() => {
721 if (this.appCheck) {
722 this.getToken(forceRefresh).then(resolve, reject);
723 }
724 else {
725 resolve(null);
726 }
727 }, 0);
728 });
729 }
730 return this.appCheck.getToken(forceRefresh);
731 }
732 addTokenChangeListener(listener) {
733 var _a;
734 (_a = this.appCheckProvider) === null || _a === void 0 ? void 0 : _a.get().then(appCheck => appCheck.addTokenListener(listener));
735 }
736 notifyForInvalidToken() {
737 warn(`Provided AppCheck credentials for the app named "${this.appName_}" ` +
738 'are invalid. This usually indicates your app was not initialized correctly.');
739 }
740}
741
742/**
743 * @license
744 * Copyright 2017 Google LLC
745 *
746 * Licensed under the Apache License, Version 2.0 (the "License");
747 * you may not use this file except in compliance with the License.
748 * You may obtain a copy of the License at
749 *
750 * http://www.apache.org/licenses/LICENSE-2.0
751 *
752 * Unless required by applicable law or agreed to in writing, software
753 * distributed under the License is distributed on an "AS IS" BASIS,
754 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
755 * See the License for the specific language governing permissions and
756 * limitations under the License.
757 */
758/**
759 * Abstraction around FirebaseApp's token fetching capabilities.
760 */
761class FirebaseAuthTokenProvider {
762 constructor(appName_, firebaseOptions_, authProvider_) {
763 this.appName_ = appName_;
764 this.firebaseOptions_ = firebaseOptions_;
765 this.authProvider_ = authProvider_;
766 this.auth_ = null;
767 this.auth_ = authProvider_.getImmediate({ optional: true });
768 if (!this.auth_) {
769 authProvider_.onInit(auth => (this.auth_ = auth));
770 }
771 }
772 getToken(forceRefresh) {
773 if (!this.auth_) {
774 return new Promise((resolve, reject) => {
775 // Support delayed initialization of FirebaseAuth. This allows our
776 // customers to initialize the RTDB SDK before initializing Firebase
777 // Auth and ensures that all requests are authenticated if a token
778 // becomes available before the timoeout below expires.
779 setTimeout(() => {
780 if (this.auth_) {
781 this.getToken(forceRefresh).then(resolve, reject);
782 }
783 else {
784 resolve(null);
785 }
786 }, 0);
787 });
788 }
789 return this.auth_.getToken(forceRefresh).catch(error => {
790 // TODO: Need to figure out all the cases this is raised and whether
791 // this makes sense.
792 if (error && error.code === 'auth/token-not-initialized') {
793 log('Got auth/token-not-initialized error. Treating as null token.');
794 return null;
795 }
796 else {
797 return Promise.reject(error);
798 }
799 });
800 }
801 addTokenChangeListener(listener) {
802 // TODO: We might want to wrap the listener and call it with no args to
803 // avoid a leaky abstraction, but that makes removing the listener harder.
804 if (this.auth_) {
805 this.auth_.addAuthTokenListener(listener);
806 }
807 else {
808 this.authProvider_
809 .get()
810 .then(auth => auth.addAuthTokenListener(listener));
811 }
812 }
813 removeTokenChangeListener(listener) {
814 this.authProvider_
815 .get()
816 .then(auth => auth.removeAuthTokenListener(listener));
817 }
818 notifyForInvalidToken() {
819 let errorMessage = 'Provided authentication credentials for the app named "' +
820 this.appName_ +
821 '" are invalid. This usually indicates your app was not ' +
822 'initialized correctly. ';
823 if ('credential' in this.firebaseOptions_) {
824 errorMessage +=
825 'Make sure the "credential" property provided to initializeApp() ' +
826 'is authorized to access the specified "databaseURL" and is from the correct ' +
827 'project.';
828 }
829 else if ('serviceAccount' in this.firebaseOptions_) {
830 errorMessage +=
831 'Make sure the "serviceAccount" property provided to initializeApp() ' +
832 'is authorized to access the specified "databaseURL" and is from the correct ' +
833 'project.';
834 }
835 else {
836 errorMessage +=
837 'Make sure the "apiKey" and "databaseURL" properties provided to ' +
838 'initializeApp() match the values provided for your app at ' +
839 'https://console.firebase.google.com/.';
840 }
841 warn(errorMessage);
842 }
843}
844/* AuthTokenProvider that supplies a constant token. Used by Admin SDK or mockUserToken with emulators. */
845class EmulatorTokenProvider {
846 constructor(accessToken) {
847 this.accessToken = accessToken;
848 }
849 getToken(forceRefresh) {
850 return Promise.resolve({
851 accessToken: this.accessToken
852 });
853 }
854 addTokenChangeListener(listener) {
855 // Invoke the listener immediately to match the behavior in Firebase Auth
856 // (see packages/auth/src/auth.js#L1807)
857 listener(this.accessToken);
858 }
859 removeTokenChangeListener(listener) { }
860 notifyForInvalidToken() { }
861}
862/** A string that is treated as an admin access token by the RTDB emulator. Used by Admin SDK. */
863EmulatorTokenProvider.OWNER = 'owner';
864
865/**
866 * @license
867 * Copyright 2017 Google LLC
868 *
869 * Licensed under the Apache License, Version 2.0 (the "License");
870 * you may not use this file except in compliance with the License.
871 * You may obtain a copy of the License at
872 *
873 * http://www.apache.org/licenses/LICENSE-2.0
874 *
875 * Unless required by applicable law or agreed to in writing, software
876 * distributed under the License is distributed on an "AS IS" BASIS,
877 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
878 * See the License for the specific language governing permissions and
879 * limitations under the License.
880 */
881const PROTOCOL_VERSION = '5';
882const VERSION_PARAM = 'v';
883const TRANSPORT_SESSION_PARAM = 's';
884const REFERER_PARAM = 'r';
885const FORGE_REF = 'f';
886// Matches console.firebase.google.com, firebase-console-*.corp.google.com and
887// firebase.corp.google.com
888const FORGE_DOMAIN_RE = /(console\.firebase|firebase-console-\w+\.corp|firebase\.corp)\.google\.com/;
889const LAST_SESSION_PARAM = 'ls';
890const APPLICATION_ID_PARAM = 'p';
891const APP_CHECK_TOKEN_PARAM = 'ac';
892const WEBSOCKET = 'websocket';
893const LONG_POLLING = 'long_polling';
894
895/**
896 * @license
897 * Copyright 2017 Google LLC
898 *
899 * Licensed under the Apache License, Version 2.0 (the "License");
900 * you may not use this file except in compliance with the License.
901 * You may obtain a copy of the License at
902 *
903 * http://www.apache.org/licenses/LICENSE-2.0
904 *
905 * Unless required by applicable law or agreed to in writing, software
906 * distributed under the License is distributed on an "AS IS" BASIS,
907 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
908 * See the License for the specific language governing permissions and
909 * limitations under the License.
910 */
911/**
912 * A class that holds metadata about a Repo object
913 */
914class RepoInfo {
915 /**
916 * @param host - Hostname portion of the url for the repo
917 * @param secure - Whether or not this repo is accessed over ssl
918 * @param namespace - The namespace represented by the repo
919 * @param webSocketOnly - Whether to prefer websockets over all other transports (used by Nest).
920 * @param nodeAdmin - Whether this instance uses Admin SDK credentials
921 * @param persistenceKey - Override the default session persistence storage key
922 */
923 constructor(host, secure, namespace, webSocketOnly, nodeAdmin = false, persistenceKey = '', includeNamespaceInQueryParams = false) {
924 this.secure = secure;
925 this.namespace = namespace;
926 this.webSocketOnly = webSocketOnly;
927 this.nodeAdmin = nodeAdmin;
928 this.persistenceKey = persistenceKey;
929 this.includeNamespaceInQueryParams = includeNamespaceInQueryParams;
930 this._host = host.toLowerCase();
931 this._domain = this._host.substr(this._host.indexOf('.') + 1);
932 this.internalHost =
933 PersistentStorage.get('host:' + host) || this._host;
934 }
935 isCacheableHost() {
936 return this.internalHost.substr(0, 2) === 's-';
937 }
938 isCustomHost() {
939 return (this._domain !== 'firebaseio.com' &&
940 this._domain !== 'firebaseio-demo.com');
941 }
942 get host() {
943 return this._host;
944 }
945 set host(newHost) {
946 if (newHost !== this.internalHost) {
947 this.internalHost = newHost;
948 if (this.isCacheableHost()) {
949 PersistentStorage.set('host:' + this._host, this.internalHost);
950 }
951 }
952 }
953 toString() {
954 let str = this.toURLString();
955 if (this.persistenceKey) {
956 str += '<' + this.persistenceKey + '>';
957 }
958 return str;
959 }
960 toURLString() {
961 const protocol = this.secure ? 'https://' : 'http://';
962 const query = this.includeNamespaceInQueryParams
963 ? `?ns=${this.namespace}`
964 : '';
965 return `${protocol}${this.host}/${query}`;
966 }
967}
968function repoInfoNeedsQueryParam(repoInfo) {
969 return (repoInfo.host !== repoInfo.internalHost ||
970 repoInfo.isCustomHost() ||
971 repoInfo.includeNamespaceInQueryParams);
972}
973/**
974 * Returns the websocket URL for this repo
975 * @param repoInfo - RepoInfo object
976 * @param type - of connection
977 * @param params - list
978 * @returns The URL for this repo
979 */
980function repoInfoConnectionURL(repoInfo, type, params) {
981 assert(typeof type === 'string', 'typeof type must == string');
982 assert(typeof params === 'object', 'typeof params must == object');
983 let connURL;
984 if (type === WEBSOCKET) {
985 connURL =
986 (repoInfo.secure ? 'wss://' : 'ws://') + repoInfo.internalHost + '/.ws?';
987 }
988 else if (type === LONG_POLLING) {
989 connURL =
990 (repoInfo.secure ? 'https://' : 'http://') +
991 repoInfo.internalHost +
992 '/.lp?';
993 }
994 else {
995 throw new Error('Unknown connection type: ' + type);
996 }
997 if (repoInfoNeedsQueryParam(repoInfo)) {
998 params['ns'] = repoInfo.namespace;
999 }
1000 const pairs = [];
1001 each(params, (key, value) => {
1002 pairs.push(key + '=' + value);
1003 });
1004 return connURL + pairs.join('&');
1005}
1006
1007/**
1008 * @license
1009 * Copyright 2017 Google LLC
1010 *
1011 * Licensed under the Apache License, Version 2.0 (the "License");
1012 * you may not use this file except in compliance with the License.
1013 * You may obtain a copy of the License at
1014 *
1015 * http://www.apache.org/licenses/LICENSE-2.0
1016 *
1017 * Unless required by applicable law or agreed to in writing, software
1018 * distributed under the License is distributed on an "AS IS" BASIS,
1019 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1020 * See the License for the specific language governing permissions and
1021 * limitations under the License.
1022 */
1023/**
1024 * Tracks a collection of stats.
1025 */
1026class StatsCollection {
1027 constructor() {
1028 this.counters_ = {};
1029 }
1030 incrementCounter(name, amount = 1) {
1031 if (!contains(this.counters_, name)) {
1032 this.counters_[name] = 0;
1033 }
1034 this.counters_[name] += amount;
1035 }
1036 get() {
1037 return deepCopy(this.counters_);
1038 }
1039}
1040
1041/**
1042 * @license
1043 * Copyright 2017 Google LLC
1044 *
1045 * Licensed under the Apache License, Version 2.0 (the "License");
1046 * you may not use this file except in compliance with the License.
1047 * You may obtain a copy of the License at
1048 *
1049 * http://www.apache.org/licenses/LICENSE-2.0
1050 *
1051 * Unless required by applicable law or agreed to in writing, software
1052 * distributed under the License is distributed on an "AS IS" BASIS,
1053 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1054 * See the License for the specific language governing permissions and
1055 * limitations under the License.
1056 */
1057const collections = {};
1058const reporters = {};
1059function statsManagerGetCollection(repoInfo) {
1060 const hashString = repoInfo.toString();
1061 if (!collections[hashString]) {
1062 collections[hashString] = new StatsCollection();
1063 }
1064 return collections[hashString];
1065}
1066function statsManagerGetOrCreateReporter(repoInfo, creatorFunction) {
1067 const hashString = repoInfo.toString();
1068 if (!reporters[hashString]) {
1069 reporters[hashString] = creatorFunction();
1070 }
1071 return reporters[hashString];
1072}
1073
1074/**
1075 * @license
1076 * Copyright 2017 Google LLC
1077 *
1078 * Licensed under the Apache License, Version 2.0 (the "License");
1079 * you may not use this file except in compliance with the License.
1080 * You may obtain a copy of the License at
1081 *
1082 * http://www.apache.org/licenses/LICENSE-2.0
1083 *
1084 * Unless required by applicable law or agreed to in writing, software
1085 * distributed under the License is distributed on an "AS IS" BASIS,
1086 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1087 * See the License for the specific language governing permissions and
1088 * limitations under the License.
1089 */
1090/**
1091 * This class ensures the packets from the server arrive in order
1092 * This class takes data from the server and ensures it gets passed into the callbacks in order.
1093 */
1094class PacketReceiver {
1095 /**
1096 * @param onMessage_
1097 */
1098 constructor(onMessage_) {
1099 this.onMessage_ = onMessage_;
1100 this.pendingResponses = [];
1101 this.currentResponseNum = 0;
1102 this.closeAfterResponse = -1;
1103 this.onClose = null;
1104 }
1105 closeAfter(responseNum, callback) {
1106 this.closeAfterResponse = responseNum;
1107 this.onClose = callback;
1108 if (this.closeAfterResponse < this.currentResponseNum) {
1109 this.onClose();
1110 this.onClose = null;
1111 }
1112 }
1113 /**
1114 * Each message from the server comes with a response number, and an array of data. The responseNumber
1115 * allows us to ensure that we process them in the right order, since we can't be guaranteed that all
1116 * browsers will respond in the same order as the requests we sent
1117 */
1118 handleResponse(requestNum, data) {
1119 this.pendingResponses[requestNum] = data;
1120 while (this.pendingResponses[this.currentResponseNum]) {
1121 const toProcess = this.pendingResponses[this.currentResponseNum];
1122 delete this.pendingResponses[this.currentResponseNum];
1123 for (let i = 0; i < toProcess.length; ++i) {
1124 if (toProcess[i]) {
1125 exceptionGuard(() => {
1126 this.onMessage_(toProcess[i]);
1127 });
1128 }
1129 }
1130 if (this.currentResponseNum === this.closeAfterResponse) {
1131 if (this.onClose) {
1132 this.onClose();
1133 this.onClose = null;
1134 }
1135 break;
1136 }
1137 this.currentResponseNum++;
1138 }
1139 }
1140}
1141
1142/**
1143 * @license
1144 * Copyright 2017 Google LLC
1145 *
1146 * Licensed under the Apache License, Version 2.0 (the "License");
1147 * you may not use this file except in compliance with the License.
1148 * You may obtain a copy of the License at
1149 *
1150 * http://www.apache.org/licenses/LICENSE-2.0
1151 *
1152 * Unless required by applicable law or agreed to in writing, software
1153 * distributed under the License is distributed on an "AS IS" BASIS,
1154 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1155 * See the License for the specific language governing permissions and
1156 * limitations under the License.
1157 */
1158// URL query parameters associated with longpolling
1159const FIREBASE_LONGPOLL_START_PARAM = 'start';
1160const FIREBASE_LONGPOLL_CLOSE_COMMAND = 'close';
1161const FIREBASE_LONGPOLL_COMMAND_CB_NAME = 'pLPCommand';
1162const FIREBASE_LONGPOLL_DATA_CB_NAME = 'pRTLPCB';
1163const FIREBASE_LONGPOLL_ID_PARAM = 'id';
1164const FIREBASE_LONGPOLL_PW_PARAM = 'pw';
1165const FIREBASE_LONGPOLL_SERIAL_PARAM = 'ser';
1166const FIREBASE_LONGPOLL_CALLBACK_ID_PARAM = 'cb';
1167const FIREBASE_LONGPOLL_SEGMENT_NUM_PARAM = 'seg';
1168const FIREBASE_LONGPOLL_SEGMENTS_IN_PACKET = 'ts';
1169const FIREBASE_LONGPOLL_DATA_PARAM = 'd';
1170const FIREBASE_LONGPOLL_DISCONN_FRAME_REQUEST_PARAM = 'dframe';
1171//Data size constants.
1172//TODO: Perf: the maximum length actually differs from browser to browser.
1173// We should check what browser we're on and set accordingly.
1174const MAX_URL_DATA_SIZE = 1870;
1175const SEG_HEADER_SIZE = 30; //ie: &seg=8299234&ts=982389123&d=
1176const MAX_PAYLOAD_SIZE = MAX_URL_DATA_SIZE - SEG_HEADER_SIZE;
1177/**
1178 * Keepalive period
1179 * send a fresh request at minimum every 25 seconds. Opera has a maximum request
1180 * length of 30 seconds that we can't exceed.
1181 */
1182const KEEPALIVE_REQUEST_INTERVAL = 25000;
1183/**
1184 * How long to wait before aborting a long-polling connection attempt.
1185 */
1186const LP_CONNECT_TIMEOUT = 30000;
1187/**
1188 * This class manages a single long-polling connection.
1189 */
1190class BrowserPollConnection {
1191 /**
1192 * @param connId An identifier for this connection, used for logging
1193 * @param repoInfo The info for the endpoint to send data to.
1194 * @param applicationId The Firebase App ID for this project.
1195 * @param appCheckToken The AppCheck token for this client.
1196 * @param authToken The AuthToken to use for this connection.
1197 * @param transportSessionId Optional transportSessionid if we are
1198 * reconnecting for an existing transport session
1199 * @param lastSessionId Optional lastSessionId if the PersistentConnection has
1200 * already created a connection previously
1201 */
1202 constructor(connId, repoInfo, applicationId, appCheckToken, authToken, transportSessionId, lastSessionId) {
1203 this.connId = connId;
1204 this.repoInfo = repoInfo;
1205 this.applicationId = applicationId;
1206 this.appCheckToken = appCheckToken;
1207 this.authToken = authToken;
1208 this.transportSessionId = transportSessionId;
1209 this.lastSessionId = lastSessionId;
1210 this.bytesSent = 0;
1211 this.bytesReceived = 0;
1212 this.everConnected_ = false;
1213 this.log_ = logWrapper(connId);
1214 this.stats_ = statsManagerGetCollection(repoInfo);
1215 this.urlFn = (params) => {
1216 // Always add the token if we have one.
1217 if (this.appCheckToken) {
1218 params[APP_CHECK_TOKEN_PARAM] = this.appCheckToken;
1219 }
1220 return repoInfoConnectionURL(repoInfo, LONG_POLLING, params);
1221 };
1222 }
1223 /**
1224 * @param onMessage - Callback when messages arrive
1225 * @param onDisconnect - Callback with connection lost.
1226 */
1227 open(onMessage, onDisconnect) {
1228 this.curSegmentNum = 0;
1229 this.onDisconnect_ = onDisconnect;
1230 this.myPacketOrderer = new PacketReceiver(onMessage);
1231 this.isClosed_ = false;
1232 this.connectTimeoutTimer_ = setTimeout(() => {
1233 this.log_('Timed out trying to connect.');
1234 // Make sure we clear the host cache
1235 this.onClosed_();
1236 this.connectTimeoutTimer_ = null;
1237 // eslint-disable-next-line @typescript-eslint/no-explicit-any
1238 }, Math.floor(LP_CONNECT_TIMEOUT));
1239 // Ensure we delay the creation of the iframe until the DOM is loaded.
1240 executeWhenDOMReady(() => {
1241 if (this.isClosed_) {
1242 return;
1243 }
1244 //Set up a callback that gets triggered once a connection is set up.
1245 this.scriptTagHolder = new FirebaseIFrameScriptHolder((...args) => {
1246 const [command, arg1, arg2, arg3, arg4] = args;
1247 this.incrementIncomingBytes_(args);
1248 if (!this.scriptTagHolder) {
1249 return; // we closed the connection.
1250 }
1251 if (this.connectTimeoutTimer_) {
1252 clearTimeout(this.connectTimeoutTimer_);
1253 this.connectTimeoutTimer_ = null;
1254 }
1255 this.everConnected_ = true;
1256 if (command === FIREBASE_LONGPOLL_START_PARAM) {
1257 this.id = arg1;
1258 this.password = arg2;
1259 }
1260 else if (command === FIREBASE_LONGPOLL_CLOSE_COMMAND) {
1261 // Don't clear the host cache. We got a response from the server, so we know it's reachable
1262 if (arg1) {
1263 // We aren't expecting any more data (other than what the server's already in the process of sending us
1264 // through our already open polls), so don't send any more.
1265 this.scriptTagHolder.sendNewPolls = false;
1266 // arg1 in this case is the last response number sent by the server. We should try to receive
1267 // all of the responses up to this one before closing
1268 this.myPacketOrderer.closeAfter(arg1, () => {
1269 this.onClosed_();
1270 });
1271 }
1272 else {
1273 this.onClosed_();
1274 }
1275 }
1276 else {
1277 throw new Error('Unrecognized command received: ' + command);
1278 }
1279 }, (...args) => {
1280 const [pN, data] = args;
1281 this.incrementIncomingBytes_(args);
1282 this.myPacketOrderer.handleResponse(pN, data);
1283 }, () => {
1284 this.onClosed_();
1285 }, this.urlFn);
1286 //Send the initial request to connect. The serial number is simply to keep the browser from pulling previous results
1287 //from cache.
1288 const urlParams = {};
1289 urlParams[FIREBASE_LONGPOLL_START_PARAM] = 't';
1290 urlParams[FIREBASE_LONGPOLL_SERIAL_PARAM] = Math.floor(Math.random() * 100000000);
1291 if (this.scriptTagHolder.uniqueCallbackIdentifier) {
1292 urlParams[FIREBASE_LONGPOLL_CALLBACK_ID_PARAM] =
1293 this.scriptTagHolder.uniqueCallbackIdentifier;
1294 }
1295 urlParams[VERSION_PARAM] = PROTOCOL_VERSION;
1296 if (this.transportSessionId) {
1297 urlParams[TRANSPORT_SESSION_PARAM] = this.transportSessionId;
1298 }
1299 if (this.lastSessionId) {
1300 urlParams[LAST_SESSION_PARAM] = this.lastSessionId;
1301 }
1302 if (this.applicationId) {
1303 urlParams[APPLICATION_ID_PARAM] = this.applicationId;
1304 }
1305 if (this.appCheckToken) {
1306 urlParams[APP_CHECK_TOKEN_PARAM] = this.appCheckToken;
1307 }
1308 if (typeof location !== 'undefined' &&
1309 location.hostname &&
1310 FORGE_DOMAIN_RE.test(location.hostname)) {
1311 urlParams[REFERER_PARAM] = FORGE_REF;
1312 }
1313 const connectURL = this.urlFn(urlParams);
1314 this.log_('Connecting via long-poll to ' + connectURL);
1315 this.scriptTagHolder.addTag(connectURL, () => {
1316 /* do nothing */
1317 });
1318 });
1319 }
1320 /**
1321 * Call this when a handshake has completed successfully and we want to consider the connection established
1322 */
1323 start() {
1324 this.scriptTagHolder.startLongPoll(this.id, this.password);
1325 this.addDisconnectPingFrame(this.id, this.password);
1326 }
1327 /**
1328 * Forces long polling to be considered as a potential transport
1329 */
1330 static forceAllow() {
1331 BrowserPollConnection.forceAllow_ = true;
1332 }
1333 /**
1334 * Forces longpolling to not be considered as a potential transport
1335 */
1336 static forceDisallow() {
1337 BrowserPollConnection.forceDisallow_ = true;
1338 }
1339 // Static method, use string literal so it can be accessed in a generic way
1340 static isAvailable() {
1341 if (isNodeSdk()) {
1342 return false;
1343 }
1344 else if (BrowserPollConnection.forceAllow_) {
1345 return true;
1346 }
1347 else {
1348 // NOTE: In React-Native there's normally no 'document', but if you debug a React-Native app in
1349 // the Chrome debugger, 'document' is defined, but document.createElement is null (2015/06/08).
1350 return (!BrowserPollConnection.forceDisallow_ &&
1351 typeof document !== 'undefined' &&
1352 document.createElement != null &&
1353 !isChromeExtensionContentScript() &&
1354 !isWindowsStoreApp());
1355 }
1356 }
1357 /**
1358 * No-op for polling
1359 */
1360 markConnectionHealthy() { }
1361 /**
1362 * Stops polling and cleans up the iframe
1363 */
1364 shutdown_() {
1365 this.isClosed_ = true;
1366 if (this.scriptTagHolder) {
1367 this.scriptTagHolder.close();
1368 this.scriptTagHolder = null;
1369 }
1370 //remove the disconnect frame, which will trigger an XHR call to the server to tell it we're leaving.
1371 if (this.myDisconnFrame) {
1372 document.body.removeChild(this.myDisconnFrame);
1373 this.myDisconnFrame = null;
1374 }
1375 if (this.connectTimeoutTimer_) {
1376 clearTimeout(this.connectTimeoutTimer_);
1377 this.connectTimeoutTimer_ = null;
1378 }
1379 }
1380 /**
1381 * Triggered when this transport is closed
1382 */
1383 onClosed_() {
1384 if (!this.isClosed_) {
1385 this.log_('Longpoll is closing itself');
1386 this.shutdown_();
1387 if (this.onDisconnect_) {
1388 this.onDisconnect_(this.everConnected_);
1389 this.onDisconnect_ = null;
1390 }
1391 }
1392 }
1393 /**
1394 * External-facing close handler. RealTime has requested we shut down. Kill our connection and tell the server
1395 * that we've left.
1396 */
1397 close() {
1398 if (!this.isClosed_) {
1399 this.log_('Longpoll is being closed.');
1400 this.shutdown_();
1401 }
1402 }
1403 /**
1404 * Send the JSON object down to the server. It will need to be stringified, base64 encoded, and then
1405 * broken into chunks (since URLs have a small maximum length).
1406 * @param data - The JSON data to transmit.
1407 */
1408 send(data) {
1409 const dataStr = stringify(data);
1410 this.bytesSent += dataStr.length;
1411 this.stats_.incrementCounter('bytes_sent', dataStr.length);
1412 //first, lets get the base64-encoded data
1413 const base64data = base64Encode(dataStr);
1414 //We can only fit a certain amount in each URL, so we need to split this request
1415 //up into multiple pieces if it doesn't fit in one request.
1416 const dataSegs = splitStringBySize(base64data, MAX_PAYLOAD_SIZE);
1417 //Enqueue each segment for transmission. We assign each chunk a sequential ID and a total number
1418 //of segments so that we can reassemble the packet on the server.
1419 for (let i = 0; i < dataSegs.length; i++) {
1420 this.scriptTagHolder.enqueueSegment(this.curSegmentNum, dataSegs.length, dataSegs[i]);
1421 this.curSegmentNum++;
1422 }
1423 }
1424 /**
1425 * This is how we notify the server that we're leaving.
1426 * We aren't able to send requests with DHTML on a window close event, but we can
1427 * trigger XHR requests in some browsers (everything but Opera basically).
1428 */
1429 addDisconnectPingFrame(id, pw) {
1430 if (isNodeSdk()) {
1431 return;
1432 }
1433 this.myDisconnFrame = document.createElement('iframe');
1434 const urlParams = {};
1435 urlParams[FIREBASE_LONGPOLL_DISCONN_FRAME_REQUEST_PARAM] = 't';
1436 urlParams[FIREBASE_LONGPOLL_ID_PARAM] = id;
1437 urlParams[FIREBASE_LONGPOLL_PW_PARAM] = pw;
1438 this.myDisconnFrame.src = this.urlFn(urlParams);
1439 this.myDisconnFrame.style.display = 'none';
1440 document.body.appendChild(this.myDisconnFrame);
1441 }
1442 /**
1443 * Used to track the bytes received by this client
1444 */
1445 incrementIncomingBytes_(args) {
1446 // TODO: This is an annoying perf hit just to track the number of incoming bytes. Maybe it should be opt-in.
1447 const bytesReceived = stringify(args).length;
1448 this.bytesReceived += bytesReceived;
1449 this.stats_.incrementCounter('bytes_received', bytesReceived);
1450 }
1451}
1452/*********************************************************************************************
1453 * A wrapper around an iframe that is used as a long-polling script holder.
1454 *********************************************************************************************/
1455class FirebaseIFrameScriptHolder {
1456 /**
1457 * @param commandCB - The callback to be called when control commands are recevied from the server.
1458 * @param onMessageCB - The callback to be triggered when responses arrive from the server.
1459 * @param onDisconnect - The callback to be triggered when this tag holder is closed
1460 * @param urlFn - A function that provides the URL of the endpoint to send data to.
1461 */
1462 constructor(commandCB, onMessageCB, onDisconnect, urlFn) {
1463 this.onDisconnect = onDisconnect;
1464 this.urlFn = urlFn;
1465 //We maintain a count of all of the outstanding requests, because if we have too many active at once it can cause
1466 //problems in some browsers.
1467 this.outstandingRequests = new Set();
1468 //A queue of the pending segments waiting for transmission to the server.
1469 this.pendingSegs = [];
1470 //A serial number. We use this for two things:
1471 // 1) A way to ensure the browser doesn't cache responses to polls
1472 // 2) A way to make the server aware when long-polls arrive in a different order than we started them. The
1473 // server needs to release both polls in this case or it will cause problems in Opera since Opera can only execute
1474 // JSONP code in the order it was added to the iframe.
1475 this.currentSerial = Math.floor(Math.random() * 100000000);
1476 // This gets set to false when we're "closing down" the connection (e.g. we're switching transports but there's still
1477 // incoming data from the server that we're waiting for).
1478 this.sendNewPolls = true;
1479 if (!isNodeSdk()) {
1480 //Each script holder registers a couple of uniquely named callbacks with the window. These are called from the
1481 //iframes where we put the long-polling script tags. We have two callbacks:
1482 // 1) Command Callback - Triggered for control issues, like starting a connection.
1483 // 2) Message Callback - Triggered when new data arrives.
1484 this.uniqueCallbackIdentifier = LUIDGenerator();
1485 window[FIREBASE_LONGPOLL_COMMAND_CB_NAME + this.uniqueCallbackIdentifier] = commandCB;
1486 window[FIREBASE_LONGPOLL_DATA_CB_NAME + this.uniqueCallbackIdentifier] =
1487 onMessageCB;
1488 //Create an iframe for us to add script tags to.
1489 this.myIFrame = FirebaseIFrameScriptHolder.createIFrame_();
1490 // Set the iframe's contents.
1491 let script = '';
1492 // if we set a javascript url, it's IE and we need to set the document domain. The javascript url is sufficient
1493 // for ie9, but ie8 needs to do it again in the document itself.
1494 if (this.myIFrame.src &&
1495 this.myIFrame.src.substr(0, 'javascript:'.length) === 'javascript:') {
1496 const currentDomain = document.domain;
1497 script = '<script>document.domain="' + currentDomain + '";</script>';
1498 }
1499 const iframeContents = '<html><body>' + script + '</body></html>';
1500 try {
1501 this.myIFrame.doc.open();
1502 this.myIFrame.doc.write(iframeContents);
1503 this.myIFrame.doc.close();
1504 }
1505 catch (e) {
1506 log('frame writing exception');
1507 if (e.stack) {
1508 log(e.stack);
1509 }
1510 log(e);
1511 }
1512 }
1513 else {
1514 this.commandCB = commandCB;
1515 this.onMessageCB = onMessageCB;
1516 }
1517 }
1518 /**
1519 * Each browser has its own funny way to handle iframes. Here we mush them all together into one object that I can
1520 * actually use.
1521 */
1522 static createIFrame_() {
1523 const iframe = document.createElement('iframe');
1524 iframe.style.display = 'none';
1525 // This is necessary in order to initialize the document inside the iframe
1526 if (document.body) {
1527 document.body.appendChild(iframe);
1528 try {
1529 // If document.domain has been modified in IE, this will throw an error, and we need to set the
1530 // domain of the iframe's document manually. We can do this via a javascript: url as the src attribute
1531 // Also note that we must do this *after* the iframe has been appended to the page. Otherwise it doesn't work.
1532 const a = iframe.contentWindow.document;
1533 if (!a) {
1534 // Apologies for the log-spam, I need to do something to keep closure from optimizing out the assignment above.
1535 log('No IE domain setting required');
1536 }
1537 }
1538 catch (e) {
1539 const domain = document.domain;
1540 iframe.src =
1541 "javascript:void((function(){document.open();document.domain='" +
1542 domain +
1543 "';document.close();})())";
1544 }
1545 }
1546 else {
1547 // LongPollConnection attempts to delay initialization until the document is ready, so hopefully this
1548 // never gets hit.
1549 throw 'Document body has not initialized. Wait to initialize Firebase until after the document is ready.';
1550 }
1551 // Get the document of the iframe in a browser-specific way.
1552 if (iframe.contentDocument) {
1553 iframe.doc = iframe.contentDocument; // Firefox, Opera, Safari
1554 }
1555 else if (iframe.contentWindow) {
1556 iframe.doc = iframe.contentWindow.document; // Internet Explorer
1557 // eslint-disable-next-line @typescript-eslint/no-explicit-any
1558 }
1559 else if (iframe.document) {
1560 // eslint-disable-next-line @typescript-eslint/no-explicit-any
1561 iframe.doc = iframe.document; //others?
1562 }
1563 return iframe;
1564 }
1565 /**
1566 * Cancel all outstanding queries and remove the frame.
1567 */
1568 close() {
1569 //Mark this iframe as dead, so no new requests are sent.
1570 this.alive = false;
1571 if (this.myIFrame) {
1572 //We have to actually remove all of the html inside this iframe before removing it from the
1573 //window, or IE will continue loading and executing the script tags we've already added, which
1574 //can lead to some errors being thrown. Setting innerHTML seems to be the easiest way to do this.
1575 this.myIFrame.doc.body.innerHTML = '';
1576 setTimeout(() => {
1577 if (this.myIFrame !== null) {
1578 document.body.removeChild(this.myIFrame);
1579 this.myIFrame = null;
1580 }
1581 }, Math.floor(0));
1582 }
1583 // Protect from being called recursively.
1584 const onDisconnect = this.onDisconnect;
1585 if (onDisconnect) {
1586 this.onDisconnect = null;
1587 onDisconnect();
1588 }
1589 }
1590 /**
1591 * Actually start the long-polling session by adding the first script tag(s) to the iframe.
1592 * @param id - The ID of this connection
1593 * @param pw - The password for this connection
1594 */
1595 startLongPoll(id, pw) {
1596 this.myID = id;
1597 this.myPW = pw;
1598 this.alive = true;
1599 //send the initial request. If there are requests queued, make sure that we transmit as many as we are currently able to.
1600 while (this.newRequest_()) { }
1601 }
1602 /**
1603 * This is called any time someone might want a script tag to be added. It adds a script tag when there aren't
1604 * too many outstanding requests and we are still alive.
1605 *
1606 * If there are outstanding packet segments to send, it sends one. If there aren't, it sends a long-poll anyways if
1607 * needed.
1608 */
1609 newRequest_() {
1610 // We keep one outstanding request open all the time to receive data, but if we need to send data
1611 // (pendingSegs.length > 0) then we create a new request to send the data. The server will automatically
1612 // close the old request.
1613 if (this.alive &&
1614 this.sendNewPolls &&
1615 this.outstandingRequests.size < (this.pendingSegs.length > 0 ? 2 : 1)) {
1616 //construct our url
1617 this.currentSerial++;
1618 const urlParams = {};
1619 urlParams[FIREBASE_LONGPOLL_ID_PARAM] = this.myID;
1620 urlParams[FIREBASE_LONGPOLL_PW_PARAM] = this.myPW;
1621 urlParams[FIREBASE_LONGPOLL_SERIAL_PARAM] = this.currentSerial;
1622 let theURL = this.urlFn(urlParams);
1623 //Now add as much data as we can.
1624 let curDataString = '';
1625 let i = 0;
1626 while (this.pendingSegs.length > 0) {
1627 //first, lets see if the next segment will fit.
1628 const nextSeg = this.pendingSegs[0];
1629 if (nextSeg.d.length +
1630 SEG_HEADER_SIZE +
1631 curDataString.length <=
1632 MAX_URL_DATA_SIZE) {
1633 //great, the segment will fit. Lets append it.
1634 const theSeg = this.pendingSegs.shift();
1635 curDataString =
1636 curDataString +
1637 '&' +
1638 FIREBASE_LONGPOLL_SEGMENT_NUM_PARAM +
1639 i +
1640 '=' +
1641 theSeg.seg +
1642 '&' +
1643 FIREBASE_LONGPOLL_SEGMENTS_IN_PACKET +
1644 i +
1645 '=' +
1646 theSeg.ts +
1647 '&' +
1648 FIREBASE_LONGPOLL_DATA_PARAM +
1649 i +
1650 '=' +
1651 theSeg.d;
1652 i++;
1653 }
1654 else {
1655 break;
1656 }
1657 }
1658 theURL = theURL + curDataString;
1659 this.addLongPollTag_(theURL, this.currentSerial);
1660 return true;
1661 }
1662 else {
1663 return false;
1664 }
1665 }
1666 /**
1667 * Queue a packet for transmission to the server.
1668 * @param segnum - A sequential id for this packet segment used for reassembly
1669 * @param totalsegs - The total number of segments in this packet
1670 * @param data - The data for this segment.
1671 */
1672 enqueueSegment(segnum, totalsegs, data) {
1673 //add this to the queue of segments to send.
1674 this.pendingSegs.push({ seg: segnum, ts: totalsegs, d: data });
1675 //send the data immediately if there isn't already data being transmitted, unless
1676 //startLongPoll hasn't been called yet.
1677 if (this.alive) {
1678 this.newRequest_();
1679 }
1680 }
1681 /**
1682 * Add a script tag for a regular long-poll request.
1683 * @param url - The URL of the script tag.
1684 * @param serial - The serial number of the request.
1685 */
1686 addLongPollTag_(url, serial) {
1687 //remember that we sent this request.
1688 this.outstandingRequests.add(serial);
1689 const doNewRequest = () => {
1690 this.outstandingRequests.delete(serial);
1691 this.newRequest_();
1692 };
1693 // If this request doesn't return on its own accord (by the server sending us some data), we'll
1694 // create a new one after the KEEPALIVE interval to make sure we always keep a fresh request open.
1695 const keepaliveTimeout = setTimeout(doNewRequest, Math.floor(KEEPALIVE_REQUEST_INTERVAL));
1696 const readyStateCB = () => {
1697 // Request completed. Cancel the keepalive.
1698 clearTimeout(keepaliveTimeout);
1699 // Trigger a new request so we can continue receiving data.
1700 doNewRequest();
1701 };
1702 this.addTag(url, readyStateCB);
1703 }
1704 /**
1705 * Add an arbitrary script tag to the iframe.
1706 * @param url - The URL for the script tag source.
1707 * @param loadCB - A callback to be triggered once the script has loaded.
1708 */
1709 addTag(url, loadCB) {
1710 if (isNodeSdk()) {
1711 // eslint-disable-next-line @typescript-eslint/no-explicit-any
1712 this.doNodeLongPoll(url, loadCB);
1713 }
1714 else {
1715 setTimeout(() => {
1716 try {
1717 // if we're already closed, don't add this poll
1718 if (!this.sendNewPolls) {
1719 return;
1720 }
1721 const newScript = this.myIFrame.doc.createElement('script');
1722 newScript.type = 'text/javascript';
1723 newScript.async = true;
1724 newScript.src = url;
1725 // eslint-disable-next-line @typescript-eslint/no-explicit-any
1726 newScript.onload = newScript.onreadystatechange =
1727 function () {
1728 // eslint-disable-next-line @typescript-eslint/no-explicit-any
1729 const rstate = newScript.readyState;
1730 if (!rstate || rstate === 'loaded' || rstate === 'complete') {
1731 // eslint-disable-next-line @typescript-eslint/no-explicit-any
1732 newScript.onload = newScript.onreadystatechange = null;
1733 if (newScript.parentNode) {
1734 newScript.parentNode.removeChild(newScript);
1735 }
1736 loadCB();
1737 }
1738 };
1739 newScript.onerror = () => {
1740 log('Long-poll script failed to load: ' + url);
1741 this.sendNewPolls = false;
1742 this.close();
1743 };
1744 this.myIFrame.doc.body.appendChild(newScript);
1745 }
1746 catch (e) {
1747 // TODO: we should make this error visible somehow
1748 }
1749 }, Math.floor(1));
1750 }
1751 }
1752}
1753
1754/**
1755 * @license
1756 * Copyright 2017 Google LLC
1757 *
1758 * Licensed under the Apache License, Version 2.0 (the "License");
1759 * you may not use this file except in compliance with the License.
1760 * You may obtain a copy of the License at
1761 *
1762 * http://www.apache.org/licenses/LICENSE-2.0
1763 *
1764 * Unless required by applicable law or agreed to in writing, software
1765 * distributed under the License is distributed on an "AS IS" BASIS,
1766 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1767 * See the License for the specific language governing permissions and
1768 * limitations under the License.
1769 */
1770const WEBSOCKET_MAX_FRAME_SIZE = 16384;
1771const WEBSOCKET_KEEPALIVE_INTERVAL = 45000;
1772let WebSocketImpl = null;
1773if (typeof MozWebSocket !== 'undefined') {
1774 WebSocketImpl = MozWebSocket;
1775}
1776else if (typeof WebSocket !== 'undefined') {
1777 WebSocketImpl = WebSocket;
1778}
1779/**
1780 * Create a new websocket connection with the given callbacks.
1781 */
1782class WebSocketConnection {
1783 /**
1784 * @param connId identifier for this transport
1785 * @param repoInfo The info for the websocket endpoint.
1786 * @param applicationId The Firebase App ID for this project.
1787 * @param appCheckToken The App Check Token for this client.
1788 * @param authToken The Auth Token for this client.
1789 * @param transportSessionId Optional transportSessionId if this is connecting
1790 * to an existing transport session
1791 * @param lastSessionId Optional lastSessionId if there was a previous
1792 * connection
1793 */
1794 constructor(connId, repoInfo, applicationId, appCheckToken, authToken, transportSessionId, lastSessionId) {
1795 this.connId = connId;
1796 this.applicationId = applicationId;
1797 this.appCheckToken = appCheckToken;
1798 this.authToken = authToken;
1799 this.keepaliveTimer = null;
1800 this.frames = null;
1801 this.totalFrames = 0;
1802 this.bytesSent = 0;
1803 this.bytesReceived = 0;
1804 this.log_ = logWrapper(this.connId);
1805 this.stats_ = statsManagerGetCollection(repoInfo);
1806 this.connURL = WebSocketConnection.connectionURL_(repoInfo, transportSessionId, lastSessionId, appCheckToken, applicationId);
1807 this.nodeAdmin = repoInfo.nodeAdmin;
1808 }
1809 /**
1810 * @param repoInfo - The info for the websocket endpoint.
1811 * @param transportSessionId - Optional transportSessionId if this is connecting to an existing transport
1812 * session
1813 * @param lastSessionId - Optional lastSessionId if there was a previous connection
1814 * @returns connection url
1815 */
1816 static connectionURL_(repoInfo, transportSessionId, lastSessionId, appCheckToken, applicationId) {
1817 const urlParams = {};
1818 urlParams[VERSION_PARAM] = PROTOCOL_VERSION;
1819 if (!isNodeSdk() &&
1820 typeof location !== 'undefined' &&
1821 location.hostname &&
1822 FORGE_DOMAIN_RE.test(location.hostname)) {
1823 urlParams[REFERER_PARAM] = FORGE_REF;
1824 }
1825 if (transportSessionId) {
1826 urlParams[TRANSPORT_SESSION_PARAM] = transportSessionId;
1827 }
1828 if (lastSessionId) {
1829 urlParams[LAST_SESSION_PARAM] = lastSessionId;
1830 }
1831 if (appCheckToken) {
1832 urlParams[APP_CHECK_TOKEN_PARAM] = appCheckToken;
1833 }
1834 if (applicationId) {
1835 urlParams[APPLICATION_ID_PARAM] = applicationId;
1836 }
1837 return repoInfoConnectionURL(repoInfo, WEBSOCKET, urlParams);
1838 }
1839 /**
1840 * @param onMessage - Callback when messages arrive
1841 * @param onDisconnect - Callback with connection lost.
1842 */
1843 open(onMessage, onDisconnect) {
1844 this.onDisconnect = onDisconnect;
1845 this.onMessage = onMessage;
1846 this.log_('Websocket connecting to ' + this.connURL);
1847 this.everConnected_ = false;
1848 // Assume failure until proven otherwise.
1849 PersistentStorage.set('previous_websocket_failure', true);
1850 try {
1851 let options;
1852 if (isNodeSdk()) {
1853 const device = this.nodeAdmin ? 'AdminNode' : 'Node';
1854 // UA Format: Firebase/<wire_protocol>/<sdk_version>/<platform>/<device>
1855 options = {
1856 headers: {
1857 'User-Agent': `Firebase/${PROTOCOL_VERSION}/${SDK_VERSION}/${process.platform}/${device}`,
1858 'X-Firebase-GMPID': this.applicationId || ''
1859 }
1860 };
1861 // If using Node with admin creds, AppCheck-related checks are unnecessary.
1862 // Note that we send the credentials here even if they aren't admin credentials, which is
1863 // not a problem.
1864 // Note that this header is just used to bypass appcheck, and the token should still be sent
1865 // through the websocket connection once it is established.
1866 if (this.authToken) {
1867 options.headers['Authorization'] = `Bearer ${this.authToken}`;
1868 }
1869 if (this.appCheckToken) {
1870 options.headers['X-Firebase-AppCheck'] = this.appCheckToken;
1871 }
1872 // Plumb appropriate http_proxy environment variable into faye-websocket if it exists.
1873 const env = process['env'];
1874 const proxy = this.connURL.indexOf('wss://') === 0
1875 ? env['HTTPS_PROXY'] || env['https_proxy']
1876 : env['HTTP_PROXY'] || env['http_proxy'];
1877 if (proxy) {
1878 options['proxy'] = { origin: proxy };
1879 }
1880 }
1881 this.mySock = new WebSocketImpl(this.connURL, [], options);
1882 }
1883 catch (e) {
1884 this.log_('Error instantiating WebSocket.');
1885 const error = e.message || e.data;
1886 if (error) {
1887 this.log_(error);
1888 }
1889 this.onClosed_();
1890 return;
1891 }
1892 this.mySock.onopen = () => {
1893 this.log_('Websocket connected.');
1894 this.everConnected_ = true;
1895 };
1896 this.mySock.onclose = () => {
1897 this.log_('Websocket connection was disconnected.');
1898 this.mySock = null;
1899 this.onClosed_();
1900 };
1901 this.mySock.onmessage = m => {
1902 this.handleIncomingFrame(m);
1903 };
1904 this.mySock.onerror = e => {
1905 this.log_('WebSocket error. Closing connection.');
1906 // eslint-disable-next-line @typescript-eslint/no-explicit-any
1907 const error = e.message || e.data;
1908 if (error) {
1909 this.log_(error);
1910 }
1911 this.onClosed_();
1912 };
1913 }
1914 /**
1915 * No-op for websockets, we don't need to do anything once the connection is confirmed as open
1916 */
1917 start() { }
1918 static forceDisallow() {
1919 WebSocketConnection.forceDisallow_ = true;
1920 }
1921 static isAvailable() {
1922 let isOldAndroid = false;
1923 if (typeof navigator !== 'undefined' && navigator.userAgent) {
1924 const oldAndroidRegex = /Android ([0-9]{0,}\.[0-9]{0,})/;
1925 const oldAndroidMatch = navigator.userAgent.match(oldAndroidRegex);
1926 if (oldAndroidMatch && oldAndroidMatch.length > 1) {
1927 if (parseFloat(oldAndroidMatch[1]) < 4.4) {
1928 isOldAndroid = true;
1929 }
1930 }
1931 }
1932 return (!isOldAndroid &&
1933 WebSocketImpl !== null &&
1934 !WebSocketConnection.forceDisallow_);
1935 }
1936 /**
1937 * Returns true if we previously failed to connect with this transport.
1938 */
1939 static previouslyFailed() {
1940 // If our persistent storage is actually only in-memory storage,
1941 // we default to assuming that it previously failed to be safe.
1942 return (PersistentStorage.isInMemoryStorage ||
1943 PersistentStorage.get('previous_websocket_failure') === true);
1944 }
1945 markConnectionHealthy() {
1946 PersistentStorage.remove('previous_websocket_failure');
1947 }
1948 appendFrame_(data) {
1949 this.frames.push(data);
1950 if (this.frames.length === this.totalFrames) {
1951 const fullMess = this.frames.join('');
1952 this.frames = null;
1953 const jsonMess = jsonEval(fullMess);
1954 //handle the message
1955 this.onMessage(jsonMess);
1956 }
1957 }
1958 /**
1959 * @param frameCount - The number of frames we are expecting from the server
1960 */
1961 handleNewFrameCount_(frameCount) {
1962 this.totalFrames = frameCount;
1963 this.frames = [];
1964 }
1965 /**
1966 * Attempts to parse a frame count out of some text. If it can't, assumes a value of 1
1967 * @returns Any remaining data to be process, or null if there is none
1968 */
1969 extractFrameCount_(data) {
1970 assert(this.frames === null, 'We already have a frame buffer');
1971 // TODO: The server is only supposed to send up to 9999 frames (i.e. length <= 4), but that isn't being enforced
1972 // currently. So allowing larger frame counts (length <= 6). See https://app.asana.com/0/search/8688598998380/8237608042508
1973 if (data.length <= 6) {
1974 const frameCount = Number(data);
1975 if (!isNaN(frameCount)) {
1976 this.handleNewFrameCount_(frameCount);
1977 return null;
1978 }
1979 }
1980 this.handleNewFrameCount_(1);
1981 return data;
1982 }
1983 /**
1984 * Process a websocket frame that has arrived from the server.
1985 * @param mess - The frame data
1986 */
1987 handleIncomingFrame(mess) {
1988 if (this.mySock === null) {
1989 return; // Chrome apparently delivers incoming packets even after we .close() the connection sometimes.
1990 }
1991 const data = mess['data'];
1992 this.bytesReceived += data.length;
1993 this.stats_.incrementCounter('bytes_received', data.length);
1994 this.resetKeepAlive();
1995 if (this.frames !== null) {
1996 // we're buffering
1997 this.appendFrame_(data);
1998 }
1999 else {
2000 // try to parse out a frame count, otherwise, assume 1 and process it
2001 const remainingData = this.extractFrameCount_(data);
2002 if (remainingData !== null) {
2003 this.appendFrame_(remainingData);
2004 }
2005 }
2006 }
2007 /**
2008 * Send a message to the server
2009 * @param data - The JSON object to transmit
2010 */
2011 send(data) {
2012 this.resetKeepAlive();
2013 const dataStr = stringify(data);
2014 this.bytesSent += dataStr.length;
2015 this.stats_.incrementCounter('bytes_sent', dataStr.length);
2016 //We can only fit a certain amount in each websocket frame, so we need to split this request
2017 //up into multiple pieces if it doesn't fit in one request.
2018 const dataSegs = splitStringBySize(dataStr, WEBSOCKET_MAX_FRAME_SIZE);
2019 //Send the length header
2020 if (dataSegs.length > 1) {
2021 this.sendString_(String(dataSegs.length));
2022 }
2023 //Send the actual data in segments.
2024 for (let i = 0; i < dataSegs.length; i++) {
2025 this.sendString_(dataSegs[i]);
2026 }
2027 }
2028 shutdown_() {
2029 this.isClosed_ = true;
2030 if (this.keepaliveTimer) {
2031 clearInterval(this.keepaliveTimer);
2032 this.keepaliveTimer = null;
2033 }
2034 if (this.mySock) {
2035 this.mySock.close();
2036 this.mySock = null;
2037 }
2038 }
2039 onClosed_() {
2040 if (!this.isClosed_) {
2041 this.log_('WebSocket is closing itself');
2042 this.shutdown_();
2043 // since this is an internal close, trigger the close listener
2044 if (this.onDisconnect) {
2045 this.onDisconnect(this.everConnected_);
2046 this.onDisconnect = null;
2047 }
2048 }
2049 }
2050 /**
2051 * External-facing close handler.
2052 * Close the websocket and kill the connection.
2053 */
2054 close() {
2055 if (!this.isClosed_) {
2056 this.log_('WebSocket is being closed');
2057 this.shutdown_();
2058 }
2059 }
2060 /**
2061 * Kill the current keepalive timer and start a new one, to ensure that it always fires N seconds after
2062 * the last activity.
2063 */
2064 resetKeepAlive() {
2065 clearInterval(this.keepaliveTimer);
2066 this.keepaliveTimer = setInterval(() => {
2067 //If there has been no websocket activity for a while, send a no-op
2068 if (this.mySock) {
2069 this.sendString_('0');
2070 }
2071 this.resetKeepAlive();
2072 // eslint-disable-next-line @typescript-eslint/no-explicit-any
2073 }, Math.floor(WEBSOCKET_KEEPALIVE_INTERVAL));
2074 }
2075 /**
2076 * Send a string over the websocket.
2077 *
2078 * @param str - String to send.
2079 */
2080 sendString_(str) {
2081 // Firefox seems to sometimes throw exceptions (NS_ERROR_UNEXPECTED) from websocket .send()
2082 // calls for some unknown reason. We treat these as an error and disconnect.
2083 // See https://app.asana.com/0/58926111402292/68021340250410
2084 try {
2085 this.mySock.send(str);
2086 }
2087 catch (e) {
2088 this.log_('Exception thrown from WebSocket.send():', e.message || e.data, 'Closing connection.');
2089 setTimeout(this.onClosed_.bind(this), 0);
2090 }
2091 }
2092}
2093/**
2094 * Number of response before we consider the connection "healthy."
2095 */
2096WebSocketConnection.responsesRequiredToBeHealthy = 2;
2097/**
2098 * Time to wait for the connection te become healthy before giving up.
2099 */
2100WebSocketConnection.healthyTimeout = 30000;
2101
2102/**
2103 * @license
2104 * Copyright 2017 Google LLC
2105 *
2106 * Licensed under the Apache License, Version 2.0 (the "License");
2107 * you may not use this file except in compliance with the License.
2108 * You may obtain a copy of the License at
2109 *
2110 * http://www.apache.org/licenses/LICENSE-2.0
2111 *
2112 * Unless required by applicable law or agreed to in writing, software
2113 * distributed under the License is distributed on an "AS IS" BASIS,
2114 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2115 * See the License for the specific language governing permissions and
2116 * limitations under the License.
2117 */
2118/**
2119 * Currently simplistic, this class manages what transport a Connection should use at various stages of its
2120 * lifecycle.
2121 *
2122 * It starts with longpolling in a browser, and httppolling on node. It then upgrades to websockets if
2123 * they are available.
2124 */
2125class TransportManager {
2126 /**
2127 * @param repoInfo - Metadata around the namespace we're connecting to
2128 */
2129 constructor(repoInfo) {
2130 this.initTransports_(repoInfo);
2131 }
2132 static get ALL_TRANSPORTS() {
2133 return [BrowserPollConnection, WebSocketConnection];
2134 }
2135 /**
2136 * Returns whether transport has been selected to ensure WebSocketConnection or BrowserPollConnection are not called after
2137 * TransportManager has already set up transports_
2138 */
2139 static get IS_TRANSPORT_INITIALIZED() {
2140 return this.globalTransportInitialized_;
2141 }
2142 initTransports_(repoInfo) {
2143 const isWebSocketsAvailable = WebSocketConnection && WebSocketConnection['isAvailable']();
2144 let isSkipPollConnection = isWebSocketsAvailable && !WebSocketConnection.previouslyFailed();
2145 if (repoInfo.webSocketOnly) {
2146 if (!isWebSocketsAvailable) {
2147 warn("wss:// URL used, but browser isn't known to support websockets. Trying anyway.");
2148 }
2149 isSkipPollConnection = true;
2150 }
2151 if (isSkipPollConnection) {
2152 this.transports_ = [WebSocketConnection];
2153 }
2154 else {
2155 const transports = (this.transports_ = []);
2156 for (const transport of TransportManager.ALL_TRANSPORTS) {
2157 if (transport && transport['isAvailable']()) {
2158 transports.push(transport);
2159 }
2160 }
2161 TransportManager.globalTransportInitialized_ = true;
2162 }
2163 }
2164 /**
2165 * @returns The constructor for the initial transport to use
2166 */
2167 initialTransport() {
2168 if (this.transports_.length > 0) {
2169 return this.transports_[0];
2170 }
2171 else {
2172 throw new Error('No transports available');
2173 }
2174 }
2175 /**
2176 * @returns The constructor for the next transport, or null
2177 */
2178 upgradeTransport() {
2179 if (this.transports_.length > 1) {
2180 return this.transports_[1];
2181 }
2182 else {
2183 return null;
2184 }
2185 }
2186}
2187// Keeps track of whether the TransportManager has already chosen a transport to use
2188TransportManager.globalTransportInitialized_ = false;
2189
2190/**
2191 * @license
2192 * Copyright 2017 Google LLC
2193 *
2194 * Licensed under the Apache License, Version 2.0 (the "License");
2195 * you may not use this file except in compliance with the License.
2196 * You may obtain a copy of the License at
2197 *
2198 * http://www.apache.org/licenses/LICENSE-2.0
2199 *
2200 * Unless required by applicable law or agreed to in writing, software
2201 * distributed under the License is distributed on an "AS IS" BASIS,
2202 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2203 * See the License for the specific language governing permissions and
2204 * limitations under the License.
2205 */
2206// Abort upgrade attempt if it takes longer than 60s.
2207const UPGRADE_TIMEOUT = 60000;
2208// For some transports (WebSockets), we need to "validate" the transport by exchanging a few requests and responses.
2209// If we haven't sent enough requests within 5s, we'll start sending noop ping requests.
2210const DELAY_BEFORE_SENDING_EXTRA_REQUESTS = 5000;
2211// 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)
2212// then we may not be able to exchange our ping/pong requests within the healthy timeout. So if we reach the timeout
2213// but we've sent/received enough bytes, we don't cancel the connection.
2214const BYTES_SENT_HEALTHY_OVERRIDE = 10 * 1024;
2215const BYTES_RECEIVED_HEALTHY_OVERRIDE = 100 * 1024;
2216const MESSAGE_TYPE = 't';
2217const MESSAGE_DATA = 'd';
2218const CONTROL_SHUTDOWN = 's';
2219const CONTROL_RESET = 'r';
2220const CONTROL_ERROR = 'e';
2221const CONTROL_PONG = 'o';
2222const SWITCH_ACK = 'a';
2223const END_TRANSMISSION = 'n';
2224const PING = 'p';
2225const SERVER_HELLO = 'h';
2226/**
2227 * Creates a new real-time connection to the server using whichever method works
2228 * best in the current browser.
2229 */
2230class Connection {
2231 /**
2232 * @param id - an id for this connection
2233 * @param repoInfo_ - the info for the endpoint to connect to
2234 * @param applicationId_ - the Firebase App ID for this project
2235 * @param appCheckToken_ - The App Check Token for this device.
2236 * @param authToken_ - The auth token for this session.
2237 * @param onMessage_ - the callback to be triggered when a server-push message arrives
2238 * @param onReady_ - the callback to be triggered when this connection is ready to send messages.
2239 * @param onDisconnect_ - the callback to be triggered when a connection was lost
2240 * @param onKill_ - the callback to be triggered when this connection has permanently shut down.
2241 * @param lastSessionId - last session id in persistent connection. is used to clean up old session in real-time server
2242 */
2243 constructor(id, repoInfo_, applicationId_, appCheckToken_, authToken_, onMessage_, onReady_, onDisconnect_, onKill_, lastSessionId) {
2244 this.id = id;
2245 this.repoInfo_ = repoInfo_;
2246 this.applicationId_ = applicationId_;
2247 this.appCheckToken_ = appCheckToken_;
2248 this.authToken_ = authToken_;
2249 this.onMessage_ = onMessage_;
2250 this.onReady_ = onReady_;
2251 this.onDisconnect_ = onDisconnect_;
2252 this.onKill_ = onKill_;
2253 this.lastSessionId = lastSessionId;
2254 this.connectionCount = 0;
2255 this.pendingDataMessages = [];
2256 this.state_ = 0 /* CONNECTING */;
2257 this.log_ = logWrapper('c:' + this.id + ':');
2258 this.transportManager_ = new TransportManager(repoInfo_);
2259 this.log_('Connection created');
2260 this.start_();
2261 }
2262 /**
2263 * Starts a connection attempt
2264 */
2265 start_() {
2266 const conn = this.transportManager_.initialTransport();
2267 this.conn_ = new conn(this.nextTransportId_(), this.repoInfo_, this.applicationId_, this.appCheckToken_, this.authToken_, null, this.lastSessionId);
2268 // For certain transports (WebSockets), we need to send and receive several messages back and forth before we
2269 // can consider the transport healthy.
2270 this.primaryResponsesRequired_ = conn['responsesRequiredToBeHealthy'] || 0;
2271 const onMessageReceived = this.connReceiver_(this.conn_);
2272 const onConnectionLost = this.disconnReceiver_(this.conn_);
2273 this.tx_ = this.conn_;
2274 this.rx_ = this.conn_;
2275 this.secondaryConn_ = null;
2276 this.isHealthy_ = false;
2277 /*
2278 * Firefox doesn't like when code from one iframe tries to create another iframe by way of the parent frame.
2279 * This can occur in the case of a redirect, i.e. we guessed wrong on what server to connect to and received a reset.
2280 * Somehow, setTimeout seems to make this ok. That doesn't make sense from a security perspective, since you should
2281 * still have the context of your originating frame.
2282 */
2283 setTimeout(() => {
2284 // this.conn_ gets set to null in some of the tests. Check to make sure it still exists before using it
2285 this.conn_ && this.conn_.open(onMessageReceived, onConnectionLost);
2286 }, Math.floor(0));
2287 const healthyTimeoutMS = conn['healthyTimeout'] || 0;
2288 if (healthyTimeoutMS > 0) {
2289 this.healthyTimeout_ = setTimeoutNonBlocking(() => {
2290 this.healthyTimeout_ = null;
2291 if (!this.isHealthy_) {
2292 if (this.conn_ &&
2293 this.conn_.bytesReceived > BYTES_RECEIVED_HEALTHY_OVERRIDE) {
2294 this.log_('Connection exceeded healthy timeout but has received ' +
2295 this.conn_.bytesReceived +
2296 ' bytes. Marking connection healthy.');
2297 this.isHealthy_ = true;
2298 this.conn_.markConnectionHealthy();
2299 }
2300 else if (this.conn_ &&
2301 this.conn_.bytesSent > BYTES_SENT_HEALTHY_OVERRIDE) {
2302 this.log_('Connection exceeded healthy timeout but has sent ' +
2303 this.conn_.bytesSent +
2304 ' bytes. Leaving connection alive.');
2305 // NOTE: We don't want to mark it healthy, since we have no guarantee that the bytes have made it to
2306 // the server.
2307 }
2308 else {
2309 this.log_('Closing unhealthy connection after timeout.');
2310 this.close();
2311 }
2312 }
2313 // eslint-disable-next-line @typescript-eslint/no-explicit-any
2314 }, Math.floor(healthyTimeoutMS));
2315 }
2316 }
2317 nextTransportId_() {
2318 return 'c:' + this.id + ':' + this.connectionCount++;
2319 }
2320 disconnReceiver_(conn) {
2321 return everConnected => {
2322 if (conn === this.conn_) {
2323 this.onConnectionLost_(everConnected);
2324 }
2325 else if (conn === this.secondaryConn_) {
2326 this.log_('Secondary connection lost.');
2327 this.onSecondaryConnectionLost_();
2328 }
2329 else {
2330 this.log_('closing an old connection');
2331 }
2332 };
2333 }
2334 connReceiver_(conn) {
2335 return (message) => {
2336 if (this.state_ !== 2 /* DISCONNECTED */) {
2337 if (conn === this.rx_) {
2338 this.onPrimaryMessageReceived_(message);
2339 }
2340 else if (conn === this.secondaryConn_) {
2341 this.onSecondaryMessageReceived_(message);
2342 }
2343 else {
2344 this.log_('message on old connection');
2345 }
2346 }
2347 };
2348 }
2349 /**
2350 * @param dataMsg - An arbitrary data message to be sent to the server
2351 */
2352 sendRequest(dataMsg) {
2353 // wrap in a data message envelope and send it on
2354 const msg = { t: 'd', d: dataMsg };
2355 this.sendData_(msg);
2356 }
2357 tryCleanupConnection() {
2358 if (this.tx_ === this.secondaryConn_ && this.rx_ === this.secondaryConn_) {
2359 this.log_('cleaning up and promoting a connection: ' + this.secondaryConn_.connId);
2360 this.conn_ = this.secondaryConn_;
2361 this.secondaryConn_ = null;
2362 // the server will shutdown the old connection
2363 }
2364 }
2365 onSecondaryControl_(controlData) {
2366 if (MESSAGE_TYPE in controlData) {
2367 const cmd = controlData[MESSAGE_TYPE];
2368 if (cmd === SWITCH_ACK) {
2369 this.upgradeIfSecondaryHealthy_();
2370 }
2371 else if (cmd === CONTROL_RESET) {
2372 // Most likely the session wasn't valid. Abandon the switch attempt
2373 this.log_('Got a reset on secondary, closing it');
2374 this.secondaryConn_.close();
2375 // If we were already using this connection for something, than we need to fully close
2376 if (this.tx_ === this.secondaryConn_ ||
2377 this.rx_ === this.secondaryConn_) {
2378 this.close();
2379 }
2380 }
2381 else if (cmd === CONTROL_PONG) {
2382 this.log_('got pong on secondary.');
2383 this.secondaryResponsesRequired_--;
2384 this.upgradeIfSecondaryHealthy_();
2385 }
2386 }
2387 }
2388 onSecondaryMessageReceived_(parsedData) {
2389 const layer = requireKey('t', parsedData);
2390 const data = requireKey('d', parsedData);
2391 if (layer === 'c') {
2392 this.onSecondaryControl_(data);
2393 }
2394 else if (layer === 'd') {
2395 // got a data message, but we're still second connection. Need to buffer it up
2396 this.pendingDataMessages.push(data);
2397 }
2398 else {
2399 throw new Error('Unknown protocol layer: ' + layer);
2400 }
2401 }
2402 upgradeIfSecondaryHealthy_() {
2403 if (this.secondaryResponsesRequired_ <= 0) {
2404 this.log_('Secondary connection is healthy.');
2405 this.isHealthy_ = true;
2406 this.secondaryConn_.markConnectionHealthy();
2407 this.proceedWithUpgrade_();
2408 }
2409 else {
2410 // Send a ping to make sure the connection is healthy.
2411 this.log_('sending ping on secondary.');
2412 this.secondaryConn_.send({ t: 'c', d: { t: PING, d: {} } });
2413 }
2414 }
2415 proceedWithUpgrade_() {
2416 // tell this connection to consider itself open
2417 this.secondaryConn_.start();
2418 // send ack
2419 this.log_('sending client ack on secondary');
2420 this.secondaryConn_.send({ t: 'c', d: { t: SWITCH_ACK, d: {} } });
2421 // send end packet on primary transport, switch to sending on this one
2422 // can receive on this one, buffer responses until end received on primary transport
2423 this.log_('Ending transmission on primary');
2424 this.conn_.send({ t: 'c', d: { t: END_TRANSMISSION, d: {} } });
2425 this.tx_ = this.secondaryConn_;
2426 this.tryCleanupConnection();
2427 }
2428 onPrimaryMessageReceived_(parsedData) {
2429 // Must refer to parsedData properties in quotes, so closure doesn't touch them.
2430 const layer = requireKey('t', parsedData);
2431 const data = requireKey('d', parsedData);
2432 if (layer === 'c') {
2433 this.onControl_(data);
2434 }
2435 else if (layer === 'd') {
2436 this.onDataMessage_(data);
2437 }
2438 }
2439 onDataMessage_(message) {
2440 this.onPrimaryResponse_();
2441 // We don't do anything with data messages, just kick them up a level
2442 this.onMessage_(message);
2443 }
2444 onPrimaryResponse_() {
2445 if (!this.isHealthy_) {
2446 this.primaryResponsesRequired_--;
2447 if (this.primaryResponsesRequired_ <= 0) {
2448 this.log_('Primary connection is healthy.');
2449 this.isHealthy_ = true;
2450 this.conn_.markConnectionHealthy();
2451 }
2452 }
2453 }
2454 onControl_(controlData) {
2455 const cmd = requireKey(MESSAGE_TYPE, controlData);
2456 if (MESSAGE_DATA in controlData) {
2457 const payload = controlData[MESSAGE_DATA];
2458 if (cmd === SERVER_HELLO) {
2459 this.onHandshake_(payload);
2460 }
2461 else if (cmd === END_TRANSMISSION) {
2462 this.log_('recvd end transmission on primary');
2463 this.rx_ = this.secondaryConn_;
2464 for (let i = 0; i < this.pendingDataMessages.length; ++i) {
2465 this.onDataMessage_(this.pendingDataMessages[i]);
2466 }
2467 this.pendingDataMessages = [];
2468 this.tryCleanupConnection();
2469 }
2470 else if (cmd === CONTROL_SHUTDOWN) {
2471 // This was previously the 'onKill' callback passed to the lower-level connection
2472 // payload in this case is the reason for the shutdown. Generally a human-readable error
2473 this.onConnectionShutdown_(payload);
2474 }
2475 else if (cmd === CONTROL_RESET) {
2476 // payload in this case is the host we should contact
2477 this.onReset_(payload);
2478 }
2479 else if (cmd === CONTROL_ERROR) {
2480 error('Server Error: ' + payload);
2481 }
2482 else if (cmd === CONTROL_PONG) {
2483 this.log_('got pong on primary.');
2484 this.onPrimaryResponse_();
2485 this.sendPingOnPrimaryIfNecessary_();
2486 }
2487 else {
2488 error('Unknown control packet command: ' + cmd);
2489 }
2490 }
2491 }
2492 /**
2493 * @param handshake - The handshake data returned from the server
2494 */
2495 onHandshake_(handshake) {
2496 const timestamp = handshake.ts;
2497 const version = handshake.v;
2498 const host = handshake.h;
2499 this.sessionId = handshake.s;
2500 this.repoInfo_.host = host;
2501 // if we've already closed the connection, then don't bother trying to progress further
2502 if (this.state_ === 0 /* CONNECTING */) {
2503 this.conn_.start();
2504 this.onConnectionEstablished_(this.conn_, timestamp);
2505 if (PROTOCOL_VERSION !== version) {
2506 warn('Protocol version mismatch detected');
2507 }
2508 // TODO: do we want to upgrade? when? maybe a delay?
2509 this.tryStartUpgrade_();
2510 }
2511 }
2512 tryStartUpgrade_() {
2513 const conn = this.transportManager_.upgradeTransport();
2514 if (conn) {
2515 this.startUpgrade_(conn);
2516 }
2517 }
2518 startUpgrade_(conn) {
2519 this.secondaryConn_ = new conn(this.nextTransportId_(), this.repoInfo_, this.applicationId_, this.appCheckToken_, this.authToken_, this.sessionId);
2520 // For certain transports (WebSockets), we need to send and receive several messages back and forth before we
2521 // can consider the transport healthy.
2522 this.secondaryResponsesRequired_ =
2523 conn['responsesRequiredToBeHealthy'] || 0;
2524 const onMessage = this.connReceiver_(this.secondaryConn_);
2525 const onDisconnect = this.disconnReceiver_(this.secondaryConn_);
2526 this.secondaryConn_.open(onMessage, onDisconnect);
2527 // If we haven't successfully upgraded after UPGRADE_TIMEOUT, give up and kill the secondary.
2528 setTimeoutNonBlocking(() => {
2529 if (this.secondaryConn_) {
2530 this.log_('Timed out trying to upgrade.');
2531 this.secondaryConn_.close();
2532 }
2533 }, Math.floor(UPGRADE_TIMEOUT));
2534 }
2535 onReset_(host) {
2536 this.log_('Reset packet received. New host: ' + host);
2537 this.repoInfo_.host = host;
2538 // TODO: if we're already "connected", we need to trigger a disconnect at the next layer up.
2539 // We don't currently support resets after the connection has already been established
2540 if (this.state_ === 1 /* CONNECTED */) {
2541 this.close();
2542 }
2543 else {
2544 // Close whatever connections we have open and start again.
2545 this.closeConnections_();
2546 this.start_();
2547 }
2548 }
2549 onConnectionEstablished_(conn, timestamp) {
2550 this.log_('Realtime connection established.');
2551 this.conn_ = conn;
2552 this.state_ = 1 /* CONNECTED */;
2553 if (this.onReady_) {
2554 this.onReady_(timestamp, this.sessionId);
2555 this.onReady_ = null;
2556 }
2557 // If after 5 seconds we haven't sent enough requests to the server to get the connection healthy,
2558 // send some pings.
2559 if (this.primaryResponsesRequired_ === 0) {
2560 this.log_('Primary connection is healthy.');
2561 this.isHealthy_ = true;
2562 }
2563 else {
2564 setTimeoutNonBlocking(() => {
2565 this.sendPingOnPrimaryIfNecessary_();
2566 }, Math.floor(DELAY_BEFORE_SENDING_EXTRA_REQUESTS));
2567 }
2568 }
2569 sendPingOnPrimaryIfNecessary_() {
2570 // If the connection isn't considered healthy yet, we'll send a noop ping packet request.
2571 if (!this.isHealthy_ && this.state_ === 1 /* CONNECTED */) {
2572 this.log_('sending ping on primary.');
2573 this.sendData_({ t: 'c', d: { t: PING, d: {} } });
2574 }
2575 }
2576 onSecondaryConnectionLost_() {
2577 const conn = this.secondaryConn_;
2578 this.secondaryConn_ = null;
2579 if (this.tx_ === conn || this.rx_ === conn) {
2580 // we are relying on this connection already in some capacity. Therefore, a failure is real
2581 this.close();
2582 }
2583 }
2584 /**
2585 * @param everConnected - Whether or not the connection ever reached a server. Used to determine if
2586 * we should flush the host cache
2587 */
2588 onConnectionLost_(everConnected) {
2589 this.conn_ = null;
2590 // NOTE: IF you're seeing a Firefox error for this line, I think it might be because it's getting
2591 // called on window close and RealtimeState.CONNECTING is no longer defined. Just a guess.
2592 if (!everConnected && this.state_ === 0 /* CONNECTING */) {
2593 this.log_('Realtime connection failed.');
2594 // Since we failed to connect at all, clear any cached entry for this namespace in case the machine went away
2595 if (this.repoInfo_.isCacheableHost()) {
2596 PersistentStorage.remove('host:' + this.repoInfo_.host);
2597 // reset the internal host to what we would show the user, i.e. <ns>.firebaseio.com
2598 this.repoInfo_.internalHost = this.repoInfo_.host;
2599 }
2600 }
2601 else if (this.state_ === 1 /* CONNECTED */) {
2602 this.log_('Realtime connection lost.');
2603 }
2604 this.close();
2605 }
2606 onConnectionShutdown_(reason) {
2607 this.log_('Connection shutdown command received. Shutting down...');
2608 if (this.onKill_) {
2609 this.onKill_(reason);
2610 this.onKill_ = null;
2611 }
2612 // We intentionally don't want to fire onDisconnect (kill is a different case),
2613 // so clear the callback.
2614 this.onDisconnect_ = null;
2615 this.close();
2616 }
2617 sendData_(data) {
2618 if (this.state_ !== 1 /* CONNECTED */) {
2619 throw 'Connection is not connected';
2620 }
2621 else {
2622 this.tx_.send(data);
2623 }
2624 }
2625 /**
2626 * Cleans up this connection, calling the appropriate callbacks
2627 */
2628 close() {
2629 if (this.state_ !== 2 /* DISCONNECTED */) {
2630 this.log_('Closing realtime connection.');
2631 this.state_ = 2 /* DISCONNECTED */;
2632 this.closeConnections_();
2633 if (this.onDisconnect_) {
2634 this.onDisconnect_();
2635 this.onDisconnect_ = null;
2636 }
2637 }
2638 }
2639 closeConnections_() {
2640 this.log_('Shutting down all connections');
2641 if (this.conn_) {
2642 this.conn_.close();
2643 this.conn_ = null;
2644 }
2645 if (this.secondaryConn_) {
2646 this.secondaryConn_.close();
2647 this.secondaryConn_ = null;
2648 }
2649 if (this.healthyTimeout_) {
2650 clearTimeout(this.healthyTimeout_);
2651 this.healthyTimeout_ = null;
2652 }
2653 }
2654}
2655
2656/**
2657 * @license
2658 * Copyright 2017 Google LLC
2659 *
2660 * Licensed under the Apache License, Version 2.0 (the "License");
2661 * you may not use this file except in compliance with the License.
2662 * You may obtain a copy of the License at
2663 *
2664 * http://www.apache.org/licenses/LICENSE-2.0
2665 *
2666 * Unless required by applicable law or agreed to in writing, software
2667 * distributed under the License is distributed on an "AS IS" BASIS,
2668 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2669 * See the License for the specific language governing permissions and
2670 * limitations under the License.
2671 */
2672/**
2673 * Interface defining the set of actions that can be performed against the Firebase server
2674 * (basically corresponds to our wire protocol).
2675 *
2676 * @interface
2677 */
2678class ServerActions {
2679 put(pathString, data, onComplete, hash) { }
2680 merge(pathString, data, onComplete, hash) { }
2681 /**
2682 * Refreshes the auth token for the current connection.
2683 * @param token - The authentication token
2684 */
2685 refreshAuthToken(token) { }
2686 /**
2687 * Refreshes the app check token for the current connection.
2688 * @param token The app check token
2689 */
2690 refreshAppCheckToken(token) { }
2691 onDisconnectPut(pathString, data, onComplete) { }
2692 onDisconnectMerge(pathString, data, onComplete) { }
2693 onDisconnectCancel(pathString, onComplete) { }
2694 reportStats(stats) { }
2695}
2696
2697/**
2698 * @license
2699 * Copyright 2017 Google LLC
2700 *
2701 * Licensed under the Apache License, Version 2.0 (the "License");
2702 * you may not use this file except in compliance with the License.
2703 * You may obtain a copy of the License at
2704 *
2705 * http://www.apache.org/licenses/LICENSE-2.0
2706 *
2707 * Unless required by applicable law or agreed to in writing, software
2708 * distributed under the License is distributed on an "AS IS" BASIS,
2709 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2710 * See the License for the specific language governing permissions and
2711 * limitations under the License.
2712 */
2713/**
2714 * Base class to be used if you want to emit events. Call the constructor with
2715 * the set of allowed event names.
2716 */
2717class EventEmitter {
2718 constructor(allowedEvents_) {
2719 this.allowedEvents_ = allowedEvents_;
2720 this.listeners_ = {};
2721 assert(Array.isArray(allowedEvents_) && allowedEvents_.length > 0, 'Requires a non-empty array');
2722 }
2723 /**
2724 * To be called by derived classes to trigger events.
2725 */
2726 trigger(eventType, ...varArgs) {
2727 if (Array.isArray(this.listeners_[eventType])) {
2728 // Clone the list, since callbacks could add/remove listeners.
2729 const listeners = [...this.listeners_[eventType]];
2730 for (let i = 0; i < listeners.length; i++) {
2731 listeners[i].callback.apply(listeners[i].context, varArgs);
2732 }
2733 }
2734 }
2735 on(eventType, callback, context) {
2736 this.validateEventType_(eventType);
2737 this.listeners_[eventType] = this.listeners_[eventType] || [];
2738 this.listeners_[eventType].push({ callback, context });
2739 const eventData = this.getInitialEvent(eventType);
2740 if (eventData) {
2741 callback.apply(context, eventData);
2742 }
2743 }
2744 off(eventType, callback, context) {
2745 this.validateEventType_(eventType);
2746 const listeners = this.listeners_[eventType] || [];
2747 for (let i = 0; i < listeners.length; i++) {
2748 if (listeners[i].callback === callback &&
2749 (!context || context === listeners[i].context)) {
2750 listeners.splice(i, 1);
2751 return;
2752 }
2753 }
2754 }
2755 validateEventType_(eventType) {
2756 assert(this.allowedEvents_.find(et => {
2757 return et === eventType;
2758 }), 'Unknown event: ' + eventType);
2759 }
2760}
2761
2762/**
2763 * @license
2764 * Copyright 2017 Google LLC
2765 *
2766 * Licensed under the Apache License, Version 2.0 (the "License");
2767 * you may not use this file except in compliance with the License.
2768 * You may obtain a copy of the License at
2769 *
2770 * http://www.apache.org/licenses/LICENSE-2.0
2771 *
2772 * Unless required by applicable law or agreed to in writing, software
2773 * distributed under the License is distributed on an "AS IS" BASIS,
2774 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2775 * See the License for the specific language governing permissions and
2776 * limitations under the License.
2777 */
2778/**
2779 * Monitors online state (as reported by window.online/offline events).
2780 *
2781 * The expectation is that this could have many false positives (thinks we are online
2782 * when we're not), but no false negatives. So we can safely use it to determine when
2783 * we definitely cannot reach the internet.
2784 */
2785class OnlineMonitor extends EventEmitter {
2786 constructor() {
2787 super(['online']);
2788 this.online_ = true;
2789 // We've had repeated complaints that Cordova apps can get stuck "offline", e.g.
2790 // https://forum.ionicframework.com/t/firebase-connection-is-lost-and-never-come-back/43810
2791 // It would seem that the 'online' event does not always fire consistently. So we disable it
2792 // for Cordova.
2793 if (typeof window !== 'undefined' &&
2794 typeof window.addEventListener !== 'undefined' &&
2795 !isMobileCordova()) {
2796 window.addEventListener('online', () => {
2797 if (!this.online_) {
2798 this.online_ = true;
2799 this.trigger('online', true);
2800 }
2801 }, false);
2802 window.addEventListener('offline', () => {
2803 if (this.online_) {
2804 this.online_ = false;
2805 this.trigger('online', false);
2806 }
2807 }, false);
2808 }
2809 }
2810 static getInstance() {
2811 return new OnlineMonitor();
2812 }
2813 getInitialEvent(eventType) {
2814 assert(eventType === 'online', 'Unknown event type: ' + eventType);
2815 return [this.online_];
2816 }
2817 currentlyOnline() {
2818 return this.online_;
2819 }
2820}
2821
2822/**
2823 * @license
2824 * Copyright 2017 Google LLC
2825 *
2826 * Licensed under the Apache License, Version 2.0 (the "License");
2827 * you may not use this file except in compliance with the License.
2828 * You may obtain a copy of the License at
2829 *
2830 * http://www.apache.org/licenses/LICENSE-2.0
2831 *
2832 * Unless required by applicable law or agreed to in writing, software
2833 * distributed under the License is distributed on an "AS IS" BASIS,
2834 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2835 * See the License for the specific language governing permissions and
2836 * limitations under the License.
2837 */
2838/** Maximum key depth. */
2839const MAX_PATH_DEPTH = 32;
2840/** Maximum number of (UTF8) bytes in a Firebase path. */
2841const MAX_PATH_LENGTH_BYTES = 768;
2842/**
2843 * An immutable object representing a parsed path. It's immutable so that you
2844 * can pass them around to other functions without worrying about them changing
2845 * it.
2846 */
2847class Path {
2848 /**
2849 * @param pathOrString - Path string to parse, or another path, or the raw
2850 * tokens array
2851 */
2852 constructor(pathOrString, pieceNum) {
2853 if (pieceNum === void 0) {
2854 this.pieces_ = pathOrString.split('/');
2855 // Remove empty pieces.
2856 let copyTo = 0;
2857 for (let i = 0; i < this.pieces_.length; i++) {
2858 if (this.pieces_[i].length > 0) {
2859 this.pieces_[copyTo] = this.pieces_[i];
2860 copyTo++;
2861 }
2862 }
2863 this.pieces_.length = copyTo;
2864 this.pieceNum_ = 0;
2865 }
2866 else {
2867 this.pieces_ = pathOrString;
2868 this.pieceNum_ = pieceNum;
2869 }
2870 }
2871 toString() {
2872 let pathString = '';
2873 for (let i = this.pieceNum_; i < this.pieces_.length; i++) {
2874 if (this.pieces_[i] !== '') {
2875 pathString += '/' + this.pieces_[i];
2876 }
2877 }
2878 return pathString || '/';
2879 }
2880}
2881function newEmptyPath() {
2882 return new Path('');
2883}
2884function pathGetFront(path) {
2885 if (path.pieceNum_ >= path.pieces_.length) {
2886 return null;
2887 }
2888 return path.pieces_[path.pieceNum_];
2889}
2890/**
2891 * @returns The number of segments in this path
2892 */
2893function pathGetLength(path) {
2894 return path.pieces_.length - path.pieceNum_;
2895}
2896function pathPopFront(path) {
2897 let pieceNum = path.pieceNum_;
2898 if (pieceNum < path.pieces_.length) {
2899 pieceNum++;
2900 }
2901 return new Path(path.pieces_, pieceNum);
2902}
2903function pathGetBack(path) {
2904 if (path.pieceNum_ < path.pieces_.length) {
2905 return path.pieces_[path.pieces_.length - 1];
2906 }
2907 return null;
2908}
2909function pathToUrlEncodedString(path) {
2910 let pathString = '';
2911 for (let i = path.pieceNum_; i < path.pieces_.length; i++) {
2912 if (path.pieces_[i] !== '') {
2913 pathString += '/' + encodeURIComponent(String(path.pieces_[i]));
2914 }
2915 }
2916 return pathString || '/';
2917}
2918/**
2919 * Shallow copy of the parts of the path.
2920 *
2921 */
2922function pathSlice(path, begin = 0) {
2923 return path.pieces_.slice(path.pieceNum_ + begin);
2924}
2925function pathParent(path) {
2926 if (path.pieceNum_ >= path.pieces_.length) {
2927 return null;
2928 }
2929 const pieces = [];
2930 for (let i = path.pieceNum_; i < path.pieces_.length - 1; i++) {
2931 pieces.push(path.pieces_[i]);
2932 }
2933 return new Path(pieces, 0);
2934}
2935function pathChild(path, childPathObj) {
2936 const pieces = [];
2937 for (let i = path.pieceNum_; i < path.pieces_.length; i++) {
2938 pieces.push(path.pieces_[i]);
2939 }
2940 if (childPathObj instanceof Path) {
2941 for (let i = childPathObj.pieceNum_; i < childPathObj.pieces_.length; i++) {
2942 pieces.push(childPathObj.pieces_[i]);
2943 }
2944 }
2945 else {
2946 const childPieces = childPathObj.split('/');
2947 for (let i = 0; i < childPieces.length; i++) {
2948 if (childPieces[i].length > 0) {
2949 pieces.push(childPieces[i]);
2950 }
2951 }
2952 }
2953 return new Path(pieces, 0);
2954}
2955/**
2956 * @returns True if there are no segments in this path
2957 */
2958function pathIsEmpty(path) {
2959 return path.pieceNum_ >= path.pieces_.length;
2960}
2961/**
2962 * @returns The path from outerPath to innerPath
2963 */
2964function newRelativePath(outerPath, innerPath) {
2965 const outer = pathGetFront(outerPath), inner = pathGetFront(innerPath);
2966 if (outer === null) {
2967 return innerPath;
2968 }
2969 else if (outer === inner) {
2970 return newRelativePath(pathPopFront(outerPath), pathPopFront(innerPath));
2971 }
2972 else {
2973 throw new Error('INTERNAL ERROR: innerPath (' +
2974 innerPath +
2975 ') is not within ' +
2976 'outerPath (' +
2977 outerPath +
2978 ')');
2979 }
2980}
2981/**
2982 * @returns -1, 0, 1 if left is less, equal, or greater than the right.
2983 */
2984function pathCompare(left, right) {
2985 const leftKeys = pathSlice(left, 0);
2986 const rightKeys = pathSlice(right, 0);
2987 for (let i = 0; i < leftKeys.length && i < rightKeys.length; i++) {
2988 const cmp = nameCompare(leftKeys[i], rightKeys[i]);
2989 if (cmp !== 0) {
2990 return cmp;
2991 }
2992 }
2993 if (leftKeys.length === rightKeys.length) {
2994 return 0;
2995 }
2996 return leftKeys.length < rightKeys.length ? -1 : 1;
2997}
2998/**
2999 * @returns true if paths are the same.
3000 */
3001function pathEquals(path, other) {
3002 if (pathGetLength(path) !== pathGetLength(other)) {
3003 return false;
3004 }
3005 for (let i = path.pieceNum_, j = other.pieceNum_; i <= path.pieces_.length; i++, j++) {
3006 if (path.pieces_[i] !== other.pieces_[j]) {
3007 return false;
3008 }
3009 }
3010 return true;
3011}
3012/**
3013 * @returns True if this path is a parent of (or the same as) other
3014 */
3015function pathContains(path, other) {
3016 let i = path.pieceNum_;
3017 let j = other.pieceNum_;
3018 if (pathGetLength(path) > pathGetLength(other)) {
3019 return false;
3020 }
3021 while (i < path.pieces_.length) {
3022 if (path.pieces_[i] !== other.pieces_[j]) {
3023 return false;
3024 }
3025 ++i;
3026 ++j;
3027 }
3028 return true;
3029}
3030/**
3031 * Dynamic (mutable) path used to count path lengths.
3032 *
3033 * This class is used to efficiently check paths for valid
3034 * length (in UTF8 bytes) and depth (used in path validation).
3035 *
3036 * Throws Error exception if path is ever invalid.
3037 *
3038 * The definition of a path always begins with '/'.
3039 */
3040class ValidationPath {
3041 /**
3042 * @param path - Initial Path.
3043 * @param errorPrefix_ - Prefix for any error messages.
3044 */
3045 constructor(path, errorPrefix_) {
3046 this.errorPrefix_ = errorPrefix_;
3047 this.parts_ = pathSlice(path, 0);
3048 /** Initialize to number of '/' chars needed in path. */
3049 this.byteLength_ = Math.max(1, this.parts_.length);
3050 for (let i = 0; i < this.parts_.length; i++) {
3051 this.byteLength_ += stringLength(this.parts_[i]);
3052 }
3053 validationPathCheckValid(this);
3054 }
3055}
3056function validationPathPush(validationPath, child) {
3057 // Count the needed '/'
3058 if (validationPath.parts_.length > 0) {
3059 validationPath.byteLength_ += 1;
3060 }
3061 validationPath.parts_.push(child);
3062 validationPath.byteLength_ += stringLength(child);
3063 validationPathCheckValid(validationPath);
3064}
3065function validationPathPop(validationPath) {
3066 const last = validationPath.parts_.pop();
3067 validationPath.byteLength_ -= stringLength(last);
3068 // Un-count the previous '/'
3069 if (validationPath.parts_.length > 0) {
3070 validationPath.byteLength_ -= 1;
3071 }
3072}
3073function validationPathCheckValid(validationPath) {
3074 if (validationPath.byteLength_ > MAX_PATH_LENGTH_BYTES) {
3075 throw new Error(validationPath.errorPrefix_ +
3076 'has a key path longer than ' +
3077 MAX_PATH_LENGTH_BYTES +
3078 ' bytes (' +
3079 validationPath.byteLength_ +
3080 ').');
3081 }
3082 if (validationPath.parts_.length > MAX_PATH_DEPTH) {
3083 throw new Error(validationPath.errorPrefix_ +
3084 'path specified exceeds the maximum depth that can be written (' +
3085 MAX_PATH_DEPTH +
3086 ') or object contains a cycle ' +
3087 validationPathToErrorString(validationPath));
3088 }
3089}
3090/**
3091 * String for use in error messages - uses '.' notation for path.
3092 */
3093function validationPathToErrorString(validationPath) {
3094 if (validationPath.parts_.length === 0) {
3095 return '';
3096 }
3097 return "in property '" + validationPath.parts_.join('.') + "'";
3098}
3099
3100/**
3101 * @license
3102 * Copyright 2017 Google LLC
3103 *
3104 * Licensed under the Apache License, Version 2.0 (the "License");
3105 * you may not use this file except in compliance with the License.
3106 * You may obtain a copy of the License at
3107 *
3108 * http://www.apache.org/licenses/LICENSE-2.0
3109 *
3110 * Unless required by applicable law or agreed to in writing, software
3111 * distributed under the License is distributed on an "AS IS" BASIS,
3112 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3113 * See the License for the specific language governing permissions and
3114 * limitations under the License.
3115 */
3116class VisibilityMonitor extends EventEmitter {
3117 constructor() {
3118 super(['visible']);
3119 let hidden;
3120 let visibilityChange;
3121 if (typeof document !== 'undefined' &&
3122 typeof document.addEventListener !== 'undefined') {
3123 if (typeof document['hidden'] !== 'undefined') {
3124 // Opera 12.10 and Firefox 18 and later support
3125 visibilityChange = 'visibilitychange';
3126 hidden = 'hidden';
3127 }
3128 else if (typeof document['mozHidden'] !== 'undefined') {
3129 visibilityChange = 'mozvisibilitychange';
3130 hidden = 'mozHidden';
3131 }
3132 else if (typeof document['msHidden'] !== 'undefined') {
3133 visibilityChange = 'msvisibilitychange';
3134 hidden = 'msHidden';
3135 }
3136 else if (typeof document['webkitHidden'] !== 'undefined') {
3137 visibilityChange = 'webkitvisibilitychange';
3138 hidden = 'webkitHidden';
3139 }
3140 }
3141 // Initially, we always assume we are visible. This ensures that in browsers
3142 // without page visibility support or in cases where we are never visible
3143 // (e.g. chrome extension), we act as if we are visible, i.e. don't delay
3144 // reconnects
3145 this.visible_ = true;
3146 if (visibilityChange) {
3147 document.addEventListener(visibilityChange, () => {
3148 const visible = !document[hidden];
3149 if (visible !== this.visible_) {
3150 this.visible_ = visible;
3151 this.trigger('visible', visible);
3152 }
3153 }, false);
3154 }
3155 }
3156 static getInstance() {
3157 return new VisibilityMonitor();
3158 }
3159 getInitialEvent(eventType) {
3160 assert(eventType === 'visible', 'Unknown event type: ' + eventType);
3161 return [this.visible_];
3162 }
3163}
3164
3165/**
3166 * @license
3167 * Copyright 2017 Google LLC
3168 *
3169 * Licensed under the Apache License, Version 2.0 (the "License");
3170 * you may not use this file except in compliance with the License.
3171 * You may obtain a copy of the License at
3172 *
3173 * http://www.apache.org/licenses/LICENSE-2.0
3174 *
3175 * Unless required by applicable law or agreed to in writing, software
3176 * distributed under the License is distributed on an "AS IS" BASIS,
3177 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3178 * See the License for the specific language governing permissions and
3179 * limitations under the License.
3180 */
3181const RECONNECT_MIN_DELAY = 1000;
3182const RECONNECT_MAX_DELAY_DEFAULT = 60 * 5 * 1000; // 5 minutes in milliseconds (Case: 1858)
3183const RECONNECT_MAX_DELAY_FOR_ADMINS = 30 * 1000; // 30 seconds for admin clients (likely to be a backend server)
3184const RECONNECT_DELAY_MULTIPLIER = 1.3;
3185const RECONNECT_DELAY_RESET_TIMEOUT = 30000; // Reset delay back to MIN_DELAY after being connected for 30sec.
3186const SERVER_KILL_INTERRUPT_REASON = 'server_kill';
3187// If auth fails repeatedly, we'll assume something is wrong and log a warning / back off.
3188const INVALID_TOKEN_THRESHOLD = 3;
3189/**
3190 * Firebase connection. Abstracts wire protocol and handles reconnecting.
3191 *
3192 * NOTE: All JSON objects sent to the realtime connection must have property names enclosed
3193 * in quotes to make sure the closure compiler does not minify them.
3194 */
3195class PersistentConnection extends ServerActions {
3196 /**
3197 * @param repoInfo_ - Data about the namespace we are connecting to
3198 * @param applicationId_ - The Firebase App ID for this project
3199 * @param onDataUpdate_ - A callback for new data from the server
3200 */
3201 constructor(repoInfo_, applicationId_, onDataUpdate_, onConnectStatus_, onServerInfoUpdate_, authTokenProvider_, appCheckTokenProvider_, authOverride_) {
3202 super();
3203 this.repoInfo_ = repoInfo_;
3204 this.applicationId_ = applicationId_;
3205 this.onDataUpdate_ = onDataUpdate_;
3206 this.onConnectStatus_ = onConnectStatus_;
3207 this.onServerInfoUpdate_ = onServerInfoUpdate_;
3208 this.authTokenProvider_ = authTokenProvider_;
3209 this.appCheckTokenProvider_ = appCheckTokenProvider_;
3210 this.authOverride_ = authOverride_;
3211 // Used for diagnostic logging.
3212 this.id = PersistentConnection.nextPersistentConnectionId_++;
3213 this.log_ = logWrapper('p:' + this.id + ':');
3214 this.interruptReasons_ = {};
3215 this.listens = new Map();
3216 this.outstandingPuts_ = [];
3217 this.outstandingGets_ = [];
3218 this.outstandingPutCount_ = 0;
3219 this.outstandingGetCount_ = 0;
3220 this.onDisconnectRequestQueue_ = [];
3221 this.connected_ = false;
3222 this.reconnectDelay_ = RECONNECT_MIN_DELAY;
3223 this.maxReconnectDelay_ = RECONNECT_MAX_DELAY_DEFAULT;
3224 this.securityDebugCallback_ = null;
3225 this.lastSessionId = null;
3226 this.establishConnectionTimer_ = null;
3227 this.visible_ = false;
3228 // Before we get connected, we keep a queue of pending messages to send.
3229 this.requestCBHash_ = {};
3230 this.requestNumber_ = 0;
3231 this.realtime_ = null;
3232 this.authToken_ = null;
3233 this.appCheckToken_ = null;
3234 this.forceTokenRefresh_ = false;
3235 this.invalidAuthTokenCount_ = 0;
3236 this.invalidAppCheckTokenCount_ = 0;
3237 this.firstConnection_ = true;
3238 this.lastConnectionAttemptTime_ = null;
3239 this.lastConnectionEstablishedTime_ = null;
3240 if (authOverride_ && !isNodeSdk()) {
3241 throw new Error('Auth override specified in options, but not supported on non Node.js platforms');
3242 }
3243 VisibilityMonitor.getInstance().on('visible', this.onVisible_, this);
3244 if (repoInfo_.host.indexOf('fblocal') === -1) {
3245 OnlineMonitor.getInstance().on('online', this.onOnline_, this);
3246 }
3247 }
3248 sendRequest(action, body, onResponse) {
3249 const curReqNum = ++this.requestNumber_;
3250 const msg = { r: curReqNum, a: action, b: body };
3251 this.log_(stringify(msg));
3252 assert(this.connected_, "sendRequest call when we're not connected not allowed.");
3253 this.realtime_.sendRequest(msg);
3254 if (onResponse) {
3255 this.requestCBHash_[curReqNum] = onResponse;
3256 }
3257 }
3258 get(query) {
3259 this.initConnection_();
3260 const deferred = new Deferred();
3261 const request = {
3262 p: query._path.toString(),
3263 q: query._queryObject
3264 };
3265 const outstandingGet = {
3266 action: 'g',
3267 request,
3268 onComplete: (message) => {
3269 const payload = message['d'];
3270 if (message['s'] === 'ok') {
3271 deferred.resolve(payload);
3272 }
3273 else {
3274 deferred.reject(payload);
3275 }
3276 }
3277 };
3278 this.outstandingGets_.push(outstandingGet);
3279 this.outstandingGetCount_++;
3280 const index = this.outstandingGets_.length - 1;
3281 if (this.connected_) {
3282 this.sendGet_(index);
3283 }
3284 return deferred.promise;
3285 }
3286 listen(query, currentHashFn, tag, onComplete) {
3287 this.initConnection_();
3288 const queryId = query._queryIdentifier;
3289 const pathString = query._path.toString();
3290 this.log_('Listen called for ' + pathString + ' ' + queryId);
3291 if (!this.listens.has(pathString)) {
3292 this.listens.set(pathString, new Map());
3293 }
3294 assert(query._queryParams.isDefault() || !query._queryParams.loadsAllData(), 'listen() called for non-default but complete query');
3295 assert(!this.listens.get(pathString).has(queryId), `listen() called twice for same path/queryId.`);
3296 const listenSpec = {
3297 onComplete,
3298 hashFn: currentHashFn,
3299 query,
3300 tag
3301 };
3302 this.listens.get(pathString).set(queryId, listenSpec);
3303 if (this.connected_) {
3304 this.sendListen_(listenSpec);
3305 }
3306 }
3307 sendGet_(index) {
3308 const get = this.outstandingGets_[index];
3309 this.sendRequest('g', get.request, (message) => {
3310 delete this.outstandingGets_[index];
3311 this.outstandingGetCount_--;
3312 if (this.outstandingGetCount_ === 0) {
3313 this.outstandingGets_ = [];
3314 }
3315 if (get.onComplete) {
3316 get.onComplete(message);
3317 }
3318 });
3319 }
3320 sendListen_(listenSpec) {
3321 const query = listenSpec.query;
3322 const pathString = query._path.toString();
3323 const queryId = query._queryIdentifier;
3324 this.log_('Listen on ' + pathString + ' for ' + queryId);
3325 const req = { /*path*/ p: pathString };
3326 const action = 'q';
3327 // Only bother to send query if it's non-default.
3328 if (listenSpec.tag) {
3329 req['q'] = query._queryObject;
3330 req['t'] = listenSpec.tag;
3331 }
3332 req[ /*hash*/'h'] = listenSpec.hashFn();
3333 this.sendRequest(action, req, (message) => {
3334 const payload = message[ /*data*/'d'];
3335 const status = message[ /*status*/'s'];
3336 // print warnings in any case...
3337 PersistentConnection.warnOnListenWarnings_(payload, query);
3338 const currentListenSpec = this.listens.get(pathString) &&
3339 this.listens.get(pathString).get(queryId);
3340 // only trigger actions if the listen hasn't been removed and readded
3341 if (currentListenSpec === listenSpec) {
3342 this.log_('listen response', message);
3343 if (status !== 'ok') {
3344 this.removeListen_(pathString, queryId);
3345 }
3346 if (listenSpec.onComplete) {
3347 listenSpec.onComplete(status, payload);
3348 }
3349 }
3350 });
3351 }
3352 static warnOnListenWarnings_(payload, query) {
3353 if (payload && typeof payload === 'object' && contains(payload, 'w')) {
3354 // eslint-disable-next-line @typescript-eslint/no-explicit-any
3355 const warnings = safeGet(payload, 'w');
3356 if (Array.isArray(warnings) && ~warnings.indexOf('no_index')) {
3357 const indexSpec = '".indexOn": "' + query._queryParams.getIndex().toString() + '"';
3358 const indexPath = query._path.toString();
3359 warn(`Using an unspecified index. Your data will be downloaded and ` +
3360 `filtered on the client. Consider adding ${indexSpec} at ` +
3361 `${indexPath} to your security rules for better performance.`);
3362 }
3363 }
3364 }
3365 refreshAuthToken(token) {
3366 this.authToken_ = token;
3367 this.log_('Auth token refreshed');
3368 if (this.authToken_) {
3369 this.tryAuth();
3370 }
3371 else {
3372 //If we're connected we want to let the server know to unauthenticate us. If we're not connected, simply delete
3373 //the credential so we dont become authenticated next time we connect.
3374 if (this.connected_) {
3375 this.sendRequest('unauth', {}, () => { });
3376 }
3377 }
3378 this.reduceReconnectDelayIfAdminCredential_(token);
3379 }
3380 reduceReconnectDelayIfAdminCredential_(credential) {
3381 // NOTE: This isn't intended to be bulletproof (a malicious developer can always just modify the client).
3382 // Additionally, we don't bother resetting the max delay back to the default if auth fails / expires.
3383 const isFirebaseSecret = credential && credential.length === 40;
3384 if (isFirebaseSecret || isAdmin(credential)) {
3385 this.log_('Admin auth credential detected. Reducing max reconnect time.');
3386 this.maxReconnectDelay_ = RECONNECT_MAX_DELAY_FOR_ADMINS;
3387 }
3388 }
3389 refreshAppCheckToken(token) {
3390 this.appCheckToken_ = token;
3391 this.log_('App check token refreshed');
3392 if (this.appCheckToken_) {
3393 this.tryAppCheck();
3394 }
3395 else {
3396 //If we're connected we want to let the server know to unauthenticate us.
3397 //If we're not connected, simply delete the credential so we dont become
3398 // authenticated next time we connect.
3399 if (this.connected_) {
3400 this.sendRequest('unappeck', {}, () => { });
3401 }
3402 }
3403 }
3404 /**
3405 * Attempts to authenticate with the given credentials. If the authentication attempt fails, it's triggered like
3406 * a auth revoked (the connection is closed).
3407 */
3408 tryAuth() {
3409 if (this.connected_ && this.authToken_) {
3410 const token = this.authToken_;
3411 const authMethod = isValidFormat(token) ? 'auth' : 'gauth';
3412 const requestData = { cred: token };
3413 if (this.authOverride_ === null) {
3414 requestData['noauth'] = true;
3415 }
3416 else if (typeof this.authOverride_ === 'object') {
3417 requestData['authvar'] = this.authOverride_;
3418 }
3419 this.sendRequest(authMethod, requestData, (res) => {
3420 const status = res[ /*status*/'s'];
3421 const data = res[ /*data*/'d'] || 'error';
3422 if (this.authToken_ === token) {
3423 if (status === 'ok') {
3424 this.invalidAuthTokenCount_ = 0;
3425 }
3426 else {
3427 // Triggers reconnect and force refresh for auth token
3428 this.onAuthRevoked_(status, data);
3429 }
3430 }
3431 });
3432 }
3433 }
3434 /**
3435 * Attempts to authenticate with the given token. If the authentication
3436 * attempt fails, it's triggered like the token was revoked (the connection is
3437 * closed).
3438 */
3439 tryAppCheck() {
3440 if (this.connected_ && this.appCheckToken_) {
3441 this.sendRequest('appcheck', { 'token': this.appCheckToken_ }, (res) => {
3442 const status = res[ /*status*/'s'];
3443 const data = res[ /*data*/'d'] || 'error';
3444 if (status === 'ok') {
3445 this.invalidAppCheckTokenCount_ = 0;
3446 }
3447 else {
3448 this.onAppCheckRevoked_(status, data);
3449 }
3450 });
3451 }
3452 }
3453 /**
3454 * @inheritDoc
3455 */
3456 unlisten(query, tag) {
3457 const pathString = query._path.toString();
3458 const queryId = query._queryIdentifier;
3459 this.log_('Unlisten called for ' + pathString + ' ' + queryId);
3460 assert(query._queryParams.isDefault() || !query._queryParams.loadsAllData(), 'unlisten() called for non-default but complete query');
3461 const listen = this.removeListen_(pathString, queryId);
3462 if (listen && this.connected_) {
3463 this.sendUnlisten_(pathString, queryId, query._queryObject, tag);
3464 }
3465 }
3466 sendUnlisten_(pathString, queryId, queryObj, tag) {
3467 this.log_('Unlisten on ' + pathString + ' for ' + queryId);
3468 const req = { /*path*/ p: pathString };
3469 const action = 'n';
3470 // Only bother sending queryId if it's non-default.
3471 if (tag) {
3472 req['q'] = queryObj;
3473 req['t'] = tag;
3474 }
3475 this.sendRequest(action, req);
3476 }
3477 onDisconnectPut(pathString, data, onComplete) {
3478 this.initConnection_();
3479 if (this.connected_) {
3480 this.sendOnDisconnect_('o', pathString, data, onComplete);
3481 }
3482 else {
3483 this.onDisconnectRequestQueue_.push({
3484 pathString,
3485 action: 'o',
3486 data,
3487 onComplete
3488 });
3489 }
3490 }
3491 onDisconnectMerge(pathString, data, onComplete) {
3492 this.initConnection_();
3493 if (this.connected_) {
3494 this.sendOnDisconnect_('om', pathString, data, onComplete);
3495 }
3496 else {
3497 this.onDisconnectRequestQueue_.push({
3498 pathString,
3499 action: 'om',
3500 data,
3501 onComplete
3502 });
3503 }
3504 }
3505 onDisconnectCancel(pathString, onComplete) {
3506 this.initConnection_();
3507 if (this.connected_) {
3508 this.sendOnDisconnect_('oc', pathString, null, onComplete);
3509 }
3510 else {
3511 this.onDisconnectRequestQueue_.push({
3512 pathString,
3513 action: 'oc',
3514 data: null,
3515 onComplete
3516 });
3517 }
3518 }
3519 sendOnDisconnect_(action, pathString, data, onComplete) {
3520 const request = { /*path*/ p: pathString, /*data*/ d: data };
3521 this.log_('onDisconnect ' + action, request);
3522 this.sendRequest(action, request, (response) => {
3523 if (onComplete) {
3524 setTimeout(() => {
3525 onComplete(response[ /*status*/'s'], response[ /* data */'d']);
3526 }, Math.floor(0));
3527 }
3528 });
3529 }
3530 put(pathString, data, onComplete, hash) {
3531 this.putInternal('p', pathString, data, onComplete, hash);
3532 }
3533 merge(pathString, data, onComplete, hash) {
3534 this.putInternal('m', pathString, data, onComplete, hash);
3535 }
3536 putInternal(action, pathString, data, onComplete, hash) {
3537 this.initConnection_();
3538 const request = {
3539 /*path*/ p: pathString,
3540 /*data*/ d: data
3541 };
3542 if (hash !== undefined) {
3543 request[ /*hash*/'h'] = hash;
3544 }
3545 // TODO: Only keep track of the most recent put for a given path?
3546 this.outstandingPuts_.push({
3547 action,
3548 request,
3549 onComplete
3550 });
3551 this.outstandingPutCount_++;
3552 const index = this.outstandingPuts_.length - 1;
3553 if (this.connected_) {
3554 this.sendPut_(index);
3555 }
3556 else {
3557 this.log_('Buffering put: ' + pathString);
3558 }
3559 }
3560 sendPut_(index) {
3561 const action = this.outstandingPuts_[index].action;
3562 const request = this.outstandingPuts_[index].request;
3563 const onComplete = this.outstandingPuts_[index].onComplete;
3564 this.outstandingPuts_[index].queued = this.connected_;
3565 this.sendRequest(action, request, (message) => {
3566 this.log_(action + ' response', message);
3567 delete this.outstandingPuts_[index];
3568 this.outstandingPutCount_--;
3569 // Clean up array occasionally.
3570 if (this.outstandingPutCount_ === 0) {
3571 this.outstandingPuts_ = [];
3572 }
3573 if (onComplete) {
3574 onComplete(message[ /*status*/'s'], message[ /* data */'d']);
3575 }
3576 });
3577 }
3578 reportStats(stats) {
3579 // If we're not connected, we just drop the stats.
3580 if (this.connected_) {
3581 const request = { /*counters*/ c: stats };
3582 this.log_('reportStats', request);
3583 this.sendRequest(/*stats*/ 's', request, result => {
3584 const status = result[ /*status*/'s'];
3585 if (status !== 'ok') {
3586 const errorReason = result[ /* data */'d'];
3587 this.log_('reportStats', 'Error sending stats: ' + errorReason);
3588 }
3589 });
3590 }
3591 }
3592 onDataMessage_(message) {
3593 if ('r' in message) {
3594 // this is a response
3595 this.log_('from server: ' + stringify(message));
3596 const reqNum = message['r'];
3597 const onResponse = this.requestCBHash_[reqNum];
3598 if (onResponse) {
3599 delete this.requestCBHash_[reqNum];
3600 onResponse(message[ /*body*/'b']);
3601 }
3602 }
3603 else if ('error' in message) {
3604 throw 'A server-side error has occurred: ' + message['error'];
3605 }
3606 else if ('a' in message) {
3607 // a and b are action and body, respectively
3608 this.onDataPush_(message['a'], message['b']);
3609 }
3610 }
3611 onDataPush_(action, body) {
3612 this.log_('handleServerMessage', action, body);
3613 if (action === 'd') {
3614 this.onDataUpdate_(body[ /*path*/'p'], body[ /*data*/'d'],
3615 /*isMerge*/ false, body['t']);
3616 }
3617 else if (action === 'm') {
3618 this.onDataUpdate_(body[ /*path*/'p'], body[ /*data*/'d'],
3619 /*isMerge=*/ true, body['t']);
3620 }
3621 else if (action === 'c') {
3622 this.onListenRevoked_(body[ /*path*/'p'], body[ /*query*/'q']);
3623 }
3624 else if (action === 'ac') {
3625 this.onAuthRevoked_(body[ /*status code*/'s'], body[ /* explanation */'d']);
3626 }
3627 else if (action === 'apc') {
3628 this.onAppCheckRevoked_(body[ /*status code*/'s'], body[ /* explanation */'d']);
3629 }
3630 else if (action === 'sd') {
3631 this.onSecurityDebugPacket_(body);
3632 }
3633 else {
3634 error('Unrecognized action received from server: ' +
3635 stringify(action) +
3636 '\nAre you using the latest client?');
3637 }
3638 }
3639 onReady_(timestamp, sessionId) {
3640 this.log_('connection ready');
3641 this.connected_ = true;
3642 this.lastConnectionEstablishedTime_ = new Date().getTime();
3643 this.handleTimestamp_(timestamp);
3644 this.lastSessionId = sessionId;
3645 if (this.firstConnection_) {
3646 this.sendConnectStats_();
3647 }
3648 this.restoreState_();
3649 this.firstConnection_ = false;
3650 this.onConnectStatus_(true);
3651 }
3652 scheduleConnect_(timeout) {
3653 assert(!this.realtime_, "Scheduling a connect when we're already connected/ing?");
3654 if (this.establishConnectionTimer_) {
3655 clearTimeout(this.establishConnectionTimer_);
3656 }
3657 // NOTE: Even when timeout is 0, it's important to do a setTimeout to work around an infuriating "Security Error" in
3658 // Firefox when trying to write to our long-polling iframe in some scenarios (e.g. Forge or our unit tests).
3659 this.establishConnectionTimer_ = setTimeout(() => {
3660 this.establishConnectionTimer_ = null;
3661 this.establishConnection_();
3662 // eslint-disable-next-line @typescript-eslint/no-explicit-any
3663 }, Math.floor(timeout));
3664 }
3665 initConnection_() {
3666 if (!this.realtime_ && this.firstConnection_) {
3667 this.scheduleConnect_(0);
3668 }
3669 }
3670 onVisible_(visible) {
3671 // NOTE: Tabbing away and back to a window will defeat our reconnect backoff, but I think that's fine.
3672 if (visible &&
3673 !this.visible_ &&
3674 this.reconnectDelay_ === this.maxReconnectDelay_) {
3675 this.log_('Window became visible. Reducing delay.');
3676 this.reconnectDelay_ = RECONNECT_MIN_DELAY;
3677 if (!this.realtime_) {
3678 this.scheduleConnect_(0);
3679 }
3680 }
3681 this.visible_ = visible;
3682 }
3683 onOnline_(online) {
3684 if (online) {
3685 this.log_('Browser went online.');
3686 this.reconnectDelay_ = RECONNECT_MIN_DELAY;
3687 if (!this.realtime_) {
3688 this.scheduleConnect_(0);
3689 }
3690 }
3691 else {
3692 this.log_('Browser went offline. Killing connection.');
3693 if (this.realtime_) {
3694 this.realtime_.close();
3695 }
3696 }
3697 }
3698 onRealtimeDisconnect_() {
3699 this.log_('data client disconnected');
3700 this.connected_ = false;
3701 this.realtime_ = null;
3702 // Since we don't know if our sent transactions succeeded or not, we need to cancel them.
3703 this.cancelSentTransactions_();
3704 // Clear out the pending requests.
3705 this.requestCBHash_ = {};
3706 if (this.shouldReconnect_()) {
3707 if (!this.visible_) {
3708 this.log_("Window isn't visible. Delaying reconnect.");
3709 this.reconnectDelay_ = this.maxReconnectDelay_;
3710 this.lastConnectionAttemptTime_ = new Date().getTime();
3711 }
3712 else if (this.lastConnectionEstablishedTime_) {
3713 // If we've been connected long enough, reset reconnect delay to minimum.
3714 const timeSinceLastConnectSucceeded = new Date().getTime() - this.lastConnectionEstablishedTime_;
3715 if (timeSinceLastConnectSucceeded > RECONNECT_DELAY_RESET_TIMEOUT) {
3716 this.reconnectDelay_ = RECONNECT_MIN_DELAY;
3717 }
3718 this.lastConnectionEstablishedTime_ = null;
3719 }
3720 const timeSinceLastConnectAttempt = new Date().getTime() - this.lastConnectionAttemptTime_;
3721 let reconnectDelay = Math.max(0, this.reconnectDelay_ - timeSinceLastConnectAttempt);
3722 reconnectDelay = Math.random() * reconnectDelay;
3723 this.log_('Trying to reconnect in ' + reconnectDelay + 'ms');
3724 this.scheduleConnect_(reconnectDelay);
3725 // Adjust reconnect delay for next time.
3726 this.reconnectDelay_ = Math.min(this.maxReconnectDelay_, this.reconnectDelay_ * RECONNECT_DELAY_MULTIPLIER);
3727 }
3728 this.onConnectStatus_(false);
3729 }
3730 async establishConnection_() {
3731 if (this.shouldReconnect_()) {
3732 this.log_('Making a connection attempt');
3733 this.lastConnectionAttemptTime_ = new Date().getTime();
3734 this.lastConnectionEstablishedTime_ = null;
3735 const onDataMessage = this.onDataMessage_.bind(this);
3736 const onReady = this.onReady_.bind(this);
3737 const onDisconnect = this.onRealtimeDisconnect_.bind(this);
3738 const connId = this.id + ':' + PersistentConnection.nextConnectionId_++;
3739 const lastSessionId = this.lastSessionId;
3740 let canceled = false;
3741 let connection = null;
3742 const closeFn = function () {
3743 if (connection) {
3744 connection.close();
3745 }
3746 else {
3747 canceled = true;
3748 onDisconnect();
3749 }
3750 };
3751 const sendRequestFn = function (msg) {
3752 assert(connection, "sendRequest call when we're not connected not allowed.");
3753 connection.sendRequest(msg);
3754 };
3755 this.realtime_ = {
3756 close: closeFn,
3757 sendRequest: sendRequestFn
3758 };
3759 const forceRefresh = this.forceTokenRefresh_;
3760 this.forceTokenRefresh_ = false;
3761 try {
3762 // First fetch auth and app check token, and establish connection after
3763 // fetching the token was successful
3764 const [authToken, appCheckToken] = await Promise.all([
3765 this.authTokenProvider_.getToken(forceRefresh),
3766 this.appCheckTokenProvider_.getToken(forceRefresh)
3767 ]);
3768 if (!canceled) {
3769 log('getToken() completed. Creating connection.');
3770 this.authToken_ = authToken && authToken.accessToken;
3771 this.appCheckToken_ = appCheckToken && appCheckToken.token;
3772 connection = new Connection(connId, this.repoInfo_, this.applicationId_, this.appCheckToken_, this.authToken_, onDataMessage, onReady, onDisconnect,
3773 /* onKill= */ reason => {
3774 warn(reason + ' (' + this.repoInfo_.toString() + ')');
3775 this.interrupt(SERVER_KILL_INTERRUPT_REASON);
3776 }, lastSessionId);
3777 }
3778 else {
3779 log('getToken() completed but was canceled');
3780 }
3781 }
3782 catch (error) {
3783 this.log_('Failed to get token: ' + error);
3784 if (!canceled) {
3785 if (this.repoInfo_.nodeAdmin) {
3786 // This may be a critical error for the Admin Node.js SDK, so log a warning.
3787 // But getToken() may also just have temporarily failed, so we still want to
3788 // continue retrying.
3789 warn(error);
3790 }
3791 closeFn();
3792 }
3793 }
3794 }
3795 }
3796 interrupt(reason) {
3797 log('Interrupting connection for reason: ' + reason);
3798 this.interruptReasons_[reason] = true;
3799 if (this.realtime_) {
3800 this.realtime_.close();
3801 }
3802 else {
3803 if (this.establishConnectionTimer_) {
3804 clearTimeout(this.establishConnectionTimer_);
3805 this.establishConnectionTimer_ = null;
3806 }
3807 if (this.connected_) {
3808 this.onRealtimeDisconnect_();
3809 }
3810 }
3811 }
3812 resume(reason) {
3813 log('Resuming connection for reason: ' + reason);
3814 delete this.interruptReasons_[reason];
3815 if (isEmpty(this.interruptReasons_)) {
3816 this.reconnectDelay_ = RECONNECT_MIN_DELAY;
3817 if (!this.realtime_) {
3818 this.scheduleConnect_(0);
3819 }
3820 }
3821 }
3822 handleTimestamp_(timestamp) {
3823 const delta = timestamp - new Date().getTime();
3824 this.onServerInfoUpdate_({ serverTimeOffset: delta });
3825 }
3826 cancelSentTransactions_() {
3827 for (let i = 0; i < this.outstandingPuts_.length; i++) {
3828 const put = this.outstandingPuts_[i];
3829 if (put && /*hash*/ 'h' in put.request && put.queued) {
3830 if (put.onComplete) {
3831 put.onComplete('disconnect');
3832 }
3833 delete this.outstandingPuts_[i];
3834 this.outstandingPutCount_--;
3835 }
3836 }
3837 // Clean up array occasionally.
3838 if (this.outstandingPutCount_ === 0) {
3839 this.outstandingPuts_ = [];
3840 }
3841 }
3842 onListenRevoked_(pathString, query) {
3843 // Remove the listen and manufacture a "permission_denied" error for the failed listen.
3844 let queryId;
3845 if (!query) {
3846 queryId = 'default';
3847 }
3848 else {
3849 queryId = query.map(q => ObjectToUniqueKey(q)).join('$');
3850 }
3851 const listen = this.removeListen_(pathString, queryId);
3852 if (listen && listen.onComplete) {
3853 listen.onComplete('permission_denied');
3854 }
3855 }
3856 removeListen_(pathString, queryId) {
3857 const normalizedPathString = new Path(pathString).toString(); // normalize path.
3858 let listen;
3859 if (this.listens.has(normalizedPathString)) {
3860 const map = this.listens.get(normalizedPathString);
3861 listen = map.get(queryId);
3862 map.delete(queryId);
3863 if (map.size === 0) {
3864 this.listens.delete(normalizedPathString);
3865 }
3866 }
3867 else {
3868 // all listens for this path has already been removed
3869 listen = undefined;
3870 }
3871 return listen;
3872 }
3873 onAuthRevoked_(statusCode, explanation) {
3874 log('Auth token revoked: ' + statusCode + '/' + explanation);
3875 this.authToken_ = null;
3876 this.forceTokenRefresh_ = true;
3877 this.realtime_.close();
3878 if (statusCode === 'invalid_token' || statusCode === 'permission_denied') {
3879 // We'll wait a couple times before logging the warning / increasing the
3880 // retry period since oauth tokens will report as "invalid" if they're
3881 // just expired. Plus there may be transient issues that resolve themselves.
3882 this.invalidAuthTokenCount_++;
3883 if (this.invalidAuthTokenCount_ >= INVALID_TOKEN_THRESHOLD) {
3884 // Set a long reconnect delay because recovery is unlikely
3885 this.reconnectDelay_ = RECONNECT_MAX_DELAY_FOR_ADMINS;
3886 // Notify the auth token provider that the token is invalid, which will log
3887 // a warning
3888 this.authTokenProvider_.notifyForInvalidToken();
3889 }
3890 }
3891 }
3892 onAppCheckRevoked_(statusCode, explanation) {
3893 log('App check token revoked: ' + statusCode + '/' + explanation);
3894 this.appCheckToken_ = null;
3895 this.forceTokenRefresh_ = true;
3896 // Note: We don't close the connection as the developer may not have
3897 // enforcement enabled. The backend closes connections with enforcements.
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.invalidAppCheckTokenCount_++;
3903 if (this.invalidAppCheckTokenCount_ >= INVALID_TOKEN_THRESHOLD) {
3904 this.appCheckTokenProvider_.notifyForInvalidToken();
3905 }
3906 }
3907 }
3908 onSecurityDebugPacket_(body) {
3909 if (this.securityDebugCallback_) {
3910 this.securityDebugCallback_(body);
3911 }
3912 else {
3913 if ('msg' in body) {
3914 console.log('FIREBASE: ' + body['msg'].replace('\n', '\nFIREBASE: '));
3915 }
3916 }
3917 }
3918 restoreState_() {
3919 //Re-authenticate ourselves if we have a credential stored.
3920 this.tryAuth();
3921 this.tryAppCheck();
3922 // Puts depend on having received the corresponding data update from the server before they complete, so we must
3923 // make sure to send listens before puts.
3924 for (const queries of this.listens.values()) {
3925 for (const listenSpec of queries.values()) {
3926 this.sendListen_(listenSpec);
3927 }
3928 }
3929 for (let i = 0; i < this.outstandingPuts_.length; i++) {
3930 if (this.outstandingPuts_[i]) {
3931 this.sendPut_(i);
3932 }
3933 }
3934 while (this.onDisconnectRequestQueue_.length) {
3935 const request = this.onDisconnectRequestQueue_.shift();
3936 this.sendOnDisconnect_(request.action, request.pathString, request.data, request.onComplete);
3937 }
3938 for (let i = 0; i < this.outstandingGets_.length; i++) {
3939 if (this.outstandingGets_[i]) {
3940 this.sendGet_(i);
3941 }
3942 }
3943 }
3944 /**
3945 * Sends client stats for first connection
3946 */
3947 sendConnectStats_() {
3948 const stats = {};
3949 let clientName = 'js';
3950 if (isNodeSdk()) {
3951 if (this.repoInfo_.nodeAdmin) {
3952 clientName = 'admin_node';
3953 }
3954 else {
3955 clientName = 'node';
3956 }
3957 }
3958 stats['sdk.' + clientName + '.' + SDK_VERSION.replace(/\./g, '-')] = 1;
3959 if (isMobileCordova()) {
3960 stats['framework.cordova'] = 1;
3961 }
3962 else if (isReactNative()) {
3963 stats['framework.reactnative'] = 1;
3964 }
3965 this.reportStats(stats);
3966 }
3967 shouldReconnect_() {
3968 const online = OnlineMonitor.getInstance().currentlyOnline();
3969 return isEmpty(this.interruptReasons_) && online;
3970 }
3971}
3972PersistentConnection.nextPersistentConnectionId_ = 0;
3973/**
3974 * Counter for number of connections created. Mainly used for tagging in the logs
3975 */
3976PersistentConnection.nextConnectionId_ = 0;
3977
3978/**
3979 * @license
3980 * Copyright 2017 Google LLC
3981 *
3982 * Licensed under the Apache License, Version 2.0 (the "License");
3983 * you may not use this file except in compliance with the License.
3984 * You may obtain a copy of the License at
3985 *
3986 * http://www.apache.org/licenses/LICENSE-2.0
3987 *
3988 * Unless required by applicable law or agreed to in writing, software
3989 * distributed under the License is distributed on an "AS IS" BASIS,
3990 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3991 * See the License for the specific language governing permissions and
3992 * limitations under the License.
3993 */
3994class NamedNode {
3995 constructor(name, node) {
3996 this.name = name;
3997 this.node = node;
3998 }
3999 static Wrap(name, node) {
4000 return new NamedNode(name, node);
4001 }
4002}
4003
4004/**
4005 * @license
4006 * Copyright 2017 Google LLC
4007 *
4008 * Licensed under the Apache License, Version 2.0 (the "License");
4009 * you may not use this file except in compliance with the License.
4010 * You may obtain a copy of the License at
4011 *
4012 * http://www.apache.org/licenses/LICENSE-2.0
4013 *
4014 * Unless required by applicable law or agreed to in writing, software
4015 * distributed under the License is distributed on an "AS IS" BASIS,
4016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4017 * See the License for the specific language governing permissions and
4018 * limitations under the License.
4019 */
4020class Index {
4021 /**
4022 * @returns A standalone comparison function for
4023 * this index
4024 */
4025 getCompare() {
4026 return this.compare.bind(this);
4027 }
4028 /**
4029 * Given a before and after value for a node, determine if the indexed value has changed. Even if they are different,
4030 * it's possible that the changes are isolated to parts of the snapshot that are not indexed.
4031 *
4032 *
4033 * @returns True if the portion of the snapshot being indexed changed between oldNode and newNode
4034 */
4035 indexedValueChanged(oldNode, newNode) {
4036 const oldWrapped = new NamedNode(MIN_NAME, oldNode);
4037 const newWrapped = new NamedNode(MIN_NAME, newNode);
4038 return this.compare(oldWrapped, newWrapped) !== 0;
4039 }
4040 /**
4041 * @returns a node wrapper that will sort equal to or less than
4042 * any other node wrapper, using this index
4043 */
4044 minPost() {
4045 // eslint-disable-next-line @typescript-eslint/no-explicit-any
4046 return NamedNode.MIN;
4047 }
4048}
4049
4050/**
4051 * @license
4052 * Copyright 2017 Google LLC
4053 *
4054 * Licensed under the Apache License, Version 2.0 (the "License");
4055 * you may not use this file except in compliance with the License.
4056 * You may obtain a copy of the License at
4057 *
4058 * http://www.apache.org/licenses/LICENSE-2.0
4059 *
4060 * Unless required by applicable law or agreed to in writing, software
4061 * distributed under the License is distributed on an "AS IS" BASIS,
4062 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4063 * See the License for the specific language governing permissions and
4064 * limitations under the License.
4065 */
4066let __EMPTY_NODE;
4067class KeyIndex extends Index {
4068 static get __EMPTY_NODE() {
4069 return __EMPTY_NODE;
4070 }
4071 static set __EMPTY_NODE(val) {
4072 __EMPTY_NODE = val;
4073 }
4074 compare(a, b) {
4075 return nameCompare(a.name, b.name);
4076 }
4077 isDefinedOn(node) {
4078 // We could probably return true here (since every node has a key), but it's never called
4079 // so just leaving unimplemented for now.
4080 throw assertionError('KeyIndex.isDefinedOn not expected to be called.');
4081 }
4082 indexedValueChanged(oldNode, newNode) {
4083 return false; // The key for a node never changes.
4084 }
4085 minPost() {
4086 // eslint-disable-next-line @typescript-eslint/no-explicit-any
4087 return NamedNode.MIN;
4088 }
4089 maxPost() {
4090 // TODO: This should really be created once and cached in a static property, but
4091 // NamedNode isn't defined yet, so I can't use it in a static. Bleh.
4092 return new NamedNode(MAX_NAME, __EMPTY_NODE);
4093 }
4094 makePost(indexValue, name) {
4095 assert(typeof indexValue === 'string', 'KeyIndex indexValue must always be a string.');
4096 // We just use empty node, but it'll never be compared, since our comparator only looks at name.
4097 return new NamedNode(indexValue, __EMPTY_NODE);
4098 }
4099 /**
4100 * @returns String representation for inclusion in a query spec
4101 */
4102 toString() {
4103 return '.key';
4104 }
4105}
4106const KEY_INDEX = new KeyIndex();
4107
4108/**
4109 * @license
4110 * Copyright 2017 Google LLC
4111 *
4112 * Licensed under the Apache License, Version 2.0 (the "License");
4113 * you may not use this file except in compliance with the License.
4114 * You may obtain a copy of the License at
4115 *
4116 * http://www.apache.org/licenses/LICENSE-2.0
4117 *
4118 * Unless required by applicable law or agreed to in writing, software
4119 * distributed under the License is distributed on an "AS IS" BASIS,
4120 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4121 * See the License for the specific language governing permissions and
4122 * limitations under the License.
4123 */
4124/**
4125 * An iterator over an LLRBNode.
4126 */
4127class SortedMapIterator {
4128 /**
4129 * @param node - Node to iterate.
4130 * @param isReverse_ - Whether or not to iterate in reverse
4131 */
4132 constructor(node, startKey, comparator, isReverse_, resultGenerator_ = null) {
4133 this.isReverse_ = isReverse_;
4134 this.resultGenerator_ = resultGenerator_;
4135 this.nodeStack_ = [];
4136 let cmp = 1;
4137 while (!node.isEmpty()) {
4138 node = node;
4139 cmp = startKey ? comparator(node.key, startKey) : 1;
4140 // flip the comparison if we're going in reverse
4141 if (isReverse_) {
4142 cmp *= -1;
4143 }
4144 if (cmp < 0) {
4145 // This node is less than our start key. ignore it
4146 if (this.isReverse_) {
4147 node = node.left;
4148 }
4149 else {
4150 node = node.right;
4151 }
4152 }
4153 else if (cmp === 0) {
4154 // This node is exactly equal to our start key. Push it on the stack, but stop iterating;
4155 this.nodeStack_.push(node);
4156 break;
4157 }
4158 else {
4159 // This node is greater than our start key, add it to the stack and move to the next one
4160 this.nodeStack_.push(node);
4161 if (this.isReverse_) {
4162 node = node.right;
4163 }
4164 else {
4165 node = node.left;
4166 }
4167 }
4168 }
4169 }
4170 getNext() {
4171 if (this.nodeStack_.length === 0) {
4172 return null;
4173 }
4174 let node = this.nodeStack_.pop();
4175 let result;
4176 if (this.resultGenerator_) {
4177 result = this.resultGenerator_(node.key, node.value);
4178 }
4179 else {
4180 result = { key: node.key, value: node.value };
4181 }
4182 if (this.isReverse_) {
4183 node = node.left;
4184 while (!node.isEmpty()) {
4185 this.nodeStack_.push(node);
4186 node = node.right;
4187 }
4188 }
4189 else {
4190 node = node.right;
4191 while (!node.isEmpty()) {
4192 this.nodeStack_.push(node);
4193 node = node.left;
4194 }
4195 }
4196 return result;
4197 }
4198 hasNext() {
4199 return this.nodeStack_.length > 0;
4200 }
4201 peek() {
4202 if (this.nodeStack_.length === 0) {
4203 return null;
4204 }
4205 const node = this.nodeStack_[this.nodeStack_.length - 1];
4206 if (this.resultGenerator_) {
4207 return this.resultGenerator_(node.key, node.value);
4208 }
4209 else {
4210 return { key: node.key, value: node.value };
4211 }
4212 }
4213}
4214/**
4215 * Represents a node in a Left-leaning Red-Black tree.
4216 */
4217class LLRBNode {
4218 /**
4219 * @param key - Key associated with this node.
4220 * @param value - Value associated with this node.
4221 * @param color - Whether this node is red.
4222 * @param left - Left child.
4223 * @param right - Right child.
4224 */
4225 constructor(key, value, color, left, right) {
4226 this.key = key;
4227 this.value = value;
4228 this.color = color != null ? color : LLRBNode.RED;
4229 this.left =
4230 left != null ? left : SortedMap.EMPTY_NODE;
4231 this.right =
4232 right != null ? right : SortedMap.EMPTY_NODE;
4233 }
4234 /**
4235 * Returns a copy of the current node, optionally replacing pieces of it.
4236 *
4237 * @param key - New key for the node, or null.
4238 * @param value - New value for the node, or null.
4239 * @param color - New color for the node, or null.
4240 * @param left - New left child for the node, or null.
4241 * @param right - New right child for the node, or null.
4242 * @returns The node copy.
4243 */
4244 copy(key, value, color, left, right) {
4245 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);
4246 }
4247 /**
4248 * @returns The total number of nodes in the tree.
4249 */
4250 count() {
4251 return this.left.count() + 1 + this.right.count();
4252 }
4253 /**
4254 * @returns True if the tree is empty.
4255 */
4256 isEmpty() {
4257 return false;
4258 }
4259 /**
4260 * Traverses the tree in key order and calls the specified action function
4261 * for each node.
4262 *
4263 * @param action - Callback function to be called for each
4264 * node. If it returns true, traversal is aborted.
4265 * @returns The first truthy value returned by action, or the last falsey
4266 * value returned by action
4267 */
4268 inorderTraversal(action) {
4269 return (this.left.inorderTraversal(action) ||
4270 !!action(this.key, this.value) ||
4271 this.right.inorderTraversal(action));
4272 }
4273 /**
4274 * Traverses the tree in reverse key order and calls the specified action function
4275 * for each node.
4276 *
4277 * @param action - Callback function to be called for each
4278 * node. If it returns true, traversal is aborted.
4279 * @returns True if traversal was aborted.
4280 */
4281 reverseTraversal(action) {
4282 return (this.right.reverseTraversal(action) ||
4283 action(this.key, this.value) ||
4284 this.left.reverseTraversal(action));
4285 }
4286 /**
4287 * @returns The minimum node in the tree.
4288 */
4289 min_() {
4290 if (this.left.isEmpty()) {
4291 return this;
4292 }
4293 else {
4294 return this.left.min_();
4295 }
4296 }
4297 /**
4298 * @returns The maximum key in the tree.
4299 */
4300 minKey() {
4301 return this.min_().key;
4302 }
4303 /**
4304 * @returns The maximum key in the tree.
4305 */
4306 maxKey() {
4307 if (this.right.isEmpty()) {
4308 return this.key;
4309 }
4310 else {
4311 return this.right.maxKey();
4312 }
4313 }
4314 /**
4315 * @param key - Key to insert.
4316 * @param value - Value to insert.
4317 * @param comparator - Comparator.
4318 * @returns New tree, with the key/value added.
4319 */
4320 insert(key, value, comparator) {
4321 let n = this;
4322 const cmp = comparator(key, n.key);
4323 if (cmp < 0) {
4324 n = n.copy(null, null, null, n.left.insert(key, value, comparator), null);
4325 }
4326 else if (cmp === 0) {
4327 n = n.copy(null, value, null, null, null);
4328 }
4329 else {
4330 n = n.copy(null, null, null, null, n.right.insert(key, value, comparator));
4331 }
4332 return n.fixUp_();
4333 }
4334 /**
4335 * @returns New tree, with the minimum key removed.
4336 */
4337 removeMin_() {
4338 if (this.left.isEmpty()) {
4339 return SortedMap.EMPTY_NODE;
4340 }
4341 let n = this;
4342 if (!n.left.isRed_() && !n.left.left.isRed_()) {
4343 n = n.moveRedLeft_();
4344 }
4345 n = n.copy(null, null, null, n.left.removeMin_(), null);
4346 return n.fixUp_();
4347 }
4348 /**
4349 * @param key - The key of the item to remove.
4350 * @param comparator - Comparator.
4351 * @returns New tree, with the specified item removed.
4352 */
4353 remove(key, comparator) {
4354 let n, smallest;
4355 n = this;
4356 if (comparator(key, n.key) < 0) {
4357 if (!n.left.isEmpty() && !n.left.isRed_() && !n.left.left.isRed_()) {
4358 n = n.moveRedLeft_();
4359 }
4360 n = n.copy(null, null, null, n.left.remove(key, comparator), null);
4361 }
4362 else {
4363 if (n.left.isRed_()) {
4364 n = n.rotateRight_();
4365 }
4366 if (!n.right.isEmpty() && !n.right.isRed_() && !n.right.left.isRed_()) {
4367 n = n.moveRedRight_();
4368 }
4369 if (comparator(key, n.key) === 0) {
4370 if (n.right.isEmpty()) {
4371 return SortedMap.EMPTY_NODE;
4372 }
4373 else {
4374 smallest = n.right.min_();
4375 n = n.copy(smallest.key, smallest.value, null, null, n.right.removeMin_());
4376 }
4377 }
4378 n = n.copy(null, null, null, null, n.right.remove(key, comparator));
4379 }
4380 return n.fixUp_();
4381 }
4382 /**
4383 * @returns Whether this is a RED node.
4384 */
4385 isRed_() {
4386 return this.color;
4387 }
4388 /**
4389 * @returns New tree after performing any needed rotations.
4390 */
4391 fixUp_() {
4392 let n = this;
4393 if (n.right.isRed_() && !n.left.isRed_()) {
4394 n = n.rotateLeft_();
4395 }
4396 if (n.left.isRed_() && n.left.left.isRed_()) {
4397 n = n.rotateRight_();
4398 }
4399 if (n.left.isRed_() && n.right.isRed_()) {
4400 n = n.colorFlip_();
4401 }
4402 return n;
4403 }
4404 /**
4405 * @returns New tree, after moveRedLeft.
4406 */
4407 moveRedLeft_() {
4408 let n = this.colorFlip_();
4409 if (n.right.left.isRed_()) {
4410 n = n.copy(null, null, null, null, n.right.rotateRight_());
4411 n = n.rotateLeft_();
4412 n = n.colorFlip_();
4413 }
4414 return n;
4415 }
4416 /**
4417 * @returns New tree, after moveRedRight.
4418 */
4419 moveRedRight_() {
4420 let n = this.colorFlip_();
4421 if (n.left.left.isRed_()) {
4422 n = n.rotateRight_();
4423 n = n.colorFlip_();
4424 }
4425 return n;
4426 }
4427 /**
4428 * @returns New tree, after rotateLeft.
4429 */
4430 rotateLeft_() {
4431 const nl = this.copy(null, null, LLRBNode.RED, null, this.right.left);
4432 return this.right.copy(null, null, this.color, nl, null);
4433 }
4434 /**
4435 * @returns New tree, after rotateRight.
4436 */
4437 rotateRight_() {
4438 const nr = this.copy(null, null, LLRBNode.RED, this.left.right, null);
4439 return this.left.copy(null, null, this.color, null, nr);
4440 }
4441 /**
4442 * @returns Newt ree, after colorFlip.
4443 */
4444 colorFlip_() {
4445 const left = this.left.copy(null, null, !this.left.color, null, null);
4446 const right = this.right.copy(null, null, !this.right.color, null, null);
4447 return this.copy(null, null, !this.color, left, right);
4448 }
4449 /**
4450 * For testing.
4451 *
4452 * @returns True if all is well.
4453 */
4454 checkMaxDepth_() {
4455 const blackDepth = this.check_();
4456 return Math.pow(2.0, blackDepth) <= this.count() + 1;
4457 }
4458 check_() {
4459 if (this.isRed_() && this.left.isRed_()) {
4460 throw new Error('Red node has red child(' + this.key + ',' + this.value + ')');
4461 }
4462 if (this.right.isRed_()) {
4463 throw new Error('Right child of (' + this.key + ',' + this.value + ') is red');
4464 }
4465 const blackDepth = this.left.check_();
4466 if (blackDepth !== this.right.check_()) {
4467 throw new Error('Black depths differ');
4468 }
4469 else {
4470 return blackDepth + (this.isRed_() ? 0 : 1);
4471 }
4472 }
4473}
4474LLRBNode.RED = true;
4475LLRBNode.BLACK = false;
4476/**
4477 * Represents an empty node (a leaf node in the Red-Black Tree).
4478 */
4479class LLRBEmptyNode {
4480 /**
4481 * Returns a copy of the current node.
4482 *
4483 * @returns The node copy.
4484 */
4485 copy(key, value, color, left, right) {
4486 return this;
4487 }
4488 /**
4489 * Returns a copy of the tree, with the specified key/value added.
4490 *
4491 * @param key - Key to be added.
4492 * @param value - Value to be added.
4493 * @param comparator - Comparator.
4494 * @returns New tree, with item added.
4495 */
4496 insert(key, value, comparator) {
4497 return new LLRBNode(key, value, null);
4498 }
4499 /**
4500 * Returns a copy of the tree, with the specified key removed.
4501 *
4502 * @param key - The key to remove.
4503 * @param comparator - Comparator.
4504 * @returns New tree, with item removed.
4505 */
4506 remove(key, comparator) {
4507 return this;
4508 }
4509 /**
4510 * @returns The total number of nodes in the tree.
4511 */
4512 count() {
4513 return 0;
4514 }
4515 /**
4516 * @returns True if the tree is empty.
4517 */
4518 isEmpty() {
4519 return true;
4520 }
4521 /**
4522 * Traverses the tree in key order and calls the specified action function
4523 * for each node.
4524 *
4525 * @param action - Callback function to be called for each
4526 * node. If it returns true, traversal is aborted.
4527 * @returns True if traversal was aborted.
4528 */
4529 inorderTraversal(action) {
4530 return false;
4531 }
4532 /**
4533 * Traverses the tree in reverse key order and calls the specified action function
4534 * for each node.
4535 *
4536 * @param action - Callback function to be called for each
4537 * node. If it returns true, traversal is aborted.
4538 * @returns True if traversal was aborted.
4539 */
4540 reverseTraversal(action) {
4541 return false;
4542 }
4543 minKey() {
4544 return null;
4545 }
4546 maxKey() {
4547 return null;
4548 }
4549 check_() {
4550 return 0;
4551 }
4552 /**
4553 * @returns Whether this node is red.
4554 */
4555 isRed_() {
4556 return false;
4557 }
4558}
4559/**
4560 * An immutable sorted map implementation, based on a Left-leaning Red-Black
4561 * tree.
4562 */
4563class SortedMap {
4564 /**
4565 * @param comparator_ - Key comparator.
4566 * @param root_ - Optional root node for the map.
4567 */
4568 constructor(comparator_, root_ = SortedMap.EMPTY_NODE) {
4569 this.comparator_ = comparator_;
4570 this.root_ = root_;
4571 }
4572 /**
4573 * Returns a copy of the map, with the specified key/value added or replaced.
4574 * (TODO: We should perhaps rename this method to 'put')
4575 *
4576 * @param key - Key to be added.
4577 * @param value - Value to be added.
4578 * @returns New map, with item added.
4579 */
4580 insert(key, value) {
4581 return new SortedMap(this.comparator_, this.root_
4582 .insert(key, value, this.comparator_)
4583 .copy(null, null, LLRBNode.BLACK, null, null));
4584 }
4585 /**
4586 * Returns a copy of the map, with the specified key removed.
4587 *
4588 * @param key - The key to remove.
4589 * @returns New map, with item removed.
4590 */
4591 remove(key) {
4592 return new SortedMap(this.comparator_, this.root_
4593 .remove(key, this.comparator_)
4594 .copy(null, null, LLRBNode.BLACK, null, null));
4595 }
4596 /**
4597 * Returns the value of the node with the given key, or null.
4598 *
4599 * @param key - The key to look up.
4600 * @returns The value of the node with the given key, or null if the
4601 * key doesn't exist.
4602 */
4603 get(key) {
4604 let cmp;
4605 let node = this.root_;
4606 while (!node.isEmpty()) {
4607 cmp = this.comparator_(key, node.key);
4608 if (cmp === 0) {
4609 return node.value;
4610 }
4611 else if (cmp < 0) {
4612 node = node.left;
4613 }
4614 else if (cmp > 0) {
4615 node = node.right;
4616 }
4617 }
4618 return null;
4619 }
4620 /**
4621 * Returns the key of the item *before* the specified key, or null if key is the first item.
4622 * @param key - The key to find the predecessor of
4623 * @returns The predecessor key.
4624 */
4625 getPredecessorKey(key) {
4626 let cmp, node = this.root_, rightParent = null;
4627 while (!node.isEmpty()) {
4628 cmp = this.comparator_(key, node.key);
4629 if (cmp === 0) {
4630 if (!node.left.isEmpty()) {
4631 node = node.left;
4632 while (!node.right.isEmpty()) {
4633 node = node.right;
4634 }
4635 return node.key;
4636 }
4637 else if (rightParent) {
4638 return rightParent.key;
4639 }
4640 else {
4641 return null; // first item.
4642 }
4643 }
4644 else if (cmp < 0) {
4645 node = node.left;
4646 }
4647 else if (cmp > 0) {
4648 rightParent = node;
4649 node = node.right;
4650 }
4651 }
4652 throw new Error('Attempted to find predecessor key for a nonexistent key. What gives?');
4653 }
4654 /**
4655 * @returns True if the map is empty.
4656 */
4657 isEmpty() {
4658 return this.root_.isEmpty();
4659 }
4660 /**
4661 * @returns The total number of nodes in the map.
4662 */
4663 count() {
4664 return this.root_.count();
4665 }
4666 /**
4667 * @returns The minimum key in the map.
4668 */
4669 minKey() {
4670 return this.root_.minKey();
4671 }
4672 /**
4673 * @returns The maximum key in the map.
4674 */
4675 maxKey() {
4676 return this.root_.maxKey();
4677 }
4678 /**
4679 * Traverses the map in key order and calls the specified action function
4680 * for each key/value pair.
4681 *
4682 * @param action - Callback function to be called
4683 * for each key/value pair. If action returns true, traversal is aborted.
4684 * @returns The first truthy value returned by action, or the last falsey
4685 * value returned by action
4686 */
4687 inorderTraversal(action) {
4688 return this.root_.inorderTraversal(action);
4689 }
4690 /**
4691 * Traverses the map in reverse key order and calls the specified action function
4692 * for each key/value pair.
4693 *
4694 * @param action - Callback function to be called
4695 * for each key/value pair. If action returns true, traversal is aborted.
4696 * @returns True if the traversal was aborted.
4697 */
4698 reverseTraversal(action) {
4699 return this.root_.reverseTraversal(action);
4700 }
4701 /**
4702 * Returns an iterator over the SortedMap.
4703 * @returns The iterator.
4704 */
4705 getIterator(resultGenerator) {
4706 return new SortedMapIterator(this.root_, null, this.comparator_, false, resultGenerator);
4707 }
4708 getIteratorFrom(key, resultGenerator) {
4709 return new SortedMapIterator(this.root_, key, this.comparator_, false, resultGenerator);
4710 }
4711 getReverseIteratorFrom(key, resultGenerator) {
4712 return new SortedMapIterator(this.root_, key, this.comparator_, true, resultGenerator);
4713 }
4714 getReverseIterator(resultGenerator) {
4715 return new SortedMapIterator(this.root_, null, this.comparator_, true, resultGenerator);
4716 }
4717}
4718/**
4719 * Always use the same empty node, to reduce memory.
4720 */
4721SortedMap.EMPTY_NODE = new LLRBEmptyNode();
4722
4723/**
4724 * @license
4725 * Copyright 2017 Google LLC
4726 *
4727 * Licensed under the Apache License, Version 2.0 (the "License");
4728 * you may not use this file except in compliance with the License.
4729 * You may obtain a copy of the License at
4730 *
4731 * http://www.apache.org/licenses/LICENSE-2.0
4732 *
4733 * Unless required by applicable law or agreed to in writing, software
4734 * distributed under the License is distributed on an "AS IS" BASIS,
4735 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4736 * See the License for the specific language governing permissions and
4737 * limitations under the License.
4738 */
4739function NAME_ONLY_COMPARATOR(left, right) {
4740 return nameCompare(left.name, right.name);
4741}
4742function NAME_COMPARATOR(left, right) {
4743 return nameCompare(left, right);
4744}
4745
4746/**
4747 * @license
4748 * Copyright 2017 Google LLC
4749 *
4750 * Licensed under the Apache License, Version 2.0 (the "License");
4751 * you may not use this file except in compliance with the License.
4752 * You may obtain a copy of the License at
4753 *
4754 * http://www.apache.org/licenses/LICENSE-2.0
4755 *
4756 * Unless required by applicable law or agreed to in writing, software
4757 * distributed under the License is distributed on an "AS IS" BASIS,
4758 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4759 * See the License for the specific language governing permissions and
4760 * limitations under the License.
4761 */
4762let MAX_NODE$2;
4763function setMaxNode$1(val) {
4764 MAX_NODE$2 = val;
4765}
4766const priorityHashText = function (priority) {
4767 if (typeof priority === 'number') {
4768 return 'number:' + doubleToIEEE754String(priority);
4769 }
4770 else {
4771 return 'string:' + priority;
4772 }
4773};
4774/**
4775 * Validates that a priority snapshot Node is valid.
4776 */
4777const validatePriorityNode = function (priorityNode) {
4778 if (priorityNode.isLeafNode()) {
4779 const val = priorityNode.val();
4780 assert(typeof val === 'string' ||
4781 typeof val === 'number' ||
4782 (typeof val === 'object' && contains(val, '.sv')), 'Priority must be a string or number.');
4783 }
4784 else {
4785 assert(priorityNode === MAX_NODE$2 || priorityNode.isEmpty(), 'priority of unexpected type.');
4786 }
4787 // Don't call getPriority() on MAX_NODE to avoid hitting assertion.
4788 assert(priorityNode === MAX_NODE$2 || priorityNode.getPriority().isEmpty(), "Priority nodes can't have a priority of their own.");
4789};
4790
4791/**
4792 * @license
4793 * Copyright 2017 Google LLC
4794 *
4795 * Licensed under the Apache License, Version 2.0 (the "License");
4796 * you may not use this file except in compliance with the License.
4797 * You may obtain a copy of the License at
4798 *
4799 * http://www.apache.org/licenses/LICENSE-2.0
4800 *
4801 * Unless required by applicable law or agreed to in writing, software
4802 * distributed under the License is distributed on an "AS IS" BASIS,
4803 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4804 * See the License for the specific language governing permissions and
4805 * limitations under the License.
4806 */
4807let __childrenNodeConstructor;
4808/**
4809 * LeafNode is a class for storing leaf nodes in a DataSnapshot. It
4810 * implements Node and stores the value of the node (a string,
4811 * number, or boolean) accessible via getValue().
4812 */
4813class LeafNode {
4814 /**
4815 * @param value_ - The value to store in this leaf node. The object type is
4816 * possible in the event of a deferred value
4817 * @param priorityNode_ - The priority of this node.
4818 */
4819 constructor(value_, priorityNode_ = LeafNode.__childrenNodeConstructor.EMPTY_NODE) {
4820 this.value_ = value_;
4821 this.priorityNode_ = priorityNode_;
4822 this.lazyHash_ = null;
4823 assert(this.value_ !== undefined && this.value_ !== null, "LeafNode shouldn't be created with null/undefined value.");
4824 validatePriorityNode(this.priorityNode_);
4825 }
4826 static set __childrenNodeConstructor(val) {
4827 __childrenNodeConstructor = val;
4828 }
4829 static get __childrenNodeConstructor() {
4830 return __childrenNodeConstructor;
4831 }
4832 /** @inheritDoc */
4833 isLeafNode() {
4834 return true;
4835 }
4836 /** @inheritDoc */
4837 getPriority() {
4838 return this.priorityNode_;
4839 }
4840 /** @inheritDoc */
4841 updatePriority(newPriorityNode) {
4842 return new LeafNode(this.value_, newPriorityNode);
4843 }
4844 /** @inheritDoc */
4845 getImmediateChild(childName) {
4846 // Hack to treat priority as a regular child
4847 if (childName === '.priority') {
4848 return this.priorityNode_;
4849 }
4850 else {
4851 return LeafNode.__childrenNodeConstructor.EMPTY_NODE;
4852 }
4853 }
4854 /** @inheritDoc */
4855 getChild(path) {
4856 if (pathIsEmpty(path)) {
4857 return this;
4858 }
4859 else if (pathGetFront(path) === '.priority') {
4860 return this.priorityNode_;
4861 }
4862 else {
4863 return LeafNode.__childrenNodeConstructor.EMPTY_NODE;
4864 }
4865 }
4866 hasChild() {
4867 return false;
4868 }
4869 /** @inheritDoc */
4870 getPredecessorChildName(childName, childNode) {
4871 return null;
4872 }
4873 /** @inheritDoc */
4874 updateImmediateChild(childName, newChildNode) {
4875 if (childName === '.priority') {
4876 return this.updatePriority(newChildNode);
4877 }
4878 else if (newChildNode.isEmpty() && childName !== '.priority') {
4879 return this;
4880 }
4881 else {
4882 return LeafNode.__childrenNodeConstructor.EMPTY_NODE.updateImmediateChild(childName, newChildNode).updatePriority(this.priorityNode_);
4883 }
4884 }
4885 /** @inheritDoc */
4886 updateChild(path, newChildNode) {
4887 const front = pathGetFront(path);
4888 if (front === null) {
4889 return newChildNode;
4890 }
4891 else if (newChildNode.isEmpty() && front !== '.priority') {
4892 return this;
4893 }
4894 else {
4895 assert(front !== '.priority' || pathGetLength(path) === 1, '.priority must be the last token in a path');
4896 return this.updateImmediateChild(front, LeafNode.__childrenNodeConstructor.EMPTY_NODE.updateChild(pathPopFront(path), newChildNode));
4897 }
4898 }
4899 /** @inheritDoc */
4900 isEmpty() {
4901 return false;
4902 }
4903 /** @inheritDoc */
4904 numChildren() {
4905 return 0;
4906 }
4907 /** @inheritDoc */
4908 forEachChild(index, action) {
4909 return false;
4910 }
4911 val(exportFormat) {
4912 if (exportFormat && !this.getPriority().isEmpty()) {
4913 return {
4914 '.value': this.getValue(),
4915 '.priority': this.getPriority().val()
4916 };
4917 }
4918 else {
4919 return this.getValue();
4920 }
4921 }
4922 /** @inheritDoc */
4923 hash() {
4924 if (this.lazyHash_ === null) {
4925 let toHash = '';
4926 if (!this.priorityNode_.isEmpty()) {
4927 toHash +=
4928 'priority:' +
4929 priorityHashText(this.priorityNode_.val()) +
4930 ':';
4931 }
4932 const type = typeof this.value_;
4933 toHash += type + ':';
4934 if (type === 'number') {
4935 toHash += doubleToIEEE754String(this.value_);
4936 }
4937 else {
4938 toHash += this.value_;
4939 }
4940 this.lazyHash_ = sha1(toHash);
4941 }
4942 return this.lazyHash_;
4943 }
4944 /**
4945 * Returns the value of the leaf node.
4946 * @returns The value of the node.
4947 */
4948 getValue() {
4949 return this.value_;
4950 }
4951 compareTo(other) {
4952 if (other === LeafNode.__childrenNodeConstructor.EMPTY_NODE) {
4953 return 1;
4954 }
4955 else if (other instanceof LeafNode.__childrenNodeConstructor) {
4956 return -1;
4957 }
4958 else {
4959 assert(other.isLeafNode(), 'Unknown node type');
4960 return this.compareToLeafNode_(other);
4961 }
4962 }
4963 /**
4964 * Comparison specifically for two leaf nodes
4965 */
4966 compareToLeafNode_(otherLeaf) {
4967 const otherLeafType = typeof otherLeaf.value_;
4968 const thisLeafType = typeof this.value_;
4969 const otherIndex = LeafNode.VALUE_TYPE_ORDER.indexOf(otherLeafType);
4970 const thisIndex = LeafNode.VALUE_TYPE_ORDER.indexOf(thisLeafType);
4971 assert(otherIndex >= 0, 'Unknown leaf type: ' + otherLeafType);
4972 assert(thisIndex >= 0, 'Unknown leaf type: ' + thisLeafType);
4973 if (otherIndex === thisIndex) {
4974 // Same type, compare values
4975 if (thisLeafType === 'object') {
4976 // Deferred value nodes are all equal, but we should also never get to this point...
4977 return 0;
4978 }
4979 else {
4980 // Note that this works because true > false, all others are number or string comparisons
4981 if (this.value_ < otherLeaf.value_) {
4982 return -1;
4983 }
4984 else if (this.value_ === otherLeaf.value_) {
4985 return 0;
4986 }
4987 else {
4988 return 1;
4989 }
4990 }
4991 }
4992 else {
4993 return thisIndex - otherIndex;
4994 }
4995 }
4996 withIndex() {
4997 return this;
4998 }
4999 isIndexed() {
5000 return true;
5001 }
5002 equals(other) {
5003 if (other === this) {
5004 return true;
5005 }
5006 else if (other.isLeafNode()) {
5007 const otherLeaf = other;
5008 return (this.value_ === otherLeaf.value_ &&
5009 this.priorityNode_.equals(otherLeaf.priorityNode_));
5010 }
5011 else {
5012 return false;
5013 }
5014 }
5015}
5016/**
5017 * The sort order for comparing leaf nodes of different types. If two leaf nodes have
5018 * the same type, the comparison falls back to their value
5019 */
5020LeafNode.VALUE_TYPE_ORDER = ['object', 'boolean', 'number', 'string'];
5021
5022/**
5023 * @license
5024 * Copyright 2017 Google LLC
5025 *
5026 * Licensed under the Apache License, Version 2.0 (the "License");
5027 * you may not use this file except in compliance with the License.
5028 * You may obtain a copy of the License at
5029 *
5030 * http://www.apache.org/licenses/LICENSE-2.0
5031 *
5032 * Unless required by applicable law or agreed to in writing, software
5033 * distributed under the License is distributed on an "AS IS" BASIS,
5034 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
5035 * See the License for the specific language governing permissions and
5036 * limitations under the License.
5037 */
5038let nodeFromJSON$1;
5039let MAX_NODE$1;
5040function setNodeFromJSON(val) {
5041 nodeFromJSON$1 = val;
5042}
5043function setMaxNode(val) {
5044 MAX_NODE$1 = val;
5045}
5046class PriorityIndex extends Index {
5047 compare(a, b) {
5048 const aPriority = a.node.getPriority();
5049 const bPriority = b.node.getPriority();
5050 const indexCmp = aPriority.compareTo(bPriority);
5051 if (indexCmp === 0) {
5052 return nameCompare(a.name, b.name);
5053 }
5054 else {
5055 return indexCmp;
5056 }
5057 }
5058 isDefinedOn(node) {
5059 return !node.getPriority().isEmpty();
5060 }
5061 indexedValueChanged(oldNode, newNode) {
5062 return !oldNode.getPriority().equals(newNode.getPriority());
5063 }
5064 minPost() {
5065 // eslint-disable-next-line @typescript-eslint/no-explicit-any
5066 return NamedNode.MIN;
5067 }
5068 maxPost() {
5069 return new NamedNode(MAX_NAME, new LeafNode('[PRIORITY-POST]', MAX_NODE$1));
5070 }
5071 makePost(indexValue, name) {
5072 const priorityNode = nodeFromJSON$1(indexValue);
5073 return new NamedNode(name, new LeafNode('[PRIORITY-POST]', priorityNode));
5074 }
5075 /**
5076 * @returns String representation for inclusion in a query spec
5077 */
5078 toString() {
5079 return '.priority';
5080 }
5081}
5082const PRIORITY_INDEX = new PriorityIndex();
5083
5084/**
5085 * @license
5086 * Copyright 2017 Google LLC
5087 *
5088 * Licensed under the Apache License, Version 2.0 (the "License");
5089 * you may not use this file except in compliance with the License.
5090 * You may obtain a copy of the License at
5091 *
5092 * http://www.apache.org/licenses/LICENSE-2.0
5093 *
5094 * Unless required by applicable law or agreed to in writing, software
5095 * distributed under the License is distributed on an "AS IS" BASIS,
5096 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
5097 * See the License for the specific language governing permissions and
5098 * limitations under the License.
5099 */
5100const LOG_2 = Math.log(2);
5101class Base12Num {
5102 constructor(length) {
5103 const logBase2 = (num) =>
5104 // eslint-disable-next-line @typescript-eslint/no-explicit-any
5105 parseInt((Math.log(num) / LOG_2), 10);
5106 const bitMask = (bits) => parseInt(Array(bits + 1).join('1'), 2);
5107 this.count = logBase2(length + 1);
5108 this.current_ = this.count - 1;
5109 const mask = bitMask(this.count);
5110 this.bits_ = (length + 1) & mask;
5111 }
5112 nextBitIsOne() {
5113 //noinspection JSBitwiseOperatorUsage
5114 const result = !(this.bits_ & (0x1 << this.current_));
5115 this.current_--;
5116 return result;
5117 }
5118}
5119/**
5120 * Takes a list of child nodes and constructs a SortedSet using the given comparison
5121 * function
5122 *
5123 * Uses the algorithm described in the paper linked here:
5124 * http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.46.1458
5125 *
5126 * @param childList - Unsorted list of children
5127 * @param cmp - The comparison method to be used
5128 * @param keyFn - An optional function to extract K from a node wrapper, if K's
5129 * type is not NamedNode
5130 * @param mapSortFn - An optional override for comparator used by the generated sorted map
5131 */
5132const buildChildSet = function (childList, cmp, keyFn, mapSortFn) {
5133 childList.sort(cmp);
5134 const buildBalancedTree = function (low, high) {
5135 const length = high - low;
5136 let namedNode;
5137 let key;
5138 if (length === 0) {
5139 return null;
5140 }
5141 else if (length === 1) {
5142 namedNode = childList[low];
5143 key = keyFn ? keyFn(namedNode) : namedNode;
5144 return new LLRBNode(key, namedNode.node, LLRBNode.BLACK, null, null);
5145 }
5146 else {
5147 // eslint-disable-next-line @typescript-eslint/no-explicit-any
5148 const middle = parseInt((length / 2), 10) + low;
5149 const left = buildBalancedTree(low, middle);
5150 const right = buildBalancedTree(middle + 1, high);
5151 namedNode = childList[middle];
5152 key = keyFn ? keyFn(namedNode) : namedNode;
5153 return new LLRBNode(key, namedNode.node, LLRBNode.BLACK, left, right);
5154 }
5155 };
5156 const buildFrom12Array = function (base12) {
5157 let node = null;
5158 let root = null;
5159 let index = childList.length;
5160 const buildPennant = function (chunkSize, color) {
5161 const low = index - chunkSize;
5162 const high = index;
5163 index -= chunkSize;
5164 const childTree = buildBalancedTree(low + 1, high);
5165 const namedNode = childList[low];
5166 const key = keyFn ? keyFn(namedNode) : namedNode;
5167 attachPennant(new LLRBNode(key, namedNode.node, color, null, childTree));
5168 };
5169 const attachPennant = function (pennant) {
5170 if (node) {
5171 node.left = pennant;
5172 node = pennant;
5173 }
5174 else {
5175 root = pennant;
5176 node = pennant;
5177 }
5178 };
5179 for (let i = 0; i < base12.count; ++i) {
5180 const isOne = base12.nextBitIsOne();
5181 // The number of nodes taken in each slice is 2^(arr.length - (i + 1))
5182 const chunkSize = Math.pow(2, base12.count - (i + 1));
5183 if (isOne) {
5184 buildPennant(chunkSize, LLRBNode.BLACK);
5185 }
5186 else {
5187 // current == 2
5188 buildPennant(chunkSize, LLRBNode.BLACK);
5189 buildPennant(chunkSize, LLRBNode.RED);
5190 }
5191 }
5192 return root;
5193 };
5194 const base12 = new Base12Num(childList.length);
5195 const root = buildFrom12Array(base12);
5196 // eslint-disable-next-line @typescript-eslint/no-explicit-any
5197 return new SortedMap(mapSortFn || cmp, root);
5198};
5199
5200/**
5201 * @license
5202 * Copyright 2017 Google LLC
5203 *
5204 * Licensed under the Apache License, Version 2.0 (the "License");
5205 * you may not use this file except in compliance with the License.
5206 * You may obtain a copy of the License at
5207 *
5208 * http://www.apache.org/licenses/LICENSE-2.0
5209 *
5210 * Unless required by applicable law or agreed to in writing, software
5211 * distributed under the License is distributed on an "AS IS" BASIS,
5212 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
5213 * See the License for the specific language governing permissions and
5214 * limitations under the License.
5215 */
5216let _defaultIndexMap;
5217const fallbackObject = {};
5218class IndexMap {
5219 constructor(indexes_, indexSet_) {
5220 this.indexes_ = indexes_;
5221 this.indexSet_ = indexSet_;
5222 }
5223 /**
5224 * The default IndexMap for nodes without a priority
5225 */
5226 static get Default() {
5227 assert(fallbackObject && PRIORITY_INDEX, 'ChildrenNode.ts has not been loaded');
5228 _defaultIndexMap =
5229 _defaultIndexMap ||
5230 new IndexMap({ '.priority': fallbackObject }, { '.priority': PRIORITY_INDEX });
5231 return _defaultIndexMap;
5232 }
5233 get(indexKey) {
5234 const sortedMap = safeGet(this.indexes_, indexKey);
5235 if (!sortedMap) {
5236 throw new Error('No index defined for ' + indexKey);
5237 }
5238 if (sortedMap instanceof SortedMap) {
5239 return sortedMap;
5240 }
5241 else {
5242 // The index exists, but it falls back to just name comparison. Return null so that the calling code uses the
5243 // regular child map
5244 return null;
5245 }
5246 }
5247 hasIndex(indexDefinition) {
5248 return contains(this.indexSet_, indexDefinition.toString());
5249 }
5250 addIndex(indexDefinition, existingChildren) {
5251 assert(indexDefinition !== KEY_INDEX, "KeyIndex always exists and isn't meant to be added to the IndexMap.");
5252 const childList = [];
5253 let sawIndexedValue = false;
5254 const iter = existingChildren.getIterator(NamedNode.Wrap);
5255 let next = iter.getNext();
5256 while (next) {
5257 sawIndexedValue =
5258 sawIndexedValue || indexDefinition.isDefinedOn(next.node);
5259 childList.push(next);
5260 next = iter.getNext();
5261 }
5262 let newIndex;
5263 if (sawIndexedValue) {
5264 newIndex = buildChildSet(childList, indexDefinition.getCompare());
5265 }
5266 else {
5267 newIndex = fallbackObject;
5268 }
5269 const indexName = indexDefinition.toString();
5270 const newIndexSet = Object.assign({}, this.indexSet_);
5271 newIndexSet[indexName] = indexDefinition;
5272 const newIndexes = Object.assign({}, this.indexes_);
5273 newIndexes[indexName] = newIndex;
5274 return new IndexMap(newIndexes, newIndexSet);
5275 }
5276 /**
5277 * Ensure that this node is properly tracked in any indexes that we're maintaining
5278 */
5279 addToIndexes(namedNode, existingChildren) {
5280 const newIndexes = map(this.indexes_, (indexedChildren, indexName) => {
5281 const index = safeGet(this.indexSet_, indexName);
5282 assert(index, 'Missing index implementation for ' + indexName);
5283 if (indexedChildren === fallbackObject) {
5284 // Check to see if we need to index everything
5285 if (index.isDefinedOn(namedNode.node)) {
5286 // We need to build this index
5287 const childList = [];
5288 const iter = existingChildren.getIterator(NamedNode.Wrap);
5289 let next = iter.getNext();
5290 while (next) {
5291 if (next.name !== namedNode.name) {
5292 childList.push(next);
5293 }
5294 next = iter.getNext();
5295 }
5296 childList.push(namedNode);
5297 return buildChildSet(childList, index.getCompare());
5298 }
5299 else {
5300 // No change, this remains a fallback
5301 return fallbackObject;
5302 }
5303 }
5304 else {
5305 const existingSnap = existingChildren.get(namedNode.name);
5306 let newChildren = indexedChildren;
5307 if (existingSnap) {
5308 newChildren = newChildren.remove(new NamedNode(namedNode.name, existingSnap));
5309 }
5310 return newChildren.insert(namedNode, namedNode.node);
5311 }
5312 });
5313 return new IndexMap(newIndexes, this.indexSet_);
5314 }
5315 /**
5316 * Create a new IndexMap instance with the given value removed
5317 */
5318 removeFromIndexes(namedNode, existingChildren) {
5319 const newIndexes = map(this.indexes_, (indexedChildren) => {
5320 if (indexedChildren === fallbackObject) {
5321 // This is the fallback. Just return it, nothing to do in this case
5322 return indexedChildren;
5323 }
5324 else {
5325 const existingSnap = existingChildren.get(namedNode.name);
5326 if (existingSnap) {
5327 return indexedChildren.remove(new NamedNode(namedNode.name, existingSnap));
5328 }
5329 else {
5330 // No record of this child
5331 return indexedChildren;
5332 }
5333 }
5334 });
5335 return new IndexMap(newIndexes, this.indexSet_);
5336 }
5337}
5338
5339/**
5340 * @license
5341 * Copyright 2017 Google LLC
5342 *
5343 * Licensed under the Apache License, Version 2.0 (the "License");
5344 * you may not use this file except in compliance with the License.
5345 * You may obtain a copy of the License at
5346 *
5347 * http://www.apache.org/licenses/LICENSE-2.0
5348 *
5349 * Unless required by applicable law or agreed to in writing, software
5350 * distributed under the License is distributed on an "AS IS" BASIS,
5351 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
5352 * See the License for the specific language governing permissions and
5353 * limitations under the License.
5354 */
5355// TODO: For memory savings, don't store priorityNode_ if it's empty.
5356let EMPTY_NODE;
5357/**
5358 * ChildrenNode is a class for storing internal nodes in a DataSnapshot
5359 * (i.e. nodes with children). It implements Node and stores the
5360 * list of children in the children property, sorted by child name.
5361 */
5362class ChildrenNode {
5363 /**
5364 * @param children_ - List of children of this node..
5365 * @param priorityNode_ - The priority of this node (as a snapshot node).
5366 */
5367 constructor(children_, priorityNode_, indexMap_) {
5368 this.children_ = children_;
5369 this.priorityNode_ = priorityNode_;
5370 this.indexMap_ = indexMap_;
5371 this.lazyHash_ = null;
5372 /**
5373 * Note: The only reason we allow null priority is for EMPTY_NODE, since we can't use
5374 * EMPTY_NODE as the priority of EMPTY_NODE. We might want to consider making EMPTY_NODE its own
5375 * class instead of an empty ChildrenNode.
5376 */
5377 if (this.priorityNode_) {
5378 validatePriorityNode(this.priorityNode_);
5379 }
5380 if (this.children_.isEmpty()) {
5381 assert(!this.priorityNode_ || this.priorityNode_.isEmpty(), 'An empty node cannot have a priority');
5382 }
5383 }
5384 static get EMPTY_NODE() {
5385 return (EMPTY_NODE ||
5386 (EMPTY_NODE = new ChildrenNode(new SortedMap(NAME_COMPARATOR), null, IndexMap.Default)));
5387 }
5388 /** @inheritDoc */
5389 isLeafNode() {
5390 return false;
5391 }
5392 /** @inheritDoc */
5393 getPriority() {
5394 return this.priorityNode_ || EMPTY_NODE;
5395 }
5396 /** @inheritDoc */
5397 updatePriority(newPriorityNode) {
5398 if (this.children_.isEmpty()) {
5399 // Don't allow priorities on empty nodes
5400 return this;
5401 }
5402 else {
5403 return new ChildrenNode(this.children_, newPriorityNode, this.indexMap_);
5404 }
5405 }
5406 /** @inheritDoc */
5407 getImmediateChild(childName) {
5408 // Hack to treat priority as a regular child
5409 if (childName === '.priority') {
5410 return this.getPriority();
5411 }
5412 else {
5413 const child = this.children_.get(childName);
5414 return child === null ? EMPTY_NODE : child;
5415 }
5416 }
5417 /** @inheritDoc */
5418 getChild(path) {
5419 const front = pathGetFront(path);
5420 if (front === null) {
5421 return this;
5422 }
5423 return this.getImmediateChild(front).getChild(pathPopFront(path));
5424 }
5425 /** @inheritDoc */
5426 hasChild(childName) {
5427 return this.children_.get(childName) !== null;
5428 }
5429 /** @inheritDoc */
5430 updateImmediateChild(childName, newChildNode) {
5431 assert(newChildNode, 'We should always be passing snapshot nodes');
5432 if (childName === '.priority') {
5433 return this.updatePriority(newChildNode);
5434 }
5435 else {
5436 const namedNode = new NamedNode(childName, newChildNode);
5437 let newChildren, newIndexMap;
5438 if (newChildNode.isEmpty()) {
5439 newChildren = this.children_.remove(childName);
5440 newIndexMap = this.indexMap_.removeFromIndexes(namedNode, this.children_);
5441 }
5442 else {
5443 newChildren = this.children_.insert(childName, newChildNode);
5444 newIndexMap = this.indexMap_.addToIndexes(namedNode, this.children_);
5445 }
5446 const newPriority = newChildren.isEmpty()
5447 ? EMPTY_NODE
5448 : this.priorityNode_;
5449 return new ChildrenNode(newChildren, newPriority, newIndexMap);
5450 }
5451 }
5452 /** @inheritDoc */
5453 updateChild(path, newChildNode) {
5454 const front = pathGetFront(path);
5455 if (front === null) {
5456 return newChildNode;
5457 }
5458 else {
5459 assert(pathGetFront(path) !== '.priority' || pathGetLength(path) === 1, '.priority must be the last token in a path');
5460 const newImmediateChild = this.getImmediateChild(front).updateChild(pathPopFront(path), newChildNode);
5461 return this.updateImmediateChild(front, newImmediateChild);
5462 }
5463 }
5464 /** @inheritDoc */
5465 isEmpty() {
5466 return this.children_.isEmpty();
5467 }
5468 /** @inheritDoc */
5469 numChildren() {
5470 return this.children_.count();
5471 }
5472 /** @inheritDoc */
5473 val(exportFormat) {
5474 if (this.isEmpty()) {
5475 return null;
5476 }
5477 const obj = {};
5478 let numKeys = 0, maxKey = 0, allIntegerKeys = true;
5479 this.forEachChild(PRIORITY_INDEX, (key, childNode) => {
5480 obj[key] = childNode.val(exportFormat);
5481 numKeys++;
5482 if (allIntegerKeys && ChildrenNode.INTEGER_REGEXP_.test(key)) {
5483 maxKey = Math.max(maxKey, Number(key));
5484 }
5485 else {
5486 allIntegerKeys = false;
5487 }
5488 });
5489 if (!exportFormat && allIntegerKeys && maxKey < 2 * numKeys) {
5490 // convert to array.
5491 const array = [];
5492 // eslint-disable-next-line guard-for-in
5493 for (const key in obj) {
5494 array[key] = obj[key];
5495 }
5496 return array;
5497 }
5498 else {
5499 if (exportFormat && !this.getPriority().isEmpty()) {
5500 obj['.priority'] = this.getPriority().val();
5501 }
5502 return obj;
5503 }
5504 }
5505 /** @inheritDoc */
5506 hash() {
5507 if (this.lazyHash_ === null) {
5508 let toHash = '';
5509 if (!this.getPriority().isEmpty()) {
5510 toHash +=
5511 'priority:' +
5512 priorityHashText(this.getPriority().val()) +
5513 ':';
5514 }
5515 this.forEachChild(PRIORITY_INDEX, (key, childNode) => {
5516 const childHash = childNode.hash();
5517 if (childHash !== '') {
5518 toHash += ':' + key + ':' + childHash;
5519 }
5520 });
5521 this.lazyHash_ = toHash === '' ? '' : sha1(toHash);
5522 }
5523 return this.lazyHash_;
5524 }
5525 /** @inheritDoc */
5526 getPredecessorChildName(childName, childNode, index) {
5527 const idx = this.resolveIndex_(index);
5528 if (idx) {
5529 const predecessor = idx.getPredecessorKey(new NamedNode(childName, childNode));
5530 return predecessor ? predecessor.name : null;
5531 }
5532 else {
5533 return this.children_.getPredecessorKey(childName);
5534 }
5535 }
5536 getFirstChildName(indexDefinition) {
5537 const idx = this.resolveIndex_(indexDefinition);
5538 if (idx) {
5539 const minKey = idx.minKey();
5540 return minKey && minKey.name;
5541 }
5542 else {
5543 return this.children_.minKey();
5544 }
5545 }
5546 getFirstChild(indexDefinition) {
5547 const minKey = this.getFirstChildName(indexDefinition);
5548 if (minKey) {
5549 return new NamedNode(minKey, this.children_.get(minKey));
5550 }
5551 else {
5552 return null;
5553 }
5554 }
5555 /**
5556 * Given an index, return the key name of the largest value we have, according to that index
5557 */
5558 getLastChildName(indexDefinition) {
5559 const idx = this.resolveIndex_(indexDefinition);
5560 if (idx) {
5561 const maxKey = idx.maxKey();
5562 return maxKey && maxKey.name;
5563 }
5564 else {
5565 return this.children_.maxKey();
5566 }
5567 }
5568 getLastChild(indexDefinition) {
5569 const maxKey = this.getLastChildName(indexDefinition);
5570 if (maxKey) {
5571 return new NamedNode(maxKey, this.children_.get(maxKey));
5572 }
5573 else {
5574 return null;
5575 }
5576 }
5577 forEachChild(index, action) {
5578 const idx = this.resolveIndex_(index);
5579 if (idx) {
5580 return idx.inorderTraversal(wrappedNode => {
5581 return action(wrappedNode.name, wrappedNode.node);
5582 });
5583 }
5584 else {
5585 return this.children_.inorderTraversal(action);
5586 }
5587 }
5588 getIterator(indexDefinition) {
5589 return this.getIteratorFrom(indexDefinition.minPost(), indexDefinition);
5590 }
5591 getIteratorFrom(startPost, indexDefinition) {
5592 const idx = this.resolveIndex_(indexDefinition);
5593 if (idx) {
5594 return idx.getIteratorFrom(startPost, key => key);
5595 }
5596 else {
5597 const iterator = this.children_.getIteratorFrom(startPost.name, NamedNode.Wrap);
5598 let next = iterator.peek();
5599 while (next != null && indexDefinition.compare(next, startPost) < 0) {
5600 iterator.getNext();
5601 next = iterator.peek();
5602 }
5603 return iterator;
5604 }
5605 }
5606 getReverseIterator(indexDefinition) {
5607 return this.getReverseIteratorFrom(indexDefinition.maxPost(), indexDefinition);
5608 }
5609 getReverseIteratorFrom(endPost, indexDefinition) {
5610 const idx = this.resolveIndex_(indexDefinition);
5611 if (idx) {
5612 return idx.getReverseIteratorFrom(endPost, key => {
5613 return key;
5614 });
5615 }
5616 else {
5617 const iterator = this.children_.getReverseIteratorFrom(endPost.name, NamedNode.Wrap);
5618 let next = iterator.peek();
5619 while (next != null && indexDefinition.compare(next, endPost) > 0) {
5620 iterator.getNext();
5621 next = iterator.peek();
5622 }
5623 return iterator;
5624 }
5625 }
5626 compareTo(other) {
5627 if (this.isEmpty()) {
5628 if (other.isEmpty()) {
5629 return 0;
5630 }
5631 else {
5632 return -1;
5633 }
5634 }
5635 else if (other.isLeafNode() || other.isEmpty()) {
5636 return 1;
5637 }
5638 else if (other === MAX_NODE) {
5639 return -1;
5640 }
5641 else {
5642 // Must be another node with children.
5643 return 0;
5644 }
5645 }
5646 withIndex(indexDefinition) {
5647 if (indexDefinition === KEY_INDEX ||
5648 this.indexMap_.hasIndex(indexDefinition)) {
5649 return this;
5650 }
5651 else {
5652 const newIndexMap = this.indexMap_.addIndex(indexDefinition, this.children_);
5653 return new ChildrenNode(this.children_, this.priorityNode_, newIndexMap);
5654 }
5655 }
5656 isIndexed(index) {
5657 return index === KEY_INDEX || this.indexMap_.hasIndex(index);
5658 }
5659 equals(other) {
5660 if (other === this) {
5661 return true;
5662 }
5663 else if (other.isLeafNode()) {
5664 return false;
5665 }
5666 else {
5667 const otherChildrenNode = other;
5668 if (!this.getPriority().equals(otherChildrenNode.getPriority())) {
5669 return false;
5670 }
5671 else if (this.children_.count() === otherChildrenNode.children_.count()) {
5672 const thisIter = this.getIterator(PRIORITY_INDEX);
5673 const otherIter = otherChildrenNode.getIterator(PRIORITY_INDEX);
5674 let thisCurrent = thisIter.getNext();
5675 let otherCurrent = otherIter.getNext();
5676 while (thisCurrent && otherCurrent) {
5677 if (thisCurrent.name !== otherCurrent.name ||
5678 !thisCurrent.node.equals(otherCurrent.node)) {
5679 return false;
5680 }
5681 thisCurrent = thisIter.getNext();
5682 otherCurrent = otherIter.getNext();
5683 }
5684 return thisCurrent === null && otherCurrent === null;
5685 }
5686 else {
5687 return false;
5688 }
5689 }
5690 }
5691 /**
5692 * Returns a SortedMap ordered by index, or null if the default (by-key) ordering can be used
5693 * instead.
5694 *
5695 */
5696 resolveIndex_(indexDefinition) {
5697 if (indexDefinition === KEY_INDEX) {
5698 return null;
5699 }
5700 else {
5701 return this.indexMap_.get(indexDefinition.toString());
5702 }
5703 }
5704}
5705ChildrenNode.INTEGER_REGEXP_ = /^(0|[1-9]\d*)$/;
5706class MaxNode extends ChildrenNode {
5707 constructor() {
5708 super(new SortedMap(NAME_COMPARATOR), ChildrenNode.EMPTY_NODE, IndexMap.Default);
5709 }
5710 compareTo(other) {
5711 if (other === this) {
5712 return 0;
5713 }
5714 else {
5715 return 1;
5716 }
5717 }
5718 equals(other) {
5719 // Not that we every compare it, but MAX_NODE is only ever equal to itself
5720 return other === this;
5721 }
5722 getPriority() {
5723 return this;
5724 }
5725 getImmediateChild(childName) {
5726 return ChildrenNode.EMPTY_NODE;
5727 }
5728 isEmpty() {
5729 return false;
5730 }
5731}
5732/**
5733 * Marker that will sort higher than any other snapshot.
5734 */
5735const MAX_NODE = new MaxNode();
5736Object.defineProperties(NamedNode, {
5737 MIN: {
5738 value: new NamedNode(MIN_NAME, ChildrenNode.EMPTY_NODE)
5739 },
5740 MAX: {
5741 value: new NamedNode(MAX_NAME, MAX_NODE)
5742 }
5743});
5744/**
5745 * Reference Extensions
5746 */
5747KeyIndex.__EMPTY_NODE = ChildrenNode.EMPTY_NODE;
5748LeafNode.__childrenNodeConstructor = ChildrenNode;
5749setMaxNode$1(MAX_NODE);
5750setMaxNode(MAX_NODE);
5751
5752/**
5753 * @license
5754 * Copyright 2017 Google LLC
5755 *
5756 * Licensed under the Apache License, Version 2.0 (the "License");
5757 * you may not use this file except in compliance with the License.
5758 * You may obtain a copy of the License at
5759 *
5760 * http://www.apache.org/licenses/LICENSE-2.0
5761 *
5762 * Unless required by applicable law or agreed to in writing, software
5763 * distributed under the License is distributed on an "AS IS" BASIS,
5764 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
5765 * See the License for the specific language governing permissions and
5766 * limitations under the License.
5767 */
5768const USE_HINZE = true;
5769/**
5770 * Constructs a snapshot node representing the passed JSON and returns it.
5771 * @param json - JSON to create a node for.
5772 * @param priority - Optional priority to use. This will be ignored if the
5773 * passed JSON contains a .priority property.
5774 */
5775function nodeFromJSON(json, priority = null) {
5776 if (json === null) {
5777 return ChildrenNode.EMPTY_NODE;
5778 }
5779 if (typeof json === 'object' && '.priority' in json) {
5780 priority = json['.priority'];
5781 }
5782 assert(priority === null ||
5783 typeof priority === 'string' ||
5784 typeof priority === 'number' ||
5785 (typeof priority === 'object' && '.sv' in priority), 'Invalid priority type found: ' + typeof priority);
5786 if (typeof json === 'object' && '.value' in json && json['.value'] !== null) {
5787 json = json['.value'];
5788 }
5789 // Valid leaf nodes include non-objects or server-value wrapper objects
5790 if (typeof json !== 'object' || '.sv' in json) {
5791 const jsonLeaf = json;
5792 return new LeafNode(jsonLeaf, nodeFromJSON(priority));
5793 }
5794 if (!(json instanceof Array) && USE_HINZE) {
5795 const children = [];
5796 let childrenHavePriority = false;
5797 const hinzeJsonObj = json;
5798 each(hinzeJsonObj, (key, child) => {
5799 if (key.substring(0, 1) !== '.') {
5800 // Ignore metadata nodes
5801 const childNode = nodeFromJSON(child);
5802 if (!childNode.isEmpty()) {
5803 childrenHavePriority =
5804 childrenHavePriority || !childNode.getPriority().isEmpty();
5805 children.push(new NamedNode(key, childNode));
5806 }
5807 }
5808 });
5809 if (children.length === 0) {
5810 return ChildrenNode.EMPTY_NODE;
5811 }
5812 const childSet = buildChildSet(children, NAME_ONLY_COMPARATOR, namedNode => namedNode.name, NAME_COMPARATOR);
5813 if (childrenHavePriority) {
5814 const sortedChildSet = buildChildSet(children, PRIORITY_INDEX.getCompare());
5815 return new ChildrenNode(childSet, nodeFromJSON(priority), new IndexMap({ '.priority': sortedChildSet }, { '.priority': PRIORITY_INDEX }));
5816 }
5817 else {
5818 return new ChildrenNode(childSet, nodeFromJSON(priority), IndexMap.Default);
5819 }
5820 }
5821 else {
5822 let node = ChildrenNode.EMPTY_NODE;
5823 each(json, (key, childData) => {
5824 if (contains(json, key)) {
5825 if (key.substring(0, 1) !== '.') {
5826 // ignore metadata nodes.
5827 const childNode = nodeFromJSON(childData);
5828 if (childNode.isLeafNode() || !childNode.isEmpty()) {
5829 node = node.updateImmediateChild(key, childNode);
5830 }
5831 }
5832 }
5833 });
5834 return node.updatePriority(nodeFromJSON(priority));
5835 }
5836}
5837setNodeFromJSON(nodeFromJSON);
5838
5839/**
5840 * @license
5841 * Copyright 2017 Google LLC
5842 *
5843 * Licensed under the Apache License, Version 2.0 (the "License");
5844 * you may not use this file except in compliance with the License.
5845 * You may obtain a copy of the License at
5846 *
5847 * http://www.apache.org/licenses/LICENSE-2.0
5848 *
5849 * Unless required by applicable law or agreed to in writing, software
5850 * distributed under the License is distributed on an "AS IS" BASIS,
5851 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
5852 * See the License for the specific language governing permissions and
5853 * limitations under the License.
5854 */
5855class PathIndex extends Index {
5856 constructor(indexPath_) {
5857 super();
5858 this.indexPath_ = indexPath_;
5859 assert(!pathIsEmpty(indexPath_) && pathGetFront(indexPath_) !== '.priority', "Can't create PathIndex with empty path or .priority key");
5860 }
5861 extractChild(snap) {
5862 return snap.getChild(this.indexPath_);
5863 }
5864 isDefinedOn(node) {
5865 return !node.getChild(this.indexPath_).isEmpty();
5866 }
5867 compare(a, b) {
5868 const aChild = this.extractChild(a.node);
5869 const bChild = this.extractChild(b.node);
5870 const indexCmp = aChild.compareTo(bChild);
5871 if (indexCmp === 0) {
5872 return nameCompare(a.name, b.name);
5873 }
5874 else {
5875 return indexCmp;
5876 }
5877 }
5878 makePost(indexValue, name) {
5879 const valueNode = nodeFromJSON(indexValue);
5880 const node = ChildrenNode.EMPTY_NODE.updateChild(this.indexPath_, valueNode);
5881 return new NamedNode(name, node);
5882 }
5883 maxPost() {
5884 const node = ChildrenNode.EMPTY_NODE.updateChild(this.indexPath_, MAX_NODE);
5885 return new NamedNode(MAX_NAME, node);
5886 }
5887 toString() {
5888 return pathSlice(this.indexPath_, 0).join('/');
5889 }
5890}
5891
5892/**
5893 * @license
5894 * Copyright 2017 Google LLC
5895 *
5896 * Licensed under the Apache License, Version 2.0 (the "License");
5897 * you may not use this file except in compliance with the License.
5898 * You may obtain a copy of the License at
5899 *
5900 * http://www.apache.org/licenses/LICENSE-2.0
5901 *
5902 * Unless required by applicable law or agreed to in writing, software
5903 * distributed under the License is distributed on an "AS IS" BASIS,
5904 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
5905 * See the License for the specific language governing permissions and
5906 * limitations under the License.
5907 */
5908class ValueIndex extends Index {
5909 compare(a, b) {
5910 const indexCmp = a.node.compareTo(b.node);
5911 if (indexCmp === 0) {
5912 return nameCompare(a.name, b.name);
5913 }
5914 else {
5915 return indexCmp;
5916 }
5917 }
5918 isDefinedOn(node) {
5919 return true;
5920 }
5921 indexedValueChanged(oldNode, newNode) {
5922 return !oldNode.equals(newNode);
5923 }
5924 minPost() {
5925 // eslint-disable-next-line @typescript-eslint/no-explicit-any
5926 return NamedNode.MIN;
5927 }
5928 maxPost() {
5929 // eslint-disable-next-line @typescript-eslint/no-explicit-any
5930 return NamedNode.MAX;
5931 }
5932 makePost(indexValue, name) {
5933 const valueNode = nodeFromJSON(indexValue);
5934 return new NamedNode(name, valueNode);
5935 }
5936 /**
5937 * @returns String representation for inclusion in a query spec
5938 */
5939 toString() {
5940 return '.value';
5941 }
5942}
5943const VALUE_INDEX = new ValueIndex();
5944
5945/**
5946 * @license
5947 * Copyright 2017 Google LLC
5948 *
5949 * Licensed under the Apache License, Version 2.0 (the "License");
5950 * you may not use this file except in compliance with the License.
5951 * You may obtain a copy of the License at
5952 *
5953 * http://www.apache.org/licenses/LICENSE-2.0
5954 *
5955 * Unless required by applicable law or agreed to in writing, software
5956 * distributed under the License is distributed on an "AS IS" BASIS,
5957 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
5958 * See the License for the specific language governing permissions and
5959 * limitations under the License.
5960 */
5961// Modeled after base64 web-safe chars, but ordered by ASCII.
5962const PUSH_CHARS = '-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz';
5963const MIN_PUSH_CHAR = '-';
5964const MAX_PUSH_CHAR = 'z';
5965const MAX_KEY_LEN = 786;
5966/**
5967 * Fancy ID generator that creates 20-character string identifiers with the
5968 * following properties:
5969 *
5970 * 1. They're based on timestamp so that they sort *after* any existing ids.
5971 * 2. They contain 72-bits of random data after the timestamp so that IDs won't
5972 * collide with other clients' IDs.
5973 * 3. They sort *lexicographically* (so the timestamp is converted to characters
5974 * that will sort properly).
5975 * 4. They're monotonically increasing. Even if you generate more than one in
5976 * the same timestamp, the latter ones will sort after the former ones. We do
5977 * this by using the previous random bits but "incrementing" them by 1 (only
5978 * in the case of a timestamp collision).
5979 */
5980const nextPushId = (function () {
5981 // Timestamp of last push, used to prevent local collisions if you push twice
5982 // in one ms.
5983 let lastPushTime = 0;
5984 // We generate 72-bits of randomness which get turned into 12 characters and
5985 // appended to the timestamp to prevent collisions with other clients. We
5986 // store the last characters we generated because in the event of a collision,
5987 // we'll use those same characters except "incremented" by one.
5988 const lastRandChars = [];
5989 return function (now) {
5990 const duplicateTime = now === lastPushTime;
5991 lastPushTime = now;
5992 let i;
5993 const timeStampChars = new Array(8);
5994 for (i = 7; i >= 0; i--) {
5995 timeStampChars[i] = PUSH_CHARS.charAt(now % 64);
5996 // NOTE: Can't use << here because javascript will convert to int and lose
5997 // the upper bits.
5998 now = Math.floor(now / 64);
5999 }
6000 assert(now === 0, 'Cannot push at time == 0');
6001 let id = timeStampChars.join('');
6002 if (!duplicateTime) {
6003 for (i = 0; i < 12; i++) {
6004 lastRandChars[i] = Math.floor(Math.random() * 64);
6005 }
6006 }
6007 else {
6008 // If the timestamp hasn't changed since last push, use the same random
6009 // number, except incremented by 1.
6010 for (i = 11; i >= 0 && lastRandChars[i] === 63; i--) {
6011 lastRandChars[i] = 0;
6012 }
6013 lastRandChars[i]++;
6014 }
6015 for (i = 0; i < 12; i++) {
6016 id += PUSH_CHARS.charAt(lastRandChars[i]);
6017 }
6018 assert(id.length === 20, 'nextPushId: Length should be 20.');
6019 return id;
6020 };
6021})();
6022const successor = function (key) {
6023 if (key === '' + INTEGER_32_MAX) {
6024 // See https://firebase.google.com/docs/database/web/lists-of-data#data-order
6025 return MIN_PUSH_CHAR;
6026 }
6027 const keyAsInt = tryParseInt(key);
6028 if (keyAsInt != null) {
6029 return '' + (keyAsInt + 1);
6030 }
6031 const next = new Array(key.length);
6032 for (let i = 0; i < next.length; i++) {
6033 next[i] = key.charAt(i);
6034 }
6035 if (next.length < MAX_KEY_LEN) {
6036 next.push(MIN_PUSH_CHAR);
6037 return next.join('');
6038 }
6039 let i = next.length - 1;
6040 while (i >= 0 && next[i] === MAX_PUSH_CHAR) {
6041 i--;
6042 }
6043 // `successor` was called on the largest possible key, so return the
6044 // MAX_NAME, which sorts larger than all keys.
6045 if (i === -1) {
6046 return MAX_NAME;
6047 }
6048 const source = next[i];
6049 const sourcePlusOne = PUSH_CHARS.charAt(PUSH_CHARS.indexOf(source) + 1);
6050 next[i] = sourcePlusOne;
6051 return next.slice(0, i + 1).join('');
6052};
6053// `key` is assumed to be non-empty.
6054const predecessor = function (key) {
6055 if (key === '' + INTEGER_32_MIN) {
6056 return MIN_NAME;
6057 }
6058 const keyAsInt = tryParseInt(key);
6059 if (keyAsInt != null) {
6060 return '' + (keyAsInt - 1);
6061 }
6062 const next = new Array(key.length);
6063 for (let i = 0; i < next.length; i++) {
6064 next[i] = key.charAt(i);
6065 }
6066 // If `key` ends in `MIN_PUSH_CHAR`, the largest key lexicographically
6067 // smaller than `key`, is `key[0:key.length - 1]`. The next key smaller
6068 // than that, `predecessor(predecessor(key))`, is
6069 //
6070 // `key[0:key.length - 2] + (key[key.length - 1] - 1) + \
6071 // { MAX_PUSH_CHAR repeated MAX_KEY_LEN - (key.length - 1) times }
6072 //
6073 // analogous to increment/decrement for base-10 integers.
6074 //
6075 // This works because lexigographic comparison works character-by-character,
6076 // using length as a tie-breaker if one key is a prefix of the other.
6077 if (next[next.length - 1] === MIN_PUSH_CHAR) {
6078 if (next.length === 1) {
6079 // See https://firebase.google.com/docs/database/web/lists-of-data#orderbykey
6080 return '' + INTEGER_32_MAX;
6081 }
6082 delete next[next.length - 1];
6083 return next.join('');
6084 }
6085 // Replace the last character with it's immediate predecessor, and
6086 // fill the suffix of the key with MAX_PUSH_CHAR. This is the
6087 // lexicographically largest possible key smaller than `key`.
6088 next[next.length - 1] = PUSH_CHARS.charAt(PUSH_CHARS.indexOf(next[next.length - 1]) - 1);
6089 return next.join('') + MAX_PUSH_CHAR.repeat(MAX_KEY_LEN - next.length);
6090};
6091
6092/**
6093 * @license
6094 * Copyright 2017 Google LLC
6095 *
6096 * Licensed under the Apache License, Version 2.0 (the "License");
6097 * you may not use this file except in compliance with the License.
6098 * You may obtain a copy of the License at
6099 *
6100 * http://www.apache.org/licenses/LICENSE-2.0
6101 *
6102 * Unless required by applicable law or agreed to in writing, software
6103 * distributed under the License is distributed on an "AS IS" BASIS,
6104 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
6105 * See the License for the specific language governing permissions and
6106 * limitations under the License.
6107 */
6108function changeValue(snapshotNode) {
6109 return { type: "value" /* VALUE */, snapshotNode };
6110}
6111function changeChildAdded(childName, snapshotNode) {
6112 return { type: "child_added" /* CHILD_ADDED */, snapshotNode, childName };
6113}
6114function changeChildRemoved(childName, snapshotNode) {
6115 return { type: "child_removed" /* CHILD_REMOVED */, snapshotNode, childName };
6116}
6117function changeChildChanged(childName, snapshotNode, oldSnap) {
6118 return {
6119 type: "child_changed" /* CHILD_CHANGED */,
6120 snapshotNode,
6121 childName,
6122 oldSnap
6123 };
6124}
6125function changeChildMoved(childName, snapshotNode) {
6126 return { type: "child_moved" /* CHILD_MOVED */, snapshotNode, childName };
6127}
6128
6129/**
6130 * @license
6131 * Copyright 2017 Google LLC
6132 *
6133 * Licensed under the Apache License, Version 2.0 (the "License");
6134 * you may not use this file except in compliance with the License.
6135 * You may obtain a copy of the License at
6136 *
6137 * http://www.apache.org/licenses/LICENSE-2.0
6138 *
6139 * Unless required by applicable law or agreed to in writing, software
6140 * distributed under the License is distributed on an "AS IS" BASIS,
6141 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
6142 * See the License for the specific language governing permissions and
6143 * limitations under the License.
6144 */
6145/**
6146 * Doesn't really filter nodes but applies an index to the node and keeps track of any changes
6147 */
6148class IndexedFilter {
6149 constructor(index_) {
6150 this.index_ = index_;
6151 }
6152 updateChild(snap, key, newChild, affectedPath, source, optChangeAccumulator) {
6153 assert(snap.isIndexed(this.index_), 'A node must be indexed if only a child is updated');
6154 const oldChild = snap.getImmediateChild(key);
6155 // Check if anything actually changed.
6156 if (oldChild.getChild(affectedPath).equals(newChild.getChild(affectedPath))) {
6157 // There's an edge case where a child can enter or leave the view because affectedPath was set to null.
6158 // In this case, affectedPath will appear null in both the old and new snapshots. So we need
6159 // to avoid treating these cases as "nothing changed."
6160 if (oldChild.isEmpty() === newChild.isEmpty()) {
6161 // Nothing changed.
6162 // This assert should be valid, but it's expensive (can dominate perf testing) so don't actually do it.
6163 //assert(oldChild.equals(newChild), 'Old and new snapshots should be equal.');
6164 return snap;
6165 }
6166 }
6167 if (optChangeAccumulator != null) {
6168 if (newChild.isEmpty()) {
6169 if (snap.hasChild(key)) {
6170 optChangeAccumulator.trackChildChange(changeChildRemoved(key, oldChild));
6171 }
6172 else {
6173 assert(snap.isLeafNode(), 'A child remove without an old child only makes sense on a leaf node');
6174 }
6175 }
6176 else if (oldChild.isEmpty()) {
6177 optChangeAccumulator.trackChildChange(changeChildAdded(key, newChild));
6178 }
6179 else {
6180 optChangeAccumulator.trackChildChange(changeChildChanged(key, newChild, oldChild));
6181 }
6182 }
6183 if (snap.isLeafNode() && newChild.isEmpty()) {
6184 return snap;
6185 }
6186 else {
6187 // Make sure the node is indexed
6188 return snap.updateImmediateChild(key, newChild).withIndex(this.index_);
6189 }
6190 }
6191 updateFullNode(oldSnap, newSnap, optChangeAccumulator) {
6192 if (optChangeAccumulator != null) {
6193 if (!oldSnap.isLeafNode()) {
6194 oldSnap.forEachChild(PRIORITY_INDEX, (key, childNode) => {
6195 if (!newSnap.hasChild(key)) {
6196 optChangeAccumulator.trackChildChange(changeChildRemoved(key, childNode));
6197 }
6198 });
6199 }
6200 if (!newSnap.isLeafNode()) {
6201 newSnap.forEachChild(PRIORITY_INDEX, (key, childNode) => {
6202 if (oldSnap.hasChild(key)) {
6203 const oldChild = oldSnap.getImmediateChild(key);
6204 if (!oldChild.equals(childNode)) {
6205 optChangeAccumulator.trackChildChange(changeChildChanged(key, childNode, oldChild));
6206 }
6207 }
6208 else {
6209 optChangeAccumulator.trackChildChange(changeChildAdded(key, childNode));
6210 }
6211 });
6212 }
6213 }
6214 return newSnap.withIndex(this.index_);
6215 }
6216 updatePriority(oldSnap, newPriority) {
6217 if (oldSnap.isEmpty()) {
6218 return ChildrenNode.EMPTY_NODE;
6219 }
6220 else {
6221 return oldSnap.updatePriority(newPriority);
6222 }
6223 }
6224 filtersNodes() {
6225 return false;
6226 }
6227 getIndexedFilter() {
6228 return this;
6229 }
6230 getIndex() {
6231 return this.index_;
6232 }
6233}
6234
6235/**
6236 * @license
6237 * Copyright 2017 Google LLC
6238 *
6239 * Licensed under the Apache License, Version 2.0 (the "License");
6240 * you may not use this file except in compliance with the License.
6241 * You may obtain a copy of the License at
6242 *
6243 * http://www.apache.org/licenses/LICENSE-2.0
6244 *
6245 * Unless required by applicable law or agreed to in writing, software
6246 * distributed under the License is distributed on an "AS IS" BASIS,
6247 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
6248 * See the License for the specific language governing permissions and
6249 * limitations under the License.
6250 */
6251/**
6252 * Filters nodes by range and uses an IndexFilter to track any changes after filtering the node
6253 */
6254class RangedFilter {
6255 constructor(params) {
6256 this.indexedFilter_ = new IndexedFilter(params.getIndex());
6257 this.index_ = params.getIndex();
6258 this.startPost_ = RangedFilter.getStartPost_(params);
6259 this.endPost_ = RangedFilter.getEndPost_(params);
6260 }
6261 getStartPost() {
6262 return this.startPost_;
6263 }
6264 getEndPost() {
6265 return this.endPost_;
6266 }
6267 matches(node) {
6268 return (this.index_.compare(this.getStartPost(), node) <= 0 &&
6269 this.index_.compare(node, this.getEndPost()) <= 0);
6270 }
6271 updateChild(snap, key, newChild, affectedPath, source, optChangeAccumulator) {
6272 if (!this.matches(new NamedNode(key, newChild))) {
6273 newChild = ChildrenNode.EMPTY_NODE;
6274 }
6275 return this.indexedFilter_.updateChild(snap, key, newChild, affectedPath, source, optChangeAccumulator);
6276 }
6277 updateFullNode(oldSnap, newSnap, optChangeAccumulator) {
6278 if (newSnap.isLeafNode()) {
6279 // Make sure we have a children node with the correct index, not a leaf node;
6280 newSnap = ChildrenNode.EMPTY_NODE;
6281 }
6282 let filtered = newSnap.withIndex(this.index_);
6283 // Don't support priorities on queries
6284 filtered = filtered.updatePriority(ChildrenNode.EMPTY_NODE);
6285 const self = this;
6286 newSnap.forEachChild(PRIORITY_INDEX, (key, childNode) => {
6287 if (!self.matches(new NamedNode(key, childNode))) {
6288 filtered = filtered.updateImmediateChild(key, ChildrenNode.EMPTY_NODE);
6289 }
6290 });
6291 return this.indexedFilter_.updateFullNode(oldSnap, filtered, optChangeAccumulator);
6292 }
6293 updatePriority(oldSnap, newPriority) {
6294 // Don't support priorities on queries
6295 return oldSnap;
6296 }
6297 filtersNodes() {
6298 return true;
6299 }
6300 getIndexedFilter() {
6301 return this.indexedFilter_;
6302 }
6303 getIndex() {
6304 return this.index_;
6305 }
6306 static getStartPost_(params) {
6307 if (params.hasStart()) {
6308 const startName = params.getIndexStartName();
6309 return params.getIndex().makePost(params.getIndexStartValue(), startName);
6310 }
6311 else {
6312 return params.getIndex().minPost();
6313 }
6314 }
6315 static getEndPost_(params) {
6316 if (params.hasEnd()) {
6317 const endName = params.getIndexEndName();
6318 return params.getIndex().makePost(params.getIndexEndValue(), endName);
6319 }
6320 else {
6321 return params.getIndex().maxPost();
6322 }
6323 }
6324}
6325
6326/**
6327 * @license
6328 * Copyright 2017 Google LLC
6329 *
6330 * Licensed under the Apache License, Version 2.0 (the "License");
6331 * you may not use this file except in compliance with the License.
6332 * You may obtain a copy of the License at
6333 *
6334 * http://www.apache.org/licenses/LICENSE-2.0
6335 *
6336 * Unless required by applicable law or agreed to in writing, software
6337 * distributed under the License is distributed on an "AS IS" BASIS,
6338 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
6339 * See the License for the specific language governing permissions and
6340 * limitations under the License.
6341 */
6342/**
6343 * Applies a limit and a range to a node and uses RangedFilter to do the heavy lifting where possible
6344 */
6345class LimitedFilter {
6346 constructor(params) {
6347 this.rangedFilter_ = new RangedFilter(params);
6348 this.index_ = params.getIndex();
6349 this.limit_ = params.getLimit();
6350 this.reverse_ = !params.isViewFromLeft();
6351 }
6352 updateChild(snap, key, newChild, affectedPath, source, optChangeAccumulator) {
6353 if (!this.rangedFilter_.matches(new NamedNode(key, newChild))) {
6354 newChild = ChildrenNode.EMPTY_NODE;
6355 }
6356 if (snap.getImmediateChild(key).equals(newChild)) {
6357 // No change
6358 return snap;
6359 }
6360 else if (snap.numChildren() < this.limit_) {
6361 return this.rangedFilter_
6362 .getIndexedFilter()
6363 .updateChild(snap, key, newChild, affectedPath, source, optChangeAccumulator);
6364 }
6365 else {
6366 return this.fullLimitUpdateChild_(snap, key, newChild, source, optChangeAccumulator);
6367 }
6368 }
6369 updateFullNode(oldSnap, newSnap, optChangeAccumulator) {
6370 let filtered;
6371 if (newSnap.isLeafNode() || newSnap.isEmpty()) {
6372 // Make sure we have a children node with the correct index, not a leaf node;
6373 filtered = ChildrenNode.EMPTY_NODE.withIndex(this.index_);
6374 }
6375 else {
6376 if (this.limit_ * 2 < newSnap.numChildren() &&
6377 newSnap.isIndexed(this.index_)) {
6378 // Easier to build up a snapshot, since what we're given has more than twice the elements we want
6379 filtered = ChildrenNode.EMPTY_NODE.withIndex(this.index_);
6380 // anchor to the startPost, endPost, or last element as appropriate
6381 let iterator;
6382 if (this.reverse_) {
6383 iterator = newSnap.getReverseIteratorFrom(this.rangedFilter_.getEndPost(), this.index_);
6384 }
6385 else {
6386 iterator = newSnap.getIteratorFrom(this.rangedFilter_.getStartPost(), this.index_);
6387 }
6388 let count = 0;
6389 while (iterator.hasNext() && count < this.limit_) {
6390 const next = iterator.getNext();
6391 let inRange;
6392 if (this.reverse_) {
6393 inRange =
6394 this.index_.compare(this.rangedFilter_.getStartPost(), next) <= 0;
6395 }
6396 else {
6397 inRange =
6398 this.index_.compare(next, this.rangedFilter_.getEndPost()) <= 0;
6399 }
6400 if (inRange) {
6401 filtered = filtered.updateImmediateChild(next.name, next.node);
6402 count++;
6403 }
6404 else {
6405 // if we have reached the end post, we cannot keep adding elemments
6406 break;
6407 }
6408 }
6409 }
6410 else {
6411 // The snap contains less than twice the limit. Faster to delete from the snap than build up a new one
6412 filtered = newSnap.withIndex(this.index_);
6413 // Don't support priorities on queries
6414 filtered = filtered.updatePriority(ChildrenNode.EMPTY_NODE);
6415 let startPost;
6416 let endPost;
6417 let cmp;
6418 let iterator;
6419 if (this.reverse_) {
6420 iterator = filtered.getReverseIterator(this.index_);
6421 startPost = this.rangedFilter_.getEndPost();
6422 endPost = this.rangedFilter_.getStartPost();
6423 const indexCompare = this.index_.getCompare();
6424 cmp = (a, b) => indexCompare(b, a);
6425 }
6426 else {
6427 iterator = filtered.getIterator(this.index_);
6428 startPost = this.rangedFilter_.getStartPost();
6429 endPost = this.rangedFilter_.getEndPost();
6430 cmp = this.index_.getCompare();
6431 }
6432 let count = 0;
6433 let foundStartPost = false;
6434 while (iterator.hasNext()) {
6435 const next = iterator.getNext();
6436 if (!foundStartPost && cmp(startPost, next) <= 0) {
6437 // start adding
6438 foundStartPost = true;
6439 }
6440 const inRange = foundStartPost && count < this.limit_ && cmp(next, endPost) <= 0;
6441 if (inRange) {
6442 count++;
6443 }
6444 else {
6445 filtered = filtered.updateImmediateChild(next.name, ChildrenNode.EMPTY_NODE);
6446 }
6447 }
6448 }
6449 }
6450 return this.rangedFilter_
6451 .getIndexedFilter()
6452 .updateFullNode(oldSnap, filtered, optChangeAccumulator);
6453 }
6454 updatePriority(oldSnap, newPriority) {
6455 // Don't support priorities on queries
6456 return oldSnap;
6457 }
6458 filtersNodes() {
6459 return true;
6460 }
6461 getIndexedFilter() {
6462 return this.rangedFilter_.getIndexedFilter();
6463 }
6464 getIndex() {
6465 return this.index_;
6466 }
6467 fullLimitUpdateChild_(snap, childKey, childSnap, source, changeAccumulator) {
6468 // TODO: rename all cache stuff etc to general snap terminology
6469 let cmp;
6470 if (this.reverse_) {
6471 const indexCmp = this.index_.getCompare();
6472 cmp = (a, b) => indexCmp(b, a);
6473 }
6474 else {
6475 cmp = this.index_.getCompare();
6476 }
6477 const oldEventCache = snap;
6478 assert(oldEventCache.numChildren() === this.limit_, '');
6479 const newChildNamedNode = new NamedNode(childKey, childSnap);
6480 const windowBoundary = this.reverse_
6481 ? oldEventCache.getFirstChild(this.index_)
6482 : oldEventCache.getLastChild(this.index_);
6483 const inRange = this.rangedFilter_.matches(newChildNamedNode);
6484 if (oldEventCache.hasChild(childKey)) {
6485 const oldChildSnap = oldEventCache.getImmediateChild(childKey);
6486 let nextChild = source.getChildAfterChild(this.index_, windowBoundary, this.reverse_);
6487 while (nextChild != null &&
6488 (nextChild.name === childKey || oldEventCache.hasChild(nextChild.name))) {
6489 // There is a weird edge case where a node is updated as part of a merge in the write tree, but hasn't
6490 // been applied to the limited filter yet. Ignore this next child which will be updated later in
6491 // the limited filter...
6492 nextChild = source.getChildAfterChild(this.index_, nextChild, this.reverse_);
6493 }
6494 const compareNext = nextChild == null ? 1 : cmp(nextChild, newChildNamedNode);
6495 const remainsInWindow = inRange && !childSnap.isEmpty() && compareNext >= 0;
6496 if (remainsInWindow) {
6497 if (changeAccumulator != null) {
6498 changeAccumulator.trackChildChange(changeChildChanged(childKey, childSnap, oldChildSnap));
6499 }
6500 return oldEventCache.updateImmediateChild(childKey, childSnap);
6501 }
6502 else {
6503 if (changeAccumulator != null) {
6504 changeAccumulator.trackChildChange(changeChildRemoved(childKey, oldChildSnap));
6505 }
6506 const newEventCache = oldEventCache.updateImmediateChild(childKey, ChildrenNode.EMPTY_NODE);
6507 const nextChildInRange = nextChild != null && this.rangedFilter_.matches(nextChild);
6508 if (nextChildInRange) {
6509 if (changeAccumulator != null) {
6510 changeAccumulator.trackChildChange(changeChildAdded(nextChild.name, nextChild.node));
6511 }
6512 return newEventCache.updateImmediateChild(nextChild.name, nextChild.node);
6513 }
6514 else {
6515 return newEventCache;
6516 }
6517 }
6518 }
6519 else if (childSnap.isEmpty()) {
6520 // we're deleting a node, but it was not in the window, so ignore it
6521 return snap;
6522 }
6523 else if (inRange) {
6524 if (cmp(windowBoundary, newChildNamedNode) >= 0) {
6525 if (changeAccumulator != null) {
6526 changeAccumulator.trackChildChange(changeChildRemoved(windowBoundary.name, windowBoundary.node));
6527 changeAccumulator.trackChildChange(changeChildAdded(childKey, childSnap));
6528 }
6529 return oldEventCache
6530 .updateImmediateChild(childKey, childSnap)
6531 .updateImmediateChild(windowBoundary.name, ChildrenNode.EMPTY_NODE);
6532 }
6533 else {
6534 return snap;
6535 }
6536 }
6537 else {
6538 return snap;
6539 }
6540 }
6541}
6542
6543/**
6544 * @license
6545 * Copyright 2017 Google LLC
6546 *
6547 * Licensed under the Apache License, Version 2.0 (the "License");
6548 * you may not use this file except in compliance with the License.
6549 * You may obtain a copy of the License at
6550 *
6551 * http://www.apache.org/licenses/LICENSE-2.0
6552 *
6553 * Unless required by applicable law or agreed to in writing, software
6554 * distributed under the License is distributed on an "AS IS" BASIS,
6555 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
6556 * See the License for the specific language governing permissions and
6557 * limitations under the License.
6558 */
6559/**
6560 * This class is an immutable-from-the-public-api struct containing a set of query parameters defining a
6561 * range to be returned for a particular location. It is assumed that validation of parameters is done at the
6562 * user-facing API level, so it is not done here.
6563 *
6564 * @internal
6565 */
6566class QueryParams {
6567 constructor() {
6568 this.limitSet_ = false;
6569 this.startSet_ = false;
6570 this.startNameSet_ = false;
6571 this.startAfterSet_ = false;
6572 this.endSet_ = false;
6573 this.endNameSet_ = false;
6574 this.endBeforeSet_ = false;
6575 this.limit_ = 0;
6576 this.viewFrom_ = '';
6577 this.indexStartValue_ = null;
6578 this.indexStartName_ = '';
6579 this.indexEndValue_ = null;
6580 this.indexEndName_ = '';
6581 this.index_ = PRIORITY_INDEX;
6582 }
6583 hasStart() {
6584 return this.startSet_;
6585 }
6586 hasStartAfter() {
6587 return this.startAfterSet_;
6588 }
6589 hasEndBefore() {
6590 return this.endBeforeSet_;
6591 }
6592 /**
6593 * @returns True if it would return from left.
6594 */
6595 isViewFromLeft() {
6596 if (this.viewFrom_ === '') {
6597 // limit(), rather than limitToFirst or limitToLast was called.
6598 // This means that only one of startSet_ and endSet_ is true. Use them
6599 // to calculate which side of the view to anchor to. If neither is set,
6600 // anchor to the end.
6601 return this.startSet_;
6602 }
6603 else {
6604 return this.viewFrom_ === "l" /* VIEW_FROM_LEFT */;
6605 }
6606 }
6607 /**
6608 * Only valid to call if hasStart() returns true
6609 */
6610 getIndexStartValue() {
6611 assert(this.startSet_, 'Only valid if start has been set');
6612 return this.indexStartValue_;
6613 }
6614 /**
6615 * Only valid to call if hasStart() returns true.
6616 * Returns the starting key name for the range defined by these query parameters
6617 */
6618 getIndexStartName() {
6619 assert(this.startSet_, 'Only valid if start has been set');
6620 if (this.startNameSet_) {
6621 return this.indexStartName_;
6622 }
6623 else {
6624 return MIN_NAME;
6625 }
6626 }
6627 hasEnd() {
6628 return this.endSet_;
6629 }
6630 /**
6631 * Only valid to call if hasEnd() returns true.
6632 */
6633 getIndexEndValue() {
6634 assert(this.endSet_, 'Only valid if end has been set');
6635 return this.indexEndValue_;
6636 }
6637 /**
6638 * Only valid to call if hasEnd() returns true.
6639 * Returns the end key name for the range defined by these query parameters
6640 */
6641 getIndexEndName() {
6642 assert(this.endSet_, 'Only valid if end has been set');
6643 if (this.endNameSet_) {
6644 return this.indexEndName_;
6645 }
6646 else {
6647 return MAX_NAME;
6648 }
6649 }
6650 hasLimit() {
6651 return this.limitSet_;
6652 }
6653 /**
6654 * @returns True if a limit has been set and it has been explicitly anchored
6655 */
6656 hasAnchoredLimit() {
6657 return this.limitSet_ && this.viewFrom_ !== '';
6658 }
6659 /**
6660 * Only valid to call if hasLimit() returns true
6661 */
6662 getLimit() {
6663 assert(this.limitSet_, 'Only valid if limit has been set');
6664 return this.limit_;
6665 }
6666 getIndex() {
6667 return this.index_;
6668 }
6669 loadsAllData() {
6670 return !(this.startSet_ || this.endSet_ || this.limitSet_);
6671 }
6672 isDefault() {
6673 return this.loadsAllData() && this.index_ === PRIORITY_INDEX;
6674 }
6675 copy() {
6676 const copy = new QueryParams();
6677 copy.limitSet_ = this.limitSet_;
6678 copy.limit_ = this.limit_;
6679 copy.startSet_ = this.startSet_;
6680 copy.indexStartValue_ = this.indexStartValue_;
6681 copy.startNameSet_ = this.startNameSet_;
6682 copy.indexStartName_ = this.indexStartName_;
6683 copy.endSet_ = this.endSet_;
6684 copy.indexEndValue_ = this.indexEndValue_;
6685 copy.endNameSet_ = this.endNameSet_;
6686 copy.indexEndName_ = this.indexEndName_;
6687 copy.index_ = this.index_;
6688 copy.viewFrom_ = this.viewFrom_;
6689 return copy;
6690 }
6691}
6692function queryParamsGetNodeFilter(queryParams) {
6693 if (queryParams.loadsAllData()) {
6694 return new IndexedFilter(queryParams.getIndex());
6695 }
6696 else if (queryParams.hasLimit()) {
6697 return new LimitedFilter(queryParams);
6698 }
6699 else {
6700 return new RangedFilter(queryParams);
6701 }
6702}
6703function queryParamsLimitToFirst(queryParams, newLimit) {
6704 const newParams = queryParams.copy();
6705 newParams.limitSet_ = true;
6706 newParams.limit_ = newLimit;
6707 newParams.viewFrom_ = "l" /* VIEW_FROM_LEFT */;
6708 return newParams;
6709}
6710function queryParamsLimitToLast(queryParams, newLimit) {
6711 const newParams = queryParams.copy();
6712 newParams.limitSet_ = true;
6713 newParams.limit_ = newLimit;
6714 newParams.viewFrom_ = "r" /* VIEW_FROM_RIGHT */;
6715 return newParams;
6716}
6717function queryParamsStartAt(queryParams, indexValue, key) {
6718 const newParams = queryParams.copy();
6719 newParams.startSet_ = true;
6720 if (indexValue === undefined) {
6721 indexValue = null;
6722 }
6723 newParams.indexStartValue_ = indexValue;
6724 if (key != null) {
6725 newParams.startNameSet_ = true;
6726 newParams.indexStartName_ = key;
6727 }
6728 else {
6729 newParams.startNameSet_ = false;
6730 newParams.indexStartName_ = '';
6731 }
6732 return newParams;
6733}
6734function queryParamsStartAfter(queryParams, indexValue, key) {
6735 let params;
6736 if (queryParams.index_ === KEY_INDEX) {
6737 if (typeof indexValue === 'string') {
6738 indexValue = successor(indexValue);
6739 }
6740 params = queryParamsStartAt(queryParams, indexValue, key);
6741 }
6742 else {
6743 let childKey;
6744 if (key == null) {
6745 childKey = MAX_NAME;
6746 }
6747 else {
6748 childKey = successor(key);
6749 }
6750 params = queryParamsStartAt(queryParams, indexValue, childKey);
6751 }
6752 params.startAfterSet_ = true;
6753 return params;
6754}
6755function queryParamsEndAt(queryParams, indexValue, key) {
6756 const newParams = queryParams.copy();
6757 newParams.endSet_ = true;
6758 if (indexValue === undefined) {
6759 indexValue = null;
6760 }
6761 newParams.indexEndValue_ = indexValue;
6762 if (key !== undefined) {
6763 newParams.endNameSet_ = true;
6764 newParams.indexEndName_ = key;
6765 }
6766 else {
6767 newParams.endNameSet_ = false;
6768 newParams.indexEndName_ = '';
6769 }
6770 return newParams;
6771}
6772function queryParamsEndBefore(queryParams, indexValue, key) {
6773 let childKey;
6774 let params;
6775 if (queryParams.index_ === KEY_INDEX) {
6776 if (typeof indexValue === 'string') {
6777 indexValue = predecessor(indexValue);
6778 }
6779 params = queryParamsEndAt(queryParams, indexValue, key);
6780 }
6781 else {
6782 if (key == null) {
6783 childKey = MIN_NAME;
6784 }
6785 else {
6786 childKey = predecessor(key);
6787 }
6788 params = queryParamsEndAt(queryParams, indexValue, childKey);
6789 }
6790 params.endBeforeSet_ = true;
6791 return params;
6792}
6793function queryParamsOrderBy(queryParams, index) {
6794 const newParams = queryParams.copy();
6795 newParams.index_ = index;
6796 return newParams;
6797}
6798/**
6799 * Returns a set of REST query string parameters representing this query.
6800 *
6801 * @returns query string parameters
6802 */
6803function queryParamsToRestQueryStringParameters(queryParams) {
6804 const qs = {};
6805 if (queryParams.isDefault()) {
6806 return qs;
6807 }
6808 let orderBy;
6809 if (queryParams.index_ === PRIORITY_INDEX) {
6810 orderBy = "$priority" /* PRIORITY_INDEX */;
6811 }
6812 else if (queryParams.index_ === VALUE_INDEX) {
6813 orderBy = "$value" /* VALUE_INDEX */;
6814 }
6815 else if (queryParams.index_ === KEY_INDEX) {
6816 orderBy = "$key" /* KEY_INDEX */;
6817 }
6818 else {
6819 assert(queryParams.index_ instanceof PathIndex, 'Unrecognized index type!');
6820 orderBy = queryParams.index_.toString();
6821 }
6822 qs["orderBy" /* ORDER_BY */] = stringify(orderBy);
6823 if (queryParams.startSet_) {
6824 qs["startAt" /* START_AT */] = stringify(queryParams.indexStartValue_);
6825 if (queryParams.startNameSet_) {
6826 qs["startAt" /* START_AT */] +=
6827 ',' + stringify(queryParams.indexStartName_);
6828 }
6829 }
6830 if (queryParams.endSet_) {
6831 qs["endAt" /* END_AT */] = stringify(queryParams.indexEndValue_);
6832 if (queryParams.endNameSet_) {
6833 qs["endAt" /* END_AT */] +=
6834 ',' + stringify(queryParams.indexEndName_);
6835 }
6836 }
6837 if (queryParams.limitSet_) {
6838 if (queryParams.isViewFromLeft()) {
6839 qs["limitToFirst" /* LIMIT_TO_FIRST */] = queryParams.limit_;
6840 }
6841 else {
6842 qs["limitToLast" /* LIMIT_TO_LAST */] = queryParams.limit_;
6843 }
6844 }
6845 return qs;
6846}
6847function queryParamsGetQueryObject(queryParams) {
6848 const obj = {};
6849 if (queryParams.startSet_) {
6850 obj["sp" /* INDEX_START_VALUE */] =
6851 queryParams.indexStartValue_;
6852 if (queryParams.startNameSet_) {
6853 obj["sn" /* INDEX_START_NAME */] =
6854 queryParams.indexStartName_;
6855 }
6856 }
6857 if (queryParams.endSet_) {
6858 obj["ep" /* INDEX_END_VALUE */] = queryParams.indexEndValue_;
6859 if (queryParams.endNameSet_) {
6860 obj["en" /* INDEX_END_NAME */] = queryParams.indexEndName_;
6861 }
6862 }
6863 if (queryParams.limitSet_) {
6864 obj["l" /* LIMIT */] = queryParams.limit_;
6865 let viewFrom = queryParams.viewFrom_;
6866 if (viewFrom === '') {
6867 if (queryParams.isViewFromLeft()) {
6868 viewFrom = "l" /* VIEW_FROM_LEFT */;
6869 }
6870 else {
6871 viewFrom = "r" /* VIEW_FROM_RIGHT */;
6872 }
6873 }
6874 obj["vf" /* VIEW_FROM */] = viewFrom;
6875 }
6876 // For now, priority index is the default, so we only specify if it's some other index
6877 if (queryParams.index_ !== PRIORITY_INDEX) {
6878 obj["i" /* INDEX */] = queryParams.index_.toString();
6879 }
6880 return obj;
6881}
6882
6883/**
6884 * @license
6885 * Copyright 2017 Google LLC
6886 *
6887 * Licensed under the Apache License, Version 2.0 (the "License");
6888 * you may not use this file except in compliance with the License.
6889 * You may obtain a copy of the License at
6890 *
6891 * http://www.apache.org/licenses/LICENSE-2.0
6892 *
6893 * Unless required by applicable law or agreed to in writing, software
6894 * distributed under the License is distributed on an "AS IS" BASIS,
6895 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
6896 * See the License for the specific language governing permissions and
6897 * limitations under the License.
6898 */
6899/**
6900 * An implementation of ServerActions that communicates with the server via REST requests.
6901 * This is mostly useful for compatibility with crawlers, where we don't want to spin up a full
6902 * persistent connection (using WebSockets or long-polling)
6903 */
6904class ReadonlyRestClient extends ServerActions {
6905 /**
6906 * @param repoInfo_ - Data about the namespace we are connecting to
6907 * @param onDataUpdate_ - A callback for new data from the server
6908 */
6909 constructor(repoInfo_, onDataUpdate_, authTokenProvider_, appCheckTokenProvider_) {
6910 super();
6911 this.repoInfo_ = repoInfo_;
6912 this.onDataUpdate_ = onDataUpdate_;
6913 this.authTokenProvider_ = authTokenProvider_;
6914 this.appCheckTokenProvider_ = appCheckTokenProvider_;
6915 /** @private {function(...[*])} */
6916 this.log_ = logWrapper('p:rest:');
6917 /**
6918 * We don't actually need to track listens, except to prevent us calling an onComplete for a listen
6919 * that's been removed. :-/
6920 */
6921 this.listens_ = {};
6922 }
6923 reportStats(stats) {
6924 throw new Error('Method not implemented.');
6925 }
6926 static getListenId_(query, tag) {
6927 if (tag !== undefined) {
6928 return 'tag$' + tag;
6929 }
6930 else {
6931 assert(query._queryParams.isDefault(), "should have a tag if it's not a default query.");
6932 return query._path.toString();
6933 }
6934 }
6935 /** @inheritDoc */
6936 listen(query, currentHashFn, tag, onComplete) {
6937 const pathString = query._path.toString();
6938 this.log_('Listen called for ' + pathString + ' ' + query._queryIdentifier);
6939 // Mark this listener so we can tell if it's removed.
6940 const listenId = ReadonlyRestClient.getListenId_(query, tag);
6941 const thisListen = {};
6942 this.listens_[listenId] = thisListen;
6943 const queryStringParameters = queryParamsToRestQueryStringParameters(query._queryParams);
6944 this.restRequest_(pathString + '.json', queryStringParameters, (error, result) => {
6945 let data = result;
6946 if (error === 404) {
6947 data = null;
6948 error = null;
6949 }
6950 if (error === null) {
6951 this.onDataUpdate_(pathString, data, /*isMerge=*/ false, tag);
6952 }
6953 if (safeGet(this.listens_, listenId) === thisListen) {
6954 let status;
6955 if (!error) {
6956 status = 'ok';
6957 }
6958 else if (error === 401) {
6959 status = 'permission_denied';
6960 }
6961 else {
6962 status = 'rest_error:' + error;
6963 }
6964 onComplete(status, null);
6965 }
6966 });
6967 }
6968 /** @inheritDoc */
6969 unlisten(query, tag) {
6970 const listenId = ReadonlyRestClient.getListenId_(query, tag);
6971 delete this.listens_[listenId];
6972 }
6973 get(query) {
6974 const queryStringParameters = queryParamsToRestQueryStringParameters(query._queryParams);
6975 const pathString = query._path.toString();
6976 const deferred = new Deferred();
6977 this.restRequest_(pathString + '.json', queryStringParameters, (error, result) => {
6978 let data = result;
6979 if (error === 404) {
6980 data = null;
6981 error = null;
6982 }
6983 if (error === null) {
6984 this.onDataUpdate_(pathString, data,
6985 /*isMerge=*/ false,
6986 /*tag=*/ null);
6987 deferred.resolve(data);
6988 }
6989 else {
6990 deferred.reject(new Error(data));
6991 }
6992 });
6993 return deferred.promise;
6994 }
6995 /** @inheritDoc */
6996 refreshAuthToken(token) {
6997 // no-op since we just always call getToken.
6998 }
6999 /**
7000 * Performs a REST request to the given path, with the provided query string parameters,
7001 * and any auth credentials we have.
7002 */
7003 restRequest_(pathString, queryStringParameters = {}, callback) {
7004 queryStringParameters['format'] = 'export';
7005 return Promise.all([
7006 this.authTokenProvider_.getToken(/*forceRefresh=*/ false),
7007 this.appCheckTokenProvider_.getToken(/*forceRefresh=*/ false)
7008 ]).then(([authToken, appCheckToken]) => {
7009 if (authToken && authToken.accessToken) {
7010 queryStringParameters['auth'] = authToken.accessToken;
7011 }
7012 if (appCheckToken && appCheckToken.token) {
7013 queryStringParameters['ac'] = appCheckToken.token;
7014 }
7015 const url = (this.repoInfo_.secure ? 'https://' : 'http://') +
7016 this.repoInfo_.host +
7017 pathString +
7018 '?' +
7019 'ns=' +
7020 this.repoInfo_.namespace +
7021 querystring(queryStringParameters);
7022 this.log_('Sending REST request for ' + url);
7023 const xhr = new XMLHttpRequest();
7024 xhr.onreadystatechange = () => {
7025 if (callback && xhr.readyState === 4) {
7026 this.log_('REST Response for ' + url + ' received. status:', xhr.status, 'response:', xhr.responseText);
7027 let res = null;
7028 if (xhr.status >= 200 && xhr.status < 300) {
7029 try {
7030 res = jsonEval(xhr.responseText);
7031 }
7032 catch (e) {
7033 warn('Failed to parse JSON response for ' +
7034 url +
7035 ': ' +
7036 xhr.responseText);
7037 }
7038 callback(null, res);
7039 }
7040 else {
7041 // 401 and 404 are expected.
7042 if (xhr.status !== 401 && xhr.status !== 404) {
7043 warn('Got unsuccessful REST response for ' +
7044 url +
7045 ' Status: ' +
7046 xhr.status);
7047 }
7048 callback(xhr.status);
7049 }
7050 callback = null;
7051 }
7052 };
7053 xhr.open('GET', url, /*asynchronous=*/ true);
7054 xhr.send();
7055 });
7056 }
7057}
7058
7059/**
7060 * @license
7061 * Copyright 2017 Google LLC
7062 *
7063 * Licensed under the Apache License, Version 2.0 (the "License");
7064 * you may not use this file except in compliance with the License.
7065 * You may obtain a copy of the License at
7066 *
7067 * http://www.apache.org/licenses/LICENSE-2.0
7068 *
7069 * Unless required by applicable law or agreed to in writing, software
7070 * distributed under the License is distributed on an "AS IS" BASIS,
7071 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7072 * See the License for the specific language governing permissions and
7073 * limitations under the License.
7074 */
7075/**
7076 * Mutable object which basically just stores a reference to the "latest" immutable snapshot.
7077 */
7078class SnapshotHolder {
7079 constructor() {
7080 this.rootNode_ = ChildrenNode.EMPTY_NODE;
7081 }
7082 getNode(path) {
7083 return this.rootNode_.getChild(path);
7084 }
7085 updateSnapshot(path, newSnapshotNode) {
7086 this.rootNode_ = this.rootNode_.updateChild(path, newSnapshotNode);
7087 }
7088}
7089
7090/**
7091 * @license
7092 * Copyright 2017 Google LLC
7093 *
7094 * Licensed under the Apache License, Version 2.0 (the "License");
7095 * you may not use this file except in compliance with the License.
7096 * You may obtain a copy of the License at
7097 *
7098 * http://www.apache.org/licenses/LICENSE-2.0
7099 *
7100 * Unless required by applicable law or agreed to in writing, software
7101 * distributed under the License is distributed on an "AS IS" BASIS,
7102 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7103 * See the License for the specific language governing permissions and
7104 * limitations under the License.
7105 */
7106function newSparseSnapshotTree() {
7107 return {
7108 value: null,
7109 children: new Map()
7110 };
7111}
7112/**
7113 * Stores the given node at the specified path. If there is already a node
7114 * at a shallower path, it merges the new data into that snapshot node.
7115 *
7116 * @param path - Path to look up snapshot for.
7117 * @param data - The new data, or null.
7118 */
7119function sparseSnapshotTreeRemember(sparseSnapshotTree, path, data) {
7120 if (pathIsEmpty(path)) {
7121 sparseSnapshotTree.value = data;
7122 sparseSnapshotTree.children.clear();
7123 }
7124 else if (sparseSnapshotTree.value !== null) {
7125 sparseSnapshotTree.value = sparseSnapshotTree.value.updateChild(path, data);
7126 }
7127 else {
7128 const childKey = pathGetFront(path);
7129 if (!sparseSnapshotTree.children.has(childKey)) {
7130 sparseSnapshotTree.children.set(childKey, newSparseSnapshotTree());
7131 }
7132 const child = sparseSnapshotTree.children.get(childKey);
7133 path = pathPopFront(path);
7134 sparseSnapshotTreeRemember(child, path, data);
7135 }
7136}
7137/**
7138 * Purge the data at path from the cache.
7139 *
7140 * @param path - Path to look up snapshot for.
7141 * @returns True if this node should now be removed.
7142 */
7143function sparseSnapshotTreeForget(sparseSnapshotTree, path) {
7144 if (pathIsEmpty(path)) {
7145 sparseSnapshotTree.value = null;
7146 sparseSnapshotTree.children.clear();
7147 return true;
7148 }
7149 else {
7150 if (sparseSnapshotTree.value !== null) {
7151 if (sparseSnapshotTree.value.isLeafNode()) {
7152 // We're trying to forget a node that doesn't exist
7153 return false;
7154 }
7155 else {
7156 const value = sparseSnapshotTree.value;
7157 sparseSnapshotTree.value = null;
7158 value.forEachChild(PRIORITY_INDEX, (key, tree) => {
7159 sparseSnapshotTreeRemember(sparseSnapshotTree, new Path(key), tree);
7160 });
7161 return sparseSnapshotTreeForget(sparseSnapshotTree, path);
7162 }
7163 }
7164 else if (sparseSnapshotTree.children.size > 0) {
7165 const childKey = pathGetFront(path);
7166 path = pathPopFront(path);
7167 if (sparseSnapshotTree.children.has(childKey)) {
7168 const safeToRemove = sparseSnapshotTreeForget(sparseSnapshotTree.children.get(childKey), path);
7169 if (safeToRemove) {
7170 sparseSnapshotTree.children.delete(childKey);
7171 }
7172 }
7173 return sparseSnapshotTree.children.size === 0;
7174 }
7175 else {
7176 return true;
7177 }
7178 }
7179}
7180/**
7181 * Recursively iterates through all of the stored tree and calls the
7182 * callback on each one.
7183 *
7184 * @param prefixPath - Path to look up node for.
7185 * @param func - The function to invoke for each tree.
7186 */
7187function sparseSnapshotTreeForEachTree(sparseSnapshotTree, prefixPath, func) {
7188 if (sparseSnapshotTree.value !== null) {
7189 func(prefixPath, sparseSnapshotTree.value);
7190 }
7191 else {
7192 sparseSnapshotTreeForEachChild(sparseSnapshotTree, (key, tree) => {
7193 const path = new Path(prefixPath.toString() + '/' + key);
7194 sparseSnapshotTreeForEachTree(tree, path, func);
7195 });
7196 }
7197}
7198/**
7199 * Iterates through each immediate child and triggers the callback.
7200 * Only seems to be used in tests.
7201 *
7202 * @param func - The function to invoke for each child.
7203 */
7204function sparseSnapshotTreeForEachChild(sparseSnapshotTree, func) {
7205 sparseSnapshotTree.children.forEach((tree, key) => {
7206 func(key, tree);
7207 });
7208}
7209
7210/**
7211 * @license
7212 * Copyright 2017 Google LLC
7213 *
7214 * Licensed under the Apache License, Version 2.0 (the "License");
7215 * you may not use this file except in compliance with the License.
7216 * You may obtain a copy of the License at
7217 *
7218 * http://www.apache.org/licenses/LICENSE-2.0
7219 *
7220 * Unless required by applicable law or agreed to in writing, software
7221 * distributed under the License is distributed on an "AS IS" BASIS,
7222 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7223 * See the License for the specific language governing permissions and
7224 * limitations under the License.
7225 */
7226/**
7227 * Returns the delta from the previous call to get stats.
7228 *
7229 * @param collection_ - The collection to "listen" to.
7230 */
7231class StatsListener {
7232 constructor(collection_) {
7233 this.collection_ = collection_;
7234 this.last_ = null;
7235 }
7236 get() {
7237 const newStats = this.collection_.get();
7238 const delta = Object.assign({}, newStats);
7239 if (this.last_) {
7240 each(this.last_, (stat, value) => {
7241 delta[stat] = delta[stat] - value;
7242 });
7243 }
7244 this.last_ = newStats;
7245 return delta;
7246 }
7247}
7248
7249/**
7250 * @license
7251 * Copyright 2017 Google LLC
7252 *
7253 * Licensed under the Apache License, Version 2.0 (the "License");
7254 * you may not use this file except in compliance with the License.
7255 * You may obtain a copy of the License at
7256 *
7257 * http://www.apache.org/licenses/LICENSE-2.0
7258 *
7259 * Unless required by applicable law or agreed to in writing, software
7260 * distributed under the License is distributed on an "AS IS" BASIS,
7261 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7262 * See the License for the specific language governing permissions and
7263 * limitations under the License.
7264 */
7265// Assuming some apps may have a short amount of time on page, and a bulk of firebase operations probably
7266// happen on page load, we try to report our first set of stats pretty quickly, but we wait at least 10
7267// seconds to try to ensure the Firebase connection is established / settled.
7268const FIRST_STATS_MIN_TIME = 10 * 1000;
7269const FIRST_STATS_MAX_TIME = 30 * 1000;
7270// We'll continue to report stats on average every 5 minutes.
7271const REPORT_STATS_INTERVAL = 5 * 60 * 1000;
7272class StatsReporter {
7273 constructor(collection, server_) {
7274 this.server_ = server_;
7275 this.statsToReport_ = {};
7276 this.statsListener_ = new StatsListener(collection);
7277 const timeout = FIRST_STATS_MIN_TIME +
7278 (FIRST_STATS_MAX_TIME - FIRST_STATS_MIN_TIME) * Math.random();
7279 setTimeoutNonBlocking(this.reportStats_.bind(this), Math.floor(timeout));
7280 }
7281 reportStats_() {
7282 const stats = this.statsListener_.get();
7283 const reportedStats = {};
7284 let haveStatsToReport = false;
7285 each(stats, (stat, value) => {
7286 if (value > 0 && contains(this.statsToReport_, stat)) {
7287 reportedStats[stat] = value;
7288 haveStatsToReport = true;
7289 }
7290 });
7291 if (haveStatsToReport) {
7292 this.server_.reportStats(reportedStats);
7293 }
7294 // queue our next run.
7295 setTimeoutNonBlocking(this.reportStats_.bind(this), Math.floor(Math.random() * 2 * REPORT_STATS_INTERVAL));
7296 }
7297}
7298
7299/**
7300 * @license
7301 * Copyright 2017 Google LLC
7302 *
7303 * Licensed under the Apache License, Version 2.0 (the "License");
7304 * you may not use this file except in compliance with the License.
7305 * You may obtain a copy of the License at
7306 *
7307 * http://www.apache.org/licenses/LICENSE-2.0
7308 *
7309 * Unless required by applicable law or agreed to in writing, software
7310 * distributed under the License is distributed on an "AS IS" BASIS,
7311 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7312 * See the License for the specific language governing permissions and
7313 * limitations under the License.
7314 */
7315/**
7316 *
7317 * @enum
7318 */
7319var OperationType;
7320(function (OperationType) {
7321 OperationType[OperationType["OVERWRITE"] = 0] = "OVERWRITE";
7322 OperationType[OperationType["MERGE"] = 1] = "MERGE";
7323 OperationType[OperationType["ACK_USER_WRITE"] = 2] = "ACK_USER_WRITE";
7324 OperationType[OperationType["LISTEN_COMPLETE"] = 3] = "LISTEN_COMPLETE";
7325})(OperationType || (OperationType = {}));
7326function newOperationSourceUser() {
7327 return {
7328 fromUser: true,
7329 fromServer: false,
7330 queryId: null,
7331 tagged: false
7332 };
7333}
7334function newOperationSourceServer() {
7335 return {
7336 fromUser: false,
7337 fromServer: true,
7338 queryId: null,
7339 tagged: false
7340 };
7341}
7342function newOperationSourceServerTaggedQuery(queryId) {
7343 return {
7344 fromUser: false,
7345 fromServer: true,
7346 queryId,
7347 tagged: true
7348 };
7349}
7350
7351/**
7352 * @license
7353 * Copyright 2017 Google LLC
7354 *
7355 * Licensed under the Apache License, Version 2.0 (the "License");
7356 * you may not use this file except in compliance with the License.
7357 * You may obtain a copy of the License at
7358 *
7359 * http://www.apache.org/licenses/LICENSE-2.0
7360 *
7361 * Unless required by applicable law or agreed to in writing, software
7362 * distributed under the License is distributed on an "AS IS" BASIS,
7363 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7364 * See the License for the specific language governing permissions and
7365 * limitations under the License.
7366 */
7367class AckUserWrite {
7368 /**
7369 * @param affectedTree - A tree containing true for each affected path. Affected paths can't overlap.
7370 */
7371 constructor(
7372 /** @inheritDoc */ path,
7373 /** @inheritDoc */ affectedTree,
7374 /** @inheritDoc */ revert) {
7375 this.path = path;
7376 this.affectedTree = affectedTree;
7377 this.revert = revert;
7378 /** @inheritDoc */
7379 this.type = OperationType.ACK_USER_WRITE;
7380 /** @inheritDoc */
7381 this.source = newOperationSourceUser();
7382 }
7383 operationForChild(childName) {
7384 if (!pathIsEmpty(this.path)) {
7385 assert(pathGetFront(this.path) === childName, 'operationForChild called for unrelated child.');
7386 return new AckUserWrite(pathPopFront(this.path), this.affectedTree, this.revert);
7387 }
7388 else if (this.affectedTree.value != null) {
7389 assert(this.affectedTree.children.isEmpty(), 'affectedTree should not have overlapping affected paths.');
7390 // All child locations are affected as well; just return same operation.
7391 return this;
7392 }
7393 else {
7394 const childTree = this.affectedTree.subtree(new Path(childName));
7395 return new AckUserWrite(newEmptyPath(), childTree, this.revert);
7396 }
7397 }
7398}
7399
7400/**
7401 * @license
7402 * Copyright 2017 Google LLC
7403 *
7404 * Licensed under the Apache License, Version 2.0 (the "License");
7405 * you may not use this file except in compliance with the License.
7406 * You may obtain a copy of the License at
7407 *
7408 * http://www.apache.org/licenses/LICENSE-2.0
7409 *
7410 * Unless required by applicable law or agreed to in writing, software
7411 * distributed under the License is distributed on an "AS IS" BASIS,
7412 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7413 * See the License for the specific language governing permissions and
7414 * limitations under the License.
7415 */
7416class ListenComplete {
7417 constructor(source, path) {
7418 this.source = source;
7419 this.path = path;
7420 /** @inheritDoc */
7421 this.type = OperationType.LISTEN_COMPLETE;
7422 }
7423 operationForChild(childName) {
7424 if (pathIsEmpty(this.path)) {
7425 return new ListenComplete(this.source, newEmptyPath());
7426 }
7427 else {
7428 return new ListenComplete(this.source, pathPopFront(this.path));
7429 }
7430 }
7431}
7432
7433/**
7434 * @license
7435 * Copyright 2017 Google LLC
7436 *
7437 * Licensed under the Apache License, Version 2.0 (the "License");
7438 * you may not use this file except in compliance with the License.
7439 * You may obtain a copy of the License at
7440 *
7441 * http://www.apache.org/licenses/LICENSE-2.0
7442 *
7443 * Unless required by applicable law or agreed to in writing, software
7444 * distributed under the License is distributed on an "AS IS" BASIS,
7445 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7446 * See the License for the specific language governing permissions and
7447 * limitations under the License.
7448 */
7449class Overwrite {
7450 constructor(source, path, snap) {
7451 this.source = source;
7452 this.path = path;
7453 this.snap = snap;
7454 /** @inheritDoc */
7455 this.type = OperationType.OVERWRITE;
7456 }
7457 operationForChild(childName) {
7458 if (pathIsEmpty(this.path)) {
7459 return new Overwrite(this.source, newEmptyPath(), this.snap.getImmediateChild(childName));
7460 }
7461 else {
7462 return new Overwrite(this.source, pathPopFront(this.path), this.snap);
7463 }
7464 }
7465}
7466
7467/**
7468 * @license
7469 * Copyright 2017 Google LLC
7470 *
7471 * Licensed under the Apache License, Version 2.0 (the "License");
7472 * you may not use this file except in compliance with the License.
7473 * You may obtain a copy of the License at
7474 *
7475 * http://www.apache.org/licenses/LICENSE-2.0
7476 *
7477 * Unless required by applicable law or agreed to in writing, software
7478 * distributed under the License is distributed on an "AS IS" BASIS,
7479 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7480 * See the License for the specific language governing permissions and
7481 * limitations under the License.
7482 */
7483class Merge {
7484 constructor(
7485 /** @inheritDoc */ source,
7486 /** @inheritDoc */ path,
7487 /** @inheritDoc */ children) {
7488 this.source = source;
7489 this.path = path;
7490 this.children = children;
7491 /** @inheritDoc */
7492 this.type = OperationType.MERGE;
7493 }
7494 operationForChild(childName) {
7495 if (pathIsEmpty(this.path)) {
7496 const childTree = this.children.subtree(new Path(childName));
7497 if (childTree.isEmpty()) {
7498 // This child is unaffected
7499 return null;
7500 }
7501 else if (childTree.value) {
7502 // We have a snapshot for the child in question. This becomes an overwrite of the child.
7503 return new Overwrite(this.source, newEmptyPath(), childTree.value);
7504 }
7505 else {
7506 // This is a merge at a deeper level
7507 return new Merge(this.source, newEmptyPath(), childTree);
7508 }
7509 }
7510 else {
7511 assert(pathGetFront(this.path) === childName, "Can't get a merge for a child not on the path of the operation");
7512 return new Merge(this.source, pathPopFront(this.path), this.children);
7513 }
7514 }
7515 toString() {
7516 return ('Operation(' +
7517 this.path +
7518 ': ' +
7519 this.source.toString() +
7520 ' merge: ' +
7521 this.children.toString() +
7522 ')');
7523 }
7524}
7525
7526/**
7527 * @license
7528 * Copyright 2017 Google LLC
7529 *
7530 * Licensed under the Apache License, Version 2.0 (the "License");
7531 * you may not use this file except in compliance with the License.
7532 * You may obtain a copy of the License at
7533 *
7534 * http://www.apache.org/licenses/LICENSE-2.0
7535 *
7536 * Unless required by applicable law or agreed to in writing, software
7537 * distributed under the License is distributed on an "AS IS" BASIS,
7538 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7539 * See the License for the specific language governing permissions and
7540 * limitations under the License.
7541 */
7542/**
7543 * A cache node only stores complete children. Additionally it holds a flag whether the node can be considered fully
7544 * initialized in the sense that we know at one point in time this represented a valid state of the world, e.g.
7545 * initialized with data from the server, or a complete overwrite by the client. The filtered flag also tracks
7546 * whether a node potentially had children removed due to a filter.
7547 */
7548class CacheNode {
7549 constructor(node_, fullyInitialized_, filtered_) {
7550 this.node_ = node_;
7551 this.fullyInitialized_ = fullyInitialized_;
7552 this.filtered_ = filtered_;
7553 }
7554 /**
7555 * Returns whether this node was fully initialized with either server data or a complete overwrite by the client
7556 */
7557 isFullyInitialized() {
7558 return this.fullyInitialized_;
7559 }
7560 /**
7561 * Returns whether this node is potentially missing children due to a filter applied to the node
7562 */
7563 isFiltered() {
7564 return this.filtered_;
7565 }
7566 isCompleteForPath(path) {
7567 if (pathIsEmpty(path)) {
7568 return this.isFullyInitialized() && !this.filtered_;
7569 }
7570 const childKey = pathGetFront(path);
7571 return this.isCompleteForChild(childKey);
7572 }
7573 isCompleteForChild(key) {
7574 return ((this.isFullyInitialized() && !this.filtered_) || this.node_.hasChild(key));
7575 }
7576 getNode() {
7577 return this.node_;
7578 }
7579}
7580
7581/**
7582 * @license
7583 * Copyright 2017 Google LLC
7584 *
7585 * Licensed under the Apache License, Version 2.0 (the "License");
7586 * you may not use this file except in compliance with the License.
7587 * You may obtain a copy of the License at
7588 *
7589 * http://www.apache.org/licenses/LICENSE-2.0
7590 *
7591 * Unless required by applicable law or agreed to in writing, software
7592 * distributed under the License is distributed on an "AS IS" BASIS,
7593 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7594 * See the License for the specific language governing permissions and
7595 * limitations under the License.
7596 */
7597/**
7598 * An EventGenerator is used to convert "raw" changes (Change) as computed by the
7599 * CacheDiffer into actual events (Event) that can be raised. See generateEventsForChanges()
7600 * for details.
7601 *
7602 */
7603class EventGenerator {
7604 constructor(query_) {
7605 this.query_ = query_;
7606 this.index_ = this.query_._queryParams.getIndex();
7607 }
7608}
7609/**
7610 * Given a set of raw changes (no moved events and prevName not specified yet), and a set of
7611 * EventRegistrations that should be notified of these changes, generate the actual events to be raised.
7612 *
7613 * Notes:
7614 * - child_moved events will be synthesized at this time for any child_changed events that affect
7615 * our index.
7616 * - prevName will be calculated based on the index ordering.
7617 */
7618function eventGeneratorGenerateEventsForChanges(eventGenerator, changes, eventCache, eventRegistrations) {
7619 const events = [];
7620 const moves = [];
7621 changes.forEach(change => {
7622 if (change.type === "child_changed" /* CHILD_CHANGED */ &&
7623 eventGenerator.index_.indexedValueChanged(change.oldSnap, change.snapshotNode)) {
7624 moves.push(changeChildMoved(change.childName, change.snapshotNode));
7625 }
7626 });
7627 eventGeneratorGenerateEventsForType(eventGenerator, events, "child_removed" /* CHILD_REMOVED */, changes, eventRegistrations, eventCache);
7628 eventGeneratorGenerateEventsForType(eventGenerator, events, "child_added" /* CHILD_ADDED */, changes, eventRegistrations, eventCache);
7629 eventGeneratorGenerateEventsForType(eventGenerator, events, "child_moved" /* CHILD_MOVED */, moves, eventRegistrations, eventCache);
7630 eventGeneratorGenerateEventsForType(eventGenerator, events, "child_changed" /* CHILD_CHANGED */, changes, eventRegistrations, eventCache);
7631 eventGeneratorGenerateEventsForType(eventGenerator, events, "value" /* VALUE */, changes, eventRegistrations, eventCache);
7632 return events;
7633}
7634/**
7635 * Given changes of a single change type, generate the corresponding events.
7636 */
7637function eventGeneratorGenerateEventsForType(eventGenerator, events, eventType, changes, registrations, eventCache) {
7638 const filteredChanges = changes.filter(change => change.type === eventType);
7639 filteredChanges.sort((a, b) => eventGeneratorCompareChanges(eventGenerator, a, b));
7640 filteredChanges.forEach(change => {
7641 const materializedChange = eventGeneratorMaterializeSingleChange(eventGenerator, change, eventCache);
7642 registrations.forEach(registration => {
7643 if (registration.respondsTo(change.type)) {
7644 events.push(registration.createEvent(materializedChange, eventGenerator.query_));
7645 }
7646 });
7647 });
7648}
7649function eventGeneratorMaterializeSingleChange(eventGenerator, change, eventCache) {
7650 if (change.type === 'value' || change.type === 'child_removed') {
7651 return change;
7652 }
7653 else {
7654 change.prevName = eventCache.getPredecessorChildName(change.childName, change.snapshotNode, eventGenerator.index_);
7655 return change;
7656 }
7657}
7658function eventGeneratorCompareChanges(eventGenerator, a, b) {
7659 if (a.childName == null || b.childName == null) {
7660 throw assertionError('Should only compare child_ events.');
7661 }
7662 const aWrapped = new NamedNode(a.childName, a.snapshotNode);
7663 const bWrapped = new NamedNode(b.childName, b.snapshotNode);
7664 return eventGenerator.index_.compare(aWrapped, bWrapped);
7665}
7666
7667/**
7668 * @license
7669 * Copyright 2017 Google LLC
7670 *
7671 * Licensed under the Apache License, Version 2.0 (the "License");
7672 * you may not use this file except in compliance with the License.
7673 * You may obtain a copy of the License at
7674 *
7675 * http://www.apache.org/licenses/LICENSE-2.0
7676 *
7677 * Unless required by applicable law or agreed to in writing, software
7678 * distributed under the License is distributed on an "AS IS" BASIS,
7679 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7680 * See the License for the specific language governing permissions and
7681 * limitations under the License.
7682 */
7683function newViewCache(eventCache, serverCache) {
7684 return { eventCache, serverCache };
7685}
7686function viewCacheUpdateEventSnap(viewCache, eventSnap, complete, filtered) {
7687 return newViewCache(new CacheNode(eventSnap, complete, filtered), viewCache.serverCache);
7688}
7689function viewCacheUpdateServerSnap(viewCache, serverSnap, complete, filtered) {
7690 return newViewCache(viewCache.eventCache, new CacheNode(serverSnap, complete, filtered));
7691}
7692function viewCacheGetCompleteEventSnap(viewCache) {
7693 return viewCache.eventCache.isFullyInitialized()
7694 ? viewCache.eventCache.getNode()
7695 : null;
7696}
7697function viewCacheGetCompleteServerSnap(viewCache) {
7698 return viewCache.serverCache.isFullyInitialized()
7699 ? viewCache.serverCache.getNode()
7700 : null;
7701}
7702
7703/**
7704 * @license
7705 * Copyright 2017 Google LLC
7706 *
7707 * Licensed under the Apache License, Version 2.0 (the "License");
7708 * you may not use this file except in compliance with the License.
7709 * You may obtain a copy of the License at
7710 *
7711 * http://www.apache.org/licenses/LICENSE-2.0
7712 *
7713 * Unless required by applicable law or agreed to in writing, software
7714 * distributed under the License is distributed on an "AS IS" BASIS,
7715 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7716 * See the License for the specific language governing permissions and
7717 * limitations under the License.
7718 */
7719let emptyChildrenSingleton;
7720/**
7721 * Singleton empty children collection.
7722 *
7723 */
7724const EmptyChildren = () => {
7725 if (!emptyChildrenSingleton) {
7726 emptyChildrenSingleton = new SortedMap(stringCompare);
7727 }
7728 return emptyChildrenSingleton;
7729};
7730/**
7731 * A tree with immutable elements.
7732 */
7733class ImmutableTree {
7734 constructor(value, children = EmptyChildren()) {
7735 this.value = value;
7736 this.children = children;
7737 }
7738 static fromObject(obj) {
7739 let tree = new ImmutableTree(null);
7740 each(obj, (childPath, childSnap) => {
7741 tree = tree.set(new Path(childPath), childSnap);
7742 });
7743 return tree;
7744 }
7745 /**
7746 * True if the value is empty and there are no children
7747 */
7748 isEmpty() {
7749 return this.value === null && this.children.isEmpty();
7750 }
7751 /**
7752 * Given a path and predicate, return the first node and the path to that node
7753 * where the predicate returns true.
7754 *
7755 * TODO Do a perf test -- If we're creating a bunch of `{path: value:}`
7756 * objects on the way back out, it may be better to pass down a pathSoFar obj.
7757 *
7758 * @param relativePath - The remainder of the path
7759 * @param predicate - The predicate to satisfy to return a node
7760 */
7761 findRootMostMatchingPathAndValue(relativePath, predicate) {
7762 if (this.value != null && predicate(this.value)) {
7763 return { path: newEmptyPath(), value: this.value };
7764 }
7765 else {
7766 if (pathIsEmpty(relativePath)) {
7767 return null;
7768 }
7769 else {
7770 const front = pathGetFront(relativePath);
7771 const child = this.children.get(front);
7772 if (child !== null) {
7773 const childExistingPathAndValue = child.findRootMostMatchingPathAndValue(pathPopFront(relativePath), predicate);
7774 if (childExistingPathAndValue != null) {
7775 const fullPath = pathChild(new Path(front), childExistingPathAndValue.path);
7776 return { path: fullPath, value: childExistingPathAndValue.value };
7777 }
7778 else {
7779 return null;
7780 }
7781 }
7782 else {
7783 return null;
7784 }
7785 }
7786 }
7787 }
7788 /**
7789 * Find, if it exists, the shortest subpath of the given path that points a defined
7790 * value in the tree
7791 */
7792 findRootMostValueAndPath(relativePath) {
7793 return this.findRootMostMatchingPathAndValue(relativePath, () => true);
7794 }
7795 /**
7796 * @returns The subtree at the given path
7797 */
7798 subtree(relativePath) {
7799 if (pathIsEmpty(relativePath)) {
7800 return this;
7801 }
7802 else {
7803 const front = pathGetFront(relativePath);
7804 const childTree = this.children.get(front);
7805 if (childTree !== null) {
7806 return childTree.subtree(pathPopFront(relativePath));
7807 }
7808 else {
7809 return new ImmutableTree(null);
7810 }
7811 }
7812 }
7813 /**
7814 * Sets a value at the specified path.
7815 *
7816 * @param relativePath - Path to set value at.
7817 * @param toSet - Value to set.
7818 * @returns Resulting tree.
7819 */
7820 set(relativePath, toSet) {
7821 if (pathIsEmpty(relativePath)) {
7822 return new ImmutableTree(toSet, this.children);
7823 }
7824 else {
7825 const front = pathGetFront(relativePath);
7826 const child = this.children.get(front) || new ImmutableTree(null);
7827 const newChild = child.set(pathPopFront(relativePath), toSet);
7828 const newChildren = this.children.insert(front, newChild);
7829 return new ImmutableTree(this.value, newChildren);
7830 }
7831 }
7832 /**
7833 * Removes the value at the specified path.
7834 *
7835 * @param relativePath - Path to value to remove.
7836 * @returns Resulting tree.
7837 */
7838 remove(relativePath) {
7839 if (pathIsEmpty(relativePath)) {
7840 if (this.children.isEmpty()) {
7841 return new ImmutableTree(null);
7842 }
7843 else {
7844 return new ImmutableTree(null, this.children);
7845 }
7846 }
7847 else {
7848 const front = pathGetFront(relativePath);
7849 const child = this.children.get(front);
7850 if (child) {
7851 const newChild = child.remove(pathPopFront(relativePath));
7852 let newChildren;
7853 if (newChild.isEmpty()) {
7854 newChildren = this.children.remove(front);
7855 }
7856 else {
7857 newChildren = this.children.insert(front, newChild);
7858 }
7859 if (this.value === null && newChildren.isEmpty()) {
7860 return new ImmutableTree(null);
7861 }
7862 else {
7863 return new ImmutableTree(this.value, newChildren);
7864 }
7865 }
7866 else {
7867 return this;
7868 }
7869 }
7870 }
7871 /**
7872 * Gets a value from the tree.
7873 *
7874 * @param relativePath - Path to get value for.
7875 * @returns Value at path, or null.
7876 */
7877 get(relativePath) {
7878 if (pathIsEmpty(relativePath)) {
7879 return this.value;
7880 }
7881 else {
7882 const front = pathGetFront(relativePath);
7883 const child = this.children.get(front);
7884 if (child) {
7885 return child.get(pathPopFront(relativePath));
7886 }
7887 else {
7888 return null;
7889 }
7890 }
7891 }
7892 /**
7893 * Replace the subtree at the specified path with the given new tree.
7894 *
7895 * @param relativePath - Path to replace subtree for.
7896 * @param newTree - New tree.
7897 * @returns Resulting tree.
7898 */
7899 setTree(relativePath, newTree) {
7900 if (pathIsEmpty(relativePath)) {
7901 return newTree;
7902 }
7903 else {
7904 const front = pathGetFront(relativePath);
7905 const child = this.children.get(front) || new ImmutableTree(null);
7906 const newChild = child.setTree(pathPopFront(relativePath), newTree);
7907 let newChildren;
7908 if (newChild.isEmpty()) {
7909 newChildren = this.children.remove(front);
7910 }
7911 else {
7912 newChildren = this.children.insert(front, newChild);
7913 }
7914 return new ImmutableTree(this.value, newChildren);
7915 }
7916 }
7917 /**
7918 * Performs a depth first fold on this tree. Transforms a tree into a single
7919 * value, given a function that operates on the path to a node, an optional
7920 * current value, and a map of child names to folded subtrees
7921 */
7922 fold(fn) {
7923 return this.fold_(newEmptyPath(), fn);
7924 }
7925 /**
7926 * Recursive helper for public-facing fold() method
7927 */
7928 fold_(pathSoFar, fn) {
7929 const accum = {};
7930 this.children.inorderTraversal((childKey, childTree) => {
7931 accum[childKey] = childTree.fold_(pathChild(pathSoFar, childKey), fn);
7932 });
7933 return fn(pathSoFar, this.value, accum);
7934 }
7935 /**
7936 * Find the first matching value on the given path. Return the result of applying f to it.
7937 */
7938 findOnPath(path, f) {
7939 return this.findOnPath_(path, newEmptyPath(), f);
7940 }
7941 findOnPath_(pathToFollow, pathSoFar, f) {
7942 const result = this.value ? f(pathSoFar, this.value) : false;
7943 if (result) {
7944 return result;
7945 }
7946 else {
7947 if (pathIsEmpty(pathToFollow)) {
7948 return null;
7949 }
7950 else {
7951 const front = pathGetFront(pathToFollow);
7952 const nextChild = this.children.get(front);
7953 if (nextChild) {
7954 return nextChild.findOnPath_(pathPopFront(pathToFollow), pathChild(pathSoFar, front), f);
7955 }
7956 else {
7957 return null;
7958 }
7959 }
7960 }
7961 }
7962 foreachOnPath(path, f) {
7963 return this.foreachOnPath_(path, newEmptyPath(), f);
7964 }
7965 foreachOnPath_(pathToFollow, currentRelativePath, f) {
7966 if (pathIsEmpty(pathToFollow)) {
7967 return this;
7968 }
7969 else {
7970 if (this.value) {
7971 f(currentRelativePath, this.value);
7972 }
7973 const front = pathGetFront(pathToFollow);
7974 const nextChild = this.children.get(front);
7975 if (nextChild) {
7976 return nextChild.foreachOnPath_(pathPopFront(pathToFollow), pathChild(currentRelativePath, front), f);
7977 }
7978 else {
7979 return new ImmutableTree(null);
7980 }
7981 }
7982 }
7983 /**
7984 * Calls the given function for each node in the tree that has a value.
7985 *
7986 * @param f - A function to be called with the path from the root of the tree to
7987 * a node, and the value at that node. Called in depth-first order.
7988 */
7989 foreach(f) {
7990 this.foreach_(newEmptyPath(), f);
7991 }
7992 foreach_(currentRelativePath, f) {
7993 this.children.inorderTraversal((childName, childTree) => {
7994 childTree.foreach_(pathChild(currentRelativePath, childName), f);
7995 });
7996 if (this.value) {
7997 f(currentRelativePath, this.value);
7998 }
7999 }
8000 foreachChild(f) {
8001 this.children.inorderTraversal((childName, childTree) => {
8002 if (childTree.value) {
8003 f(childName, childTree.value);
8004 }
8005 });
8006 }
8007}
8008
8009/**
8010 * @license
8011 * Copyright 2017 Google LLC
8012 *
8013 * Licensed under the Apache License, Version 2.0 (the "License");
8014 * you may not use this file except in compliance with the License.
8015 * You may obtain a copy of the License at
8016 *
8017 * http://www.apache.org/licenses/LICENSE-2.0
8018 *
8019 * Unless required by applicable law or agreed to in writing, software
8020 * distributed under the License is distributed on an "AS IS" BASIS,
8021 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
8022 * See the License for the specific language governing permissions and
8023 * limitations under the License.
8024 */
8025/**
8026 * This class holds a collection of writes that can be applied to nodes in unison. It abstracts away the logic with
8027 * dealing with priority writes and multiple nested writes. At any given path there is only allowed to be one write
8028 * modifying that path. Any write to an existing path or shadowing an existing path will modify that existing write
8029 * to reflect the write added.
8030 */
8031class CompoundWrite {
8032 constructor(writeTree_) {
8033 this.writeTree_ = writeTree_;
8034 }
8035 static empty() {
8036 return new CompoundWrite(new ImmutableTree(null));
8037 }
8038}
8039function compoundWriteAddWrite(compoundWrite, path, node) {
8040 if (pathIsEmpty(path)) {
8041 return new CompoundWrite(new ImmutableTree(node));
8042 }
8043 else {
8044 const rootmost = compoundWrite.writeTree_.findRootMostValueAndPath(path);
8045 if (rootmost != null) {
8046 const rootMostPath = rootmost.path;
8047 let value = rootmost.value;
8048 const relativePath = newRelativePath(rootMostPath, path);
8049 value = value.updateChild(relativePath, node);
8050 return new CompoundWrite(compoundWrite.writeTree_.set(rootMostPath, value));
8051 }
8052 else {
8053 const subtree = new ImmutableTree(node);
8054 const newWriteTree = compoundWrite.writeTree_.setTree(path, subtree);
8055 return new CompoundWrite(newWriteTree);
8056 }
8057 }
8058}
8059function compoundWriteAddWrites(compoundWrite, path, updates) {
8060 let newWrite = compoundWrite;
8061 each(updates, (childKey, node) => {
8062 newWrite = compoundWriteAddWrite(newWrite, pathChild(path, childKey), node);
8063 });
8064 return newWrite;
8065}
8066/**
8067 * Will remove a write at the given path and deeper paths. This will <em>not</em> modify a write at a higher
8068 * location, which must be removed by calling this method with that path.
8069 *
8070 * @param compoundWrite - The CompoundWrite to remove.
8071 * @param path - The path at which a write and all deeper writes should be removed
8072 * @returns The new CompoundWrite with the removed path
8073 */
8074function compoundWriteRemoveWrite(compoundWrite, path) {
8075 if (pathIsEmpty(path)) {
8076 return CompoundWrite.empty();
8077 }
8078 else {
8079 const newWriteTree = compoundWrite.writeTree_.setTree(path, new ImmutableTree(null));
8080 return new CompoundWrite(newWriteTree);
8081 }
8082}
8083/**
8084 * Returns whether this CompoundWrite will fully overwrite a node at a given location and can therefore be
8085 * considered "complete".
8086 *
8087 * @param compoundWrite - The CompoundWrite to check.
8088 * @param path - The path to check for
8089 * @returns Whether there is a complete write at that path
8090 */
8091function compoundWriteHasCompleteWrite(compoundWrite, path) {
8092 return compoundWriteGetCompleteNode(compoundWrite, path) != null;
8093}
8094/**
8095 * Returns a node for a path if and only if the node is a "complete" overwrite at that path. This will not aggregate
8096 * writes from deeper paths, but will return child nodes from a more shallow path.
8097 *
8098 * @param compoundWrite - The CompoundWrite to get the node from.
8099 * @param path - The path to get a complete write
8100 * @returns The node if complete at that path, or null otherwise.
8101 */
8102function compoundWriteGetCompleteNode(compoundWrite, path) {
8103 const rootmost = compoundWrite.writeTree_.findRootMostValueAndPath(path);
8104 if (rootmost != null) {
8105 return compoundWrite.writeTree_
8106 .get(rootmost.path)
8107 .getChild(newRelativePath(rootmost.path, path));
8108 }
8109 else {
8110 return null;
8111 }
8112}
8113/**
8114 * Returns all children that are guaranteed to be a complete overwrite.
8115 *
8116 * @param compoundWrite - The CompoundWrite to get children from.
8117 * @returns A list of all complete children.
8118 */
8119function compoundWriteGetCompleteChildren(compoundWrite) {
8120 const children = [];
8121 const node = compoundWrite.writeTree_.value;
8122 if (node != null) {
8123 // If it's a leaf node, it has no children; so nothing to do.
8124 if (!node.isLeafNode()) {
8125 node.forEachChild(PRIORITY_INDEX, (childName, childNode) => {
8126 children.push(new NamedNode(childName, childNode));
8127 });
8128 }
8129 }
8130 else {
8131 compoundWrite.writeTree_.children.inorderTraversal((childName, childTree) => {
8132 if (childTree.value != null) {
8133 children.push(new NamedNode(childName, childTree.value));
8134 }
8135 });
8136 }
8137 return children;
8138}
8139function compoundWriteChildCompoundWrite(compoundWrite, path) {
8140 if (pathIsEmpty(path)) {
8141 return compoundWrite;
8142 }
8143 else {
8144 const shadowingNode = compoundWriteGetCompleteNode(compoundWrite, path);
8145 if (shadowingNode != null) {
8146 return new CompoundWrite(new ImmutableTree(shadowingNode));
8147 }
8148 else {
8149 return new CompoundWrite(compoundWrite.writeTree_.subtree(path));
8150 }
8151 }
8152}
8153/**
8154 * Returns true if this CompoundWrite is empty and therefore does not modify any nodes.
8155 * @returns Whether this CompoundWrite is empty
8156 */
8157function compoundWriteIsEmpty(compoundWrite) {
8158 return compoundWrite.writeTree_.isEmpty();
8159}
8160/**
8161 * Applies this CompoundWrite to a node. The node is returned with all writes from this CompoundWrite applied to the
8162 * node
8163 * @param node - The node to apply this CompoundWrite to
8164 * @returns The node with all writes applied
8165 */
8166function compoundWriteApply(compoundWrite, node) {
8167 return applySubtreeWrite(newEmptyPath(), compoundWrite.writeTree_, node);
8168}
8169function applySubtreeWrite(relativePath, writeTree, node) {
8170 if (writeTree.value != null) {
8171 // Since there a write is always a leaf, we're done here
8172 return node.updateChild(relativePath, writeTree.value);
8173 }
8174 else {
8175 let priorityWrite = null;
8176 writeTree.children.inorderTraversal((childKey, childTree) => {
8177 if (childKey === '.priority') {
8178 // Apply priorities at the end so we don't update priorities for either empty nodes or forget
8179 // to apply priorities to empty nodes that are later filled
8180 assert(childTree.value !== null, 'Priority writes must always be leaf nodes');
8181 priorityWrite = childTree.value;
8182 }
8183 else {
8184 node = applySubtreeWrite(pathChild(relativePath, childKey), childTree, node);
8185 }
8186 });
8187 // If there was a priority write, we only apply it if the node is not empty
8188 if (!node.getChild(relativePath).isEmpty() && priorityWrite !== null) {
8189 node = node.updateChild(pathChild(relativePath, '.priority'), priorityWrite);
8190 }
8191 return node;
8192 }
8193}
8194
8195/**
8196 * @license
8197 * Copyright 2017 Google LLC
8198 *
8199 * Licensed under the Apache License, Version 2.0 (the "License");
8200 * you may not use this file except in compliance with the License.
8201 * You may obtain a copy of the License at
8202 *
8203 * http://www.apache.org/licenses/LICENSE-2.0
8204 *
8205 * Unless required by applicable law or agreed to in writing, software
8206 * distributed under the License is distributed on an "AS IS" BASIS,
8207 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
8208 * See the License for the specific language governing permissions and
8209 * limitations under the License.
8210 */
8211/**
8212 * Create a new WriteTreeRef for the given path. For use with a new sync point at the given path.
8213 *
8214 */
8215function writeTreeChildWrites(writeTree, path) {
8216 return newWriteTreeRef(path, writeTree);
8217}
8218/**
8219 * Record a new overwrite from user code.
8220 *
8221 * @param visible - This is set to false by some transactions. It should be excluded from event caches
8222 */
8223function writeTreeAddOverwrite(writeTree, path, snap, writeId, visible) {
8224 assert(writeId > writeTree.lastWriteId, 'Stacking an older write on top of newer ones');
8225 if (visible === undefined) {
8226 visible = true;
8227 }
8228 writeTree.allWrites.push({
8229 path,
8230 snap,
8231 writeId,
8232 visible
8233 });
8234 if (visible) {
8235 writeTree.visibleWrites = compoundWriteAddWrite(writeTree.visibleWrites, path, snap);
8236 }
8237 writeTree.lastWriteId = writeId;
8238}
8239/**
8240 * Record a new merge from user code.
8241 */
8242function writeTreeAddMerge(writeTree, path, changedChildren, writeId) {
8243 assert(writeId > writeTree.lastWriteId, 'Stacking an older merge on top of newer ones');
8244 writeTree.allWrites.push({
8245 path,
8246 children: changedChildren,
8247 writeId,
8248 visible: true
8249 });
8250 writeTree.visibleWrites = compoundWriteAddWrites(writeTree.visibleWrites, path, changedChildren);
8251 writeTree.lastWriteId = writeId;
8252}
8253function writeTreeGetWrite(writeTree, writeId) {
8254 for (let i = 0; i < writeTree.allWrites.length; i++) {
8255 const record = writeTree.allWrites[i];
8256 if (record.writeId === writeId) {
8257 return record;
8258 }
8259 }
8260 return null;
8261}
8262/**
8263 * Remove a write (either an overwrite or merge) that has been successfully acknowledge by the server. Recalculates
8264 * the tree if necessary. We return true if it may have been visible, meaning views need to reevaluate.
8265 *
8266 * @returns true if the write may have been visible (meaning we'll need to reevaluate / raise
8267 * events as a result).
8268 */
8269function writeTreeRemoveWrite(writeTree, writeId) {
8270 // Note: disabling this check. It could be a transaction that preempted another transaction, and thus was applied
8271 // out of order.
8272 //const validClear = revert || this.allWrites_.length === 0 || writeId <= this.allWrites_[0].writeId;
8273 //assert(validClear, "Either we don't have this write, or it's the first one in the queue");
8274 const idx = writeTree.allWrites.findIndex(s => {
8275 return s.writeId === writeId;
8276 });
8277 assert(idx >= 0, 'removeWrite called with nonexistent writeId.');
8278 const writeToRemove = writeTree.allWrites[idx];
8279 writeTree.allWrites.splice(idx, 1);
8280 let removedWriteWasVisible = writeToRemove.visible;
8281 let removedWriteOverlapsWithOtherWrites = false;
8282 let i = writeTree.allWrites.length - 1;
8283 while (removedWriteWasVisible && i >= 0) {
8284 const currentWrite = writeTree.allWrites[i];
8285 if (currentWrite.visible) {
8286 if (i >= idx &&
8287 writeTreeRecordContainsPath_(currentWrite, writeToRemove.path)) {
8288 // The removed write was completely shadowed by a subsequent write.
8289 removedWriteWasVisible = false;
8290 }
8291 else if (pathContains(writeToRemove.path, currentWrite.path)) {
8292 // Either we're covering some writes or they're covering part of us (depending on which came first).
8293 removedWriteOverlapsWithOtherWrites = true;
8294 }
8295 }
8296 i--;
8297 }
8298 if (!removedWriteWasVisible) {
8299 return false;
8300 }
8301 else if (removedWriteOverlapsWithOtherWrites) {
8302 // There's some shadowing going on. Just rebuild the visible writes from scratch.
8303 writeTreeResetTree_(writeTree);
8304 return true;
8305 }
8306 else {
8307 // There's no shadowing. We can safely just remove the write(s) from visibleWrites.
8308 if (writeToRemove.snap) {
8309 writeTree.visibleWrites = compoundWriteRemoveWrite(writeTree.visibleWrites, writeToRemove.path);
8310 }
8311 else {
8312 const children = writeToRemove.children;
8313 each(children, (childName) => {
8314 writeTree.visibleWrites = compoundWriteRemoveWrite(writeTree.visibleWrites, pathChild(writeToRemove.path, childName));
8315 });
8316 }
8317 return true;
8318 }
8319}
8320function writeTreeRecordContainsPath_(writeRecord, path) {
8321 if (writeRecord.snap) {
8322 return pathContains(writeRecord.path, path);
8323 }
8324 else {
8325 for (const childName in writeRecord.children) {
8326 if (writeRecord.children.hasOwnProperty(childName) &&
8327 pathContains(pathChild(writeRecord.path, childName), path)) {
8328 return true;
8329 }
8330 }
8331 return false;
8332 }
8333}
8334/**
8335 * Re-layer the writes and merges into a tree so we can efficiently calculate event snapshots
8336 */
8337function writeTreeResetTree_(writeTree) {
8338 writeTree.visibleWrites = writeTreeLayerTree_(writeTree.allWrites, writeTreeDefaultFilter_, newEmptyPath());
8339 if (writeTree.allWrites.length > 0) {
8340 writeTree.lastWriteId =
8341 writeTree.allWrites[writeTree.allWrites.length - 1].writeId;
8342 }
8343 else {
8344 writeTree.lastWriteId = -1;
8345 }
8346}
8347/**
8348 * The default filter used when constructing the tree. Keep everything that's visible.
8349 */
8350function writeTreeDefaultFilter_(write) {
8351 return write.visible;
8352}
8353/**
8354 * Static method. Given an array of WriteRecords, a filter for which ones to include, and a path, construct the tree of
8355 * event data at that path.
8356 */
8357function writeTreeLayerTree_(writes, filter, treeRoot) {
8358 let compoundWrite = CompoundWrite.empty();
8359 for (let i = 0; i < writes.length; ++i) {
8360 const write = writes[i];
8361 // Theory, a later set will either:
8362 // a) abort a relevant transaction, so no need to worry about excluding it from calculating that transaction
8363 // b) not be relevant to a transaction (separate branch), so again will not affect the data for that transaction
8364 if (filter(write)) {
8365 const writePath = write.path;
8366 let relativePath;
8367 if (write.snap) {
8368 if (pathContains(treeRoot, writePath)) {
8369 relativePath = newRelativePath(treeRoot, writePath);
8370 compoundWrite = compoundWriteAddWrite(compoundWrite, relativePath, write.snap);
8371 }
8372 else if (pathContains(writePath, treeRoot)) {
8373 relativePath = newRelativePath(writePath, treeRoot);
8374 compoundWrite = compoundWriteAddWrite(compoundWrite, newEmptyPath(), write.snap.getChild(relativePath));
8375 }
8376 else ;
8377 }
8378 else if (write.children) {
8379 if (pathContains(treeRoot, writePath)) {
8380 relativePath = newRelativePath(treeRoot, writePath);
8381 compoundWrite = compoundWriteAddWrites(compoundWrite, relativePath, write.children);
8382 }
8383 else if (pathContains(writePath, treeRoot)) {
8384 relativePath = newRelativePath(writePath, treeRoot);
8385 if (pathIsEmpty(relativePath)) {
8386 compoundWrite = compoundWriteAddWrites(compoundWrite, newEmptyPath(), write.children);
8387 }
8388 else {
8389 const child = safeGet(write.children, pathGetFront(relativePath));
8390 if (child) {
8391 // There exists a child in this node that matches the root path
8392 const deepNode = child.getChild(pathPopFront(relativePath));
8393 compoundWrite = compoundWriteAddWrite(compoundWrite, newEmptyPath(), deepNode);
8394 }
8395 }
8396 }
8397 else ;
8398 }
8399 else {
8400 throw assertionError('WriteRecord should have .snap or .children');
8401 }
8402 }
8403 }
8404 return compoundWrite;
8405}
8406/**
8407 * Given optional, underlying server data, and an optional set of constraints (exclude some sets, include hidden
8408 * writes), attempt to calculate a complete snapshot for the given path
8409 *
8410 * @param writeIdsToExclude - An optional set to be excluded
8411 * @param includeHiddenWrites - Defaults to false, whether or not to layer on writes with visible set to false
8412 */
8413function writeTreeCalcCompleteEventCache(writeTree, treePath, completeServerCache, writeIdsToExclude, includeHiddenWrites) {
8414 if (!writeIdsToExclude && !includeHiddenWrites) {
8415 const shadowingNode = compoundWriteGetCompleteNode(writeTree.visibleWrites, treePath);
8416 if (shadowingNode != null) {
8417 return shadowingNode;
8418 }
8419 else {
8420 const subMerge = compoundWriteChildCompoundWrite(writeTree.visibleWrites, treePath);
8421 if (compoundWriteIsEmpty(subMerge)) {
8422 return completeServerCache;
8423 }
8424 else if (completeServerCache == null &&
8425 !compoundWriteHasCompleteWrite(subMerge, newEmptyPath())) {
8426 // We wouldn't have a complete snapshot, since there's no underlying data and no complete shadow
8427 return null;
8428 }
8429 else {
8430 const layeredCache = completeServerCache || ChildrenNode.EMPTY_NODE;
8431 return compoundWriteApply(subMerge, layeredCache);
8432 }
8433 }
8434 }
8435 else {
8436 const merge = compoundWriteChildCompoundWrite(writeTree.visibleWrites, treePath);
8437 if (!includeHiddenWrites && compoundWriteIsEmpty(merge)) {
8438 return completeServerCache;
8439 }
8440 else {
8441 // If the server cache is null, and we don't have a complete cache, we need to return null
8442 if (!includeHiddenWrites &&
8443 completeServerCache == null &&
8444 !compoundWriteHasCompleteWrite(merge, newEmptyPath())) {
8445 return null;
8446 }
8447 else {
8448 const filter = function (write) {
8449 return ((write.visible || includeHiddenWrites) &&
8450 (!writeIdsToExclude ||
8451 !~writeIdsToExclude.indexOf(write.writeId)) &&
8452 (pathContains(write.path, treePath) ||
8453 pathContains(treePath, write.path)));
8454 };
8455 const mergeAtPath = writeTreeLayerTree_(writeTree.allWrites, filter, treePath);
8456 const layeredCache = completeServerCache || ChildrenNode.EMPTY_NODE;
8457 return compoundWriteApply(mergeAtPath, layeredCache);
8458 }
8459 }
8460 }
8461}
8462/**
8463 * With optional, underlying server data, attempt to return a children node of children that we have complete data for.
8464 * Used when creating new views, to pre-fill their complete event children snapshot.
8465 */
8466function writeTreeCalcCompleteEventChildren(writeTree, treePath, completeServerChildren) {
8467 let completeChildren = ChildrenNode.EMPTY_NODE;
8468 const topLevelSet = compoundWriteGetCompleteNode(writeTree.visibleWrites, treePath);
8469 if (topLevelSet) {
8470 if (!topLevelSet.isLeafNode()) {
8471 // we're shadowing everything. Return the children.
8472 topLevelSet.forEachChild(PRIORITY_INDEX, (childName, childSnap) => {
8473 completeChildren = completeChildren.updateImmediateChild(childName, childSnap);
8474 });
8475 }
8476 return completeChildren;
8477 }
8478 else if (completeServerChildren) {
8479 // Layer any children we have on top of this
8480 // We know we don't have a top-level set, so just enumerate existing children
8481 const merge = compoundWriteChildCompoundWrite(writeTree.visibleWrites, treePath);
8482 completeServerChildren.forEachChild(PRIORITY_INDEX, (childName, childNode) => {
8483 const node = compoundWriteApply(compoundWriteChildCompoundWrite(merge, new Path(childName)), childNode);
8484 completeChildren = completeChildren.updateImmediateChild(childName, node);
8485 });
8486 // Add any complete children we have from the set
8487 compoundWriteGetCompleteChildren(merge).forEach(namedNode => {
8488 completeChildren = completeChildren.updateImmediateChild(namedNode.name, namedNode.node);
8489 });
8490 return completeChildren;
8491 }
8492 else {
8493 // We don't have anything to layer on top of. Layer on any children we have
8494 // Note that we can return an empty snap if we have a defined delete
8495 const merge = compoundWriteChildCompoundWrite(writeTree.visibleWrites, treePath);
8496 compoundWriteGetCompleteChildren(merge).forEach(namedNode => {
8497 completeChildren = completeChildren.updateImmediateChild(namedNode.name, namedNode.node);
8498 });
8499 return completeChildren;
8500 }
8501}
8502/**
8503 * Given that the underlying server data has updated, determine what, if anything, needs to be
8504 * applied to the event cache.
8505 *
8506 * Possibilities:
8507 *
8508 * 1. No writes are shadowing. Events should be raised, the snap to be applied comes from the server data
8509 *
8510 * 2. Some write is completely shadowing. No events to be raised
8511 *
8512 * 3. Is partially shadowed. Events
8513 *
8514 * Either existingEventSnap or existingServerSnap must exist
8515 */
8516function writeTreeCalcEventCacheAfterServerOverwrite(writeTree, treePath, childPath, existingEventSnap, existingServerSnap) {
8517 assert(existingEventSnap || existingServerSnap, 'Either existingEventSnap or existingServerSnap must exist');
8518 const path = pathChild(treePath, childPath);
8519 if (compoundWriteHasCompleteWrite(writeTree.visibleWrites, path)) {
8520 // At this point we can probably guarantee that we're in case 2, meaning no events
8521 // May need to check visibility while doing the findRootMostValueAndPath call
8522 return null;
8523 }
8524 else {
8525 // No complete shadowing. We're either partially shadowing or not shadowing at all.
8526 const childMerge = compoundWriteChildCompoundWrite(writeTree.visibleWrites, path);
8527 if (compoundWriteIsEmpty(childMerge)) {
8528 // We're not shadowing at all. Case 1
8529 return existingServerSnap.getChild(childPath);
8530 }
8531 else {
8532 // This could be more efficient if the serverNode + updates doesn't change the eventSnap
8533 // However this is tricky to find out, since user updates don't necessary change the server
8534 // snap, e.g. priority updates on empty nodes, or deep deletes. Another special case is if the server
8535 // adds nodes, but doesn't change any existing writes. It is therefore not enough to
8536 // only check if the updates change the serverNode.
8537 // Maybe check if the merge tree contains these special cases and only do a full overwrite in that case?
8538 return compoundWriteApply(childMerge, existingServerSnap.getChild(childPath));
8539 }
8540 }
8541}
8542/**
8543 * Returns a complete child for a given server snap after applying all user writes or null if there is no
8544 * complete child for this ChildKey.
8545 */
8546function writeTreeCalcCompleteChild(writeTree, treePath, childKey, existingServerSnap) {
8547 const path = pathChild(treePath, childKey);
8548 const shadowingNode = compoundWriteGetCompleteNode(writeTree.visibleWrites, path);
8549 if (shadowingNode != null) {
8550 return shadowingNode;
8551 }
8552 else {
8553 if (existingServerSnap.isCompleteForChild(childKey)) {
8554 const childMerge = compoundWriteChildCompoundWrite(writeTree.visibleWrites, path);
8555 return compoundWriteApply(childMerge, existingServerSnap.getNode().getImmediateChild(childKey));
8556 }
8557 else {
8558 return null;
8559 }
8560 }
8561}
8562/**
8563 * Returns a node if there is a complete overwrite for this path. More specifically, if there is a write at
8564 * a higher path, this will return the child of that write relative to the write and this path.
8565 * Returns null if there is no write at this path.
8566 */
8567function writeTreeShadowingWrite(writeTree, path) {
8568 return compoundWriteGetCompleteNode(writeTree.visibleWrites, path);
8569}
8570/**
8571 * This method is used when processing child remove events on a query. If we can, we pull in children that were outside
8572 * the window, but may now be in the window.
8573 */
8574function writeTreeCalcIndexedSlice(writeTree, treePath, completeServerData, startPost, count, reverse, index) {
8575 let toIterate;
8576 const merge = compoundWriteChildCompoundWrite(writeTree.visibleWrites, treePath);
8577 const shadowingNode = compoundWriteGetCompleteNode(merge, newEmptyPath());
8578 if (shadowingNode != null) {
8579 toIterate = shadowingNode;
8580 }
8581 else if (completeServerData != null) {
8582 toIterate = compoundWriteApply(merge, completeServerData);
8583 }
8584 else {
8585 // no children to iterate on
8586 return [];
8587 }
8588 toIterate = toIterate.withIndex(index);
8589 if (!toIterate.isEmpty() && !toIterate.isLeafNode()) {
8590 const nodes = [];
8591 const cmp = index.getCompare();
8592 const iter = reverse
8593 ? toIterate.getReverseIteratorFrom(startPost, index)
8594 : toIterate.getIteratorFrom(startPost, index);
8595 let next = iter.getNext();
8596 while (next && nodes.length < count) {
8597 if (cmp(next, startPost) !== 0) {
8598 nodes.push(next);
8599 }
8600 next = iter.getNext();
8601 }
8602 return nodes;
8603 }
8604 else {
8605 return [];
8606 }
8607}
8608function newWriteTree() {
8609 return {
8610 visibleWrites: CompoundWrite.empty(),
8611 allWrites: [],
8612 lastWriteId: -1
8613 };
8614}
8615/**
8616 * If possible, returns a complete event cache, using the underlying server data if possible. In addition, can be used
8617 * to get a cache that includes hidden writes, and excludes arbitrary writes. Note that customizing the returned node
8618 * can lead to a more expensive calculation.
8619 *
8620 * @param writeIdsToExclude - Optional writes to exclude.
8621 * @param includeHiddenWrites - Defaults to false, whether or not to layer on writes with visible set to false
8622 */
8623function writeTreeRefCalcCompleteEventCache(writeTreeRef, completeServerCache, writeIdsToExclude, includeHiddenWrites) {
8624 return writeTreeCalcCompleteEventCache(writeTreeRef.writeTree, writeTreeRef.treePath, completeServerCache, writeIdsToExclude, includeHiddenWrites);
8625}
8626/**
8627 * If possible, returns a children node containing all of the complete children we have data for. The returned data is a
8628 * mix of the given server data and write data.
8629 *
8630 */
8631function writeTreeRefCalcCompleteEventChildren(writeTreeRef, completeServerChildren) {
8632 return writeTreeCalcCompleteEventChildren(writeTreeRef.writeTree, writeTreeRef.treePath, completeServerChildren);
8633}
8634/**
8635 * Given that either the underlying server data has updated or the outstanding writes have updated, determine what,
8636 * if anything, needs to be applied to the event cache.
8637 *
8638 * Possibilities:
8639 *
8640 * 1. No writes are shadowing. Events should be raised, the snap to be applied comes from the server data
8641 *
8642 * 2. Some write is completely shadowing. No events to be raised
8643 *
8644 * 3. Is partially shadowed. Events should be raised
8645 *
8646 * Either existingEventSnap or existingServerSnap must exist, this is validated via an assert
8647 *
8648 *
8649 */
8650function writeTreeRefCalcEventCacheAfterServerOverwrite(writeTreeRef, path, existingEventSnap, existingServerSnap) {
8651 return writeTreeCalcEventCacheAfterServerOverwrite(writeTreeRef.writeTree, writeTreeRef.treePath, path, existingEventSnap, existingServerSnap);
8652}
8653/**
8654 * Returns a node if there is a complete overwrite for this path. More specifically, if there is a write at
8655 * a higher path, this will return the child of that write relative to the write and this path.
8656 * Returns null if there is no write at this path.
8657 *
8658 */
8659function writeTreeRefShadowingWrite(writeTreeRef, path) {
8660 return writeTreeShadowingWrite(writeTreeRef.writeTree, pathChild(writeTreeRef.treePath, path));
8661}
8662/**
8663 * This method is used when processing child remove events on a query. If we can, we pull in children that were outside
8664 * the window, but may now be in the window
8665 */
8666function writeTreeRefCalcIndexedSlice(writeTreeRef, completeServerData, startPost, count, reverse, index) {
8667 return writeTreeCalcIndexedSlice(writeTreeRef.writeTree, writeTreeRef.treePath, completeServerData, startPost, count, reverse, index);
8668}
8669/**
8670 * Returns a complete child for a given server snap after applying all user writes or null if there is no
8671 * complete child for this ChildKey.
8672 */
8673function writeTreeRefCalcCompleteChild(writeTreeRef, childKey, existingServerCache) {
8674 return writeTreeCalcCompleteChild(writeTreeRef.writeTree, writeTreeRef.treePath, childKey, existingServerCache);
8675}
8676/**
8677 * Return a WriteTreeRef for a child.
8678 */
8679function writeTreeRefChild(writeTreeRef, childName) {
8680 return newWriteTreeRef(pathChild(writeTreeRef.treePath, childName), writeTreeRef.writeTree);
8681}
8682function newWriteTreeRef(path, writeTree) {
8683 return {
8684 treePath: path,
8685 writeTree
8686 };
8687}
8688
8689/**
8690 * @license
8691 * Copyright 2017 Google LLC
8692 *
8693 * Licensed under the Apache License, Version 2.0 (the "License");
8694 * you may not use this file except in compliance with the License.
8695 * You may obtain a copy of the License at
8696 *
8697 * http://www.apache.org/licenses/LICENSE-2.0
8698 *
8699 * Unless required by applicable law or agreed to in writing, software
8700 * distributed under the License is distributed on an "AS IS" BASIS,
8701 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
8702 * See the License for the specific language governing permissions and
8703 * limitations under the License.
8704 */
8705class ChildChangeAccumulator {
8706 constructor() {
8707 this.changeMap = new Map();
8708 }
8709 trackChildChange(change) {
8710 const type = change.type;
8711 const childKey = change.childName;
8712 assert(type === "child_added" /* CHILD_ADDED */ ||
8713 type === "child_changed" /* CHILD_CHANGED */ ||
8714 type === "child_removed" /* CHILD_REMOVED */, 'Only child changes supported for tracking');
8715 assert(childKey !== '.priority', 'Only non-priority child changes can be tracked.');
8716 const oldChange = this.changeMap.get(childKey);
8717 if (oldChange) {
8718 const oldType = oldChange.type;
8719 if (type === "child_added" /* CHILD_ADDED */ &&
8720 oldType === "child_removed" /* CHILD_REMOVED */) {
8721 this.changeMap.set(childKey, changeChildChanged(childKey, change.snapshotNode, oldChange.snapshotNode));
8722 }
8723 else if (type === "child_removed" /* CHILD_REMOVED */ &&
8724 oldType === "child_added" /* CHILD_ADDED */) {
8725 this.changeMap.delete(childKey);
8726 }
8727 else if (type === "child_removed" /* CHILD_REMOVED */ &&
8728 oldType === "child_changed" /* CHILD_CHANGED */) {
8729 this.changeMap.set(childKey, changeChildRemoved(childKey, oldChange.oldSnap));
8730 }
8731 else if (type === "child_changed" /* CHILD_CHANGED */ &&
8732 oldType === "child_added" /* CHILD_ADDED */) {
8733 this.changeMap.set(childKey, changeChildAdded(childKey, change.snapshotNode));
8734 }
8735 else if (type === "child_changed" /* CHILD_CHANGED */ &&
8736 oldType === "child_changed" /* CHILD_CHANGED */) {
8737 this.changeMap.set(childKey, changeChildChanged(childKey, change.snapshotNode, oldChange.oldSnap));
8738 }
8739 else {
8740 throw assertionError('Illegal combination of changes: ' +
8741 change +
8742 ' occurred after ' +
8743 oldChange);
8744 }
8745 }
8746 else {
8747 this.changeMap.set(childKey, change);
8748 }
8749 }
8750 getChanges() {
8751 return Array.from(this.changeMap.values());
8752 }
8753}
8754
8755/**
8756 * @license
8757 * Copyright 2017 Google LLC
8758 *
8759 * Licensed under the Apache License, Version 2.0 (the "License");
8760 * you may not use this file except in compliance with the License.
8761 * You may obtain a copy of the License at
8762 *
8763 * http://www.apache.org/licenses/LICENSE-2.0
8764 *
8765 * Unless required by applicable law or agreed to in writing, software
8766 * distributed under the License is distributed on an "AS IS" BASIS,
8767 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
8768 * See the License for the specific language governing permissions and
8769 * limitations under the License.
8770 */
8771/**
8772 * An implementation of CompleteChildSource that never returns any additional children
8773 */
8774// eslint-disable-next-line @typescript-eslint/naming-convention
8775class NoCompleteChildSource_ {
8776 getCompleteChild(childKey) {
8777 return null;
8778 }
8779 getChildAfterChild(index, child, reverse) {
8780 return null;
8781 }
8782}
8783/**
8784 * Singleton instance.
8785 */
8786const NO_COMPLETE_CHILD_SOURCE = new NoCompleteChildSource_();
8787/**
8788 * An implementation of CompleteChildSource that uses a WriteTree in addition to any other server data or
8789 * old event caches available to calculate complete children.
8790 */
8791class WriteTreeCompleteChildSource {
8792 constructor(writes_, viewCache_, optCompleteServerCache_ = null) {
8793 this.writes_ = writes_;
8794 this.viewCache_ = viewCache_;
8795 this.optCompleteServerCache_ = optCompleteServerCache_;
8796 }
8797 getCompleteChild(childKey) {
8798 const node = this.viewCache_.eventCache;
8799 if (node.isCompleteForChild(childKey)) {
8800 return node.getNode().getImmediateChild(childKey);
8801 }
8802 else {
8803 const serverNode = this.optCompleteServerCache_ != null
8804 ? new CacheNode(this.optCompleteServerCache_, true, false)
8805 : this.viewCache_.serverCache;
8806 return writeTreeRefCalcCompleteChild(this.writes_, childKey, serverNode);
8807 }
8808 }
8809 getChildAfterChild(index, child, reverse) {
8810 const completeServerData = this.optCompleteServerCache_ != null
8811 ? this.optCompleteServerCache_
8812 : viewCacheGetCompleteServerSnap(this.viewCache_);
8813 const nodes = writeTreeRefCalcIndexedSlice(this.writes_, completeServerData, child, 1, reverse, index);
8814 if (nodes.length === 0) {
8815 return null;
8816 }
8817 else {
8818 return nodes[0];
8819 }
8820 }
8821}
8822
8823/**
8824 * @license
8825 * Copyright 2017 Google LLC
8826 *
8827 * Licensed under the Apache License, Version 2.0 (the "License");
8828 * you may not use this file except in compliance with the License.
8829 * You may obtain a copy of the License at
8830 *
8831 * http://www.apache.org/licenses/LICENSE-2.0
8832 *
8833 * Unless required by applicable law or agreed to in writing, software
8834 * distributed under the License is distributed on an "AS IS" BASIS,
8835 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
8836 * See the License for the specific language governing permissions and
8837 * limitations under the License.
8838 */
8839function newViewProcessor(filter) {
8840 return { filter };
8841}
8842function viewProcessorAssertIndexed(viewProcessor, viewCache) {
8843 assert(viewCache.eventCache.getNode().isIndexed(viewProcessor.filter.getIndex()), 'Event snap not indexed');
8844 assert(viewCache.serverCache.getNode().isIndexed(viewProcessor.filter.getIndex()), 'Server snap not indexed');
8845}
8846function viewProcessorApplyOperation(viewProcessor, oldViewCache, operation, writesCache, completeCache) {
8847 const accumulator = new ChildChangeAccumulator();
8848 let newViewCache, filterServerNode;
8849 if (operation.type === OperationType.OVERWRITE) {
8850 const overwrite = operation;
8851 if (overwrite.source.fromUser) {
8852 newViewCache = viewProcessorApplyUserOverwrite(viewProcessor, oldViewCache, overwrite.path, overwrite.snap, writesCache, completeCache, accumulator);
8853 }
8854 else {
8855 assert(overwrite.source.fromServer, 'Unknown source.');
8856 // We filter the node if it's a tagged update or the node has been previously filtered and the
8857 // update is not at the root in which case it is ok (and necessary) to mark the node unfiltered
8858 // again
8859 filterServerNode =
8860 overwrite.source.tagged ||
8861 (oldViewCache.serverCache.isFiltered() && !pathIsEmpty(overwrite.path));
8862 newViewCache = viewProcessorApplyServerOverwrite(viewProcessor, oldViewCache, overwrite.path, overwrite.snap, writesCache, completeCache, filterServerNode, accumulator);
8863 }
8864 }
8865 else if (operation.type === OperationType.MERGE) {
8866 const merge = operation;
8867 if (merge.source.fromUser) {
8868 newViewCache = viewProcessorApplyUserMerge(viewProcessor, oldViewCache, merge.path, merge.children, writesCache, completeCache, accumulator);
8869 }
8870 else {
8871 assert(merge.source.fromServer, 'Unknown source.');
8872 // We filter the node if it's a tagged update or the node has been previously filtered
8873 filterServerNode =
8874 merge.source.tagged || oldViewCache.serverCache.isFiltered();
8875 newViewCache = viewProcessorApplyServerMerge(viewProcessor, oldViewCache, merge.path, merge.children, writesCache, completeCache, filterServerNode, accumulator);
8876 }
8877 }
8878 else if (operation.type === OperationType.ACK_USER_WRITE) {
8879 const ackUserWrite = operation;
8880 if (!ackUserWrite.revert) {
8881 newViewCache = viewProcessorAckUserWrite(viewProcessor, oldViewCache, ackUserWrite.path, ackUserWrite.affectedTree, writesCache, completeCache, accumulator);
8882 }
8883 else {
8884 newViewCache = viewProcessorRevertUserWrite(viewProcessor, oldViewCache, ackUserWrite.path, writesCache, completeCache, accumulator);
8885 }
8886 }
8887 else if (operation.type === OperationType.LISTEN_COMPLETE) {
8888 newViewCache = viewProcessorListenComplete(viewProcessor, oldViewCache, operation.path, writesCache, accumulator);
8889 }
8890 else {
8891 throw assertionError('Unknown operation type: ' + operation.type);
8892 }
8893 const changes = accumulator.getChanges();
8894 viewProcessorMaybeAddValueEvent(oldViewCache, newViewCache, changes);
8895 return { viewCache: newViewCache, changes };
8896}
8897function viewProcessorMaybeAddValueEvent(oldViewCache, newViewCache, accumulator) {
8898 const eventSnap = newViewCache.eventCache;
8899 if (eventSnap.isFullyInitialized()) {
8900 const isLeafOrEmpty = eventSnap.getNode().isLeafNode() || eventSnap.getNode().isEmpty();
8901 const oldCompleteSnap = viewCacheGetCompleteEventSnap(oldViewCache);
8902 if (accumulator.length > 0 ||
8903 !oldViewCache.eventCache.isFullyInitialized() ||
8904 (isLeafOrEmpty && !eventSnap.getNode().equals(oldCompleteSnap)) ||
8905 !eventSnap.getNode().getPriority().equals(oldCompleteSnap.getPriority())) {
8906 accumulator.push(changeValue(viewCacheGetCompleteEventSnap(newViewCache)));
8907 }
8908 }
8909}
8910function viewProcessorGenerateEventCacheAfterServerEvent(viewProcessor, viewCache, changePath, writesCache, source, accumulator) {
8911 const oldEventSnap = viewCache.eventCache;
8912 if (writeTreeRefShadowingWrite(writesCache, changePath) != null) {
8913 // we have a shadowing write, ignore changes
8914 return viewCache;
8915 }
8916 else {
8917 let newEventCache, serverNode;
8918 if (pathIsEmpty(changePath)) {
8919 // TODO: figure out how this plays with "sliding ack windows"
8920 assert(viewCache.serverCache.isFullyInitialized(), 'If change path is empty, we must have complete server data');
8921 if (viewCache.serverCache.isFiltered()) {
8922 // We need to special case this, because we need to only apply writes to complete children, or
8923 // we might end up raising events for incomplete children. If the server data is filtered deep
8924 // writes cannot be guaranteed to be complete
8925 const serverCache = viewCacheGetCompleteServerSnap(viewCache);
8926 const completeChildren = serverCache instanceof ChildrenNode
8927 ? serverCache
8928 : ChildrenNode.EMPTY_NODE;
8929 const completeEventChildren = writeTreeRefCalcCompleteEventChildren(writesCache, completeChildren);
8930 newEventCache = viewProcessor.filter.updateFullNode(viewCache.eventCache.getNode(), completeEventChildren, accumulator);
8931 }
8932 else {
8933 const completeNode = writeTreeRefCalcCompleteEventCache(writesCache, viewCacheGetCompleteServerSnap(viewCache));
8934 newEventCache = viewProcessor.filter.updateFullNode(viewCache.eventCache.getNode(), completeNode, accumulator);
8935 }
8936 }
8937 else {
8938 const childKey = pathGetFront(changePath);
8939 if (childKey === '.priority') {
8940 assert(pathGetLength(changePath) === 1, "Can't have a priority with additional path components");
8941 const oldEventNode = oldEventSnap.getNode();
8942 serverNode = viewCache.serverCache.getNode();
8943 // we might have overwrites for this priority
8944 const updatedPriority = writeTreeRefCalcEventCacheAfterServerOverwrite(writesCache, changePath, oldEventNode, serverNode);
8945 if (updatedPriority != null) {
8946 newEventCache = viewProcessor.filter.updatePriority(oldEventNode, updatedPriority);
8947 }
8948 else {
8949 // priority didn't change, keep old node
8950 newEventCache = oldEventSnap.getNode();
8951 }
8952 }
8953 else {
8954 const childChangePath = pathPopFront(changePath);
8955 // update child
8956 let newEventChild;
8957 if (oldEventSnap.isCompleteForChild(childKey)) {
8958 serverNode = viewCache.serverCache.getNode();
8959 const eventChildUpdate = writeTreeRefCalcEventCacheAfterServerOverwrite(writesCache, changePath, oldEventSnap.getNode(), serverNode);
8960 if (eventChildUpdate != null) {
8961 newEventChild = oldEventSnap
8962 .getNode()
8963 .getImmediateChild(childKey)
8964 .updateChild(childChangePath, eventChildUpdate);
8965 }
8966 else {
8967 // Nothing changed, just keep the old child
8968 newEventChild = oldEventSnap.getNode().getImmediateChild(childKey);
8969 }
8970 }
8971 else {
8972 newEventChild = writeTreeRefCalcCompleteChild(writesCache, childKey, viewCache.serverCache);
8973 }
8974 if (newEventChild != null) {
8975 newEventCache = viewProcessor.filter.updateChild(oldEventSnap.getNode(), childKey, newEventChild, childChangePath, source, accumulator);
8976 }
8977 else {
8978 // no complete child available or no change
8979 newEventCache = oldEventSnap.getNode();
8980 }
8981 }
8982 }
8983 return viewCacheUpdateEventSnap(viewCache, newEventCache, oldEventSnap.isFullyInitialized() || pathIsEmpty(changePath), viewProcessor.filter.filtersNodes());
8984 }
8985}
8986function viewProcessorApplyServerOverwrite(viewProcessor, oldViewCache, changePath, changedSnap, writesCache, completeCache, filterServerNode, accumulator) {
8987 const oldServerSnap = oldViewCache.serverCache;
8988 let newServerCache;
8989 const serverFilter = filterServerNode
8990 ? viewProcessor.filter
8991 : viewProcessor.filter.getIndexedFilter();
8992 if (pathIsEmpty(changePath)) {
8993 newServerCache = serverFilter.updateFullNode(oldServerSnap.getNode(), changedSnap, null);
8994 }
8995 else if (serverFilter.filtersNodes() && !oldServerSnap.isFiltered()) {
8996 // we want to filter the server node, but we didn't filter the server node yet, so simulate a full update
8997 const newServerNode = oldServerSnap
8998 .getNode()
8999 .updateChild(changePath, changedSnap);
9000 newServerCache = serverFilter.updateFullNode(oldServerSnap.getNode(), newServerNode, null);
9001 }
9002 else {
9003 const childKey = pathGetFront(changePath);
9004 if (!oldServerSnap.isCompleteForPath(changePath) &&
9005 pathGetLength(changePath) > 1) {
9006 // We don't update incomplete nodes with updates intended for other listeners
9007 return oldViewCache;
9008 }
9009 const childChangePath = pathPopFront(changePath);
9010 const childNode = oldServerSnap.getNode().getImmediateChild(childKey);
9011 const newChildNode = childNode.updateChild(childChangePath, changedSnap);
9012 if (childKey === '.priority') {
9013 newServerCache = serverFilter.updatePriority(oldServerSnap.getNode(), newChildNode);
9014 }
9015 else {
9016 newServerCache = serverFilter.updateChild(oldServerSnap.getNode(), childKey, newChildNode, childChangePath, NO_COMPLETE_CHILD_SOURCE, null);
9017 }
9018 }
9019 const newViewCache = viewCacheUpdateServerSnap(oldViewCache, newServerCache, oldServerSnap.isFullyInitialized() || pathIsEmpty(changePath), serverFilter.filtersNodes());
9020 const source = new WriteTreeCompleteChildSource(writesCache, newViewCache, completeCache);
9021 return viewProcessorGenerateEventCacheAfterServerEvent(viewProcessor, newViewCache, changePath, writesCache, source, accumulator);
9022}
9023function viewProcessorApplyUserOverwrite(viewProcessor, oldViewCache, changePath, changedSnap, writesCache, completeCache, accumulator) {
9024 const oldEventSnap = oldViewCache.eventCache;
9025 let newViewCache, newEventCache;
9026 const source = new WriteTreeCompleteChildSource(writesCache, oldViewCache, completeCache);
9027 if (pathIsEmpty(changePath)) {
9028 newEventCache = viewProcessor.filter.updateFullNode(oldViewCache.eventCache.getNode(), changedSnap, accumulator);
9029 newViewCache = viewCacheUpdateEventSnap(oldViewCache, newEventCache, true, viewProcessor.filter.filtersNodes());
9030 }
9031 else {
9032 const childKey = pathGetFront(changePath);
9033 if (childKey === '.priority') {
9034 newEventCache = viewProcessor.filter.updatePriority(oldViewCache.eventCache.getNode(), changedSnap);
9035 newViewCache = viewCacheUpdateEventSnap(oldViewCache, newEventCache, oldEventSnap.isFullyInitialized(), oldEventSnap.isFiltered());
9036 }
9037 else {
9038 const childChangePath = pathPopFront(changePath);
9039 const oldChild = oldEventSnap.getNode().getImmediateChild(childKey);
9040 let newChild;
9041 if (pathIsEmpty(childChangePath)) {
9042 // Child overwrite, we can replace the child
9043 newChild = changedSnap;
9044 }
9045 else {
9046 const childNode = source.getCompleteChild(childKey);
9047 if (childNode != null) {
9048 if (pathGetBack(childChangePath) === '.priority' &&
9049 childNode.getChild(pathParent(childChangePath)).isEmpty()) {
9050 // This is a priority update on an empty node. If this node exists on the server, the
9051 // server will send down the priority in the update, so ignore for now
9052 newChild = childNode;
9053 }
9054 else {
9055 newChild = childNode.updateChild(childChangePath, changedSnap);
9056 }
9057 }
9058 else {
9059 // There is no complete child node available
9060 newChild = ChildrenNode.EMPTY_NODE;
9061 }
9062 }
9063 if (!oldChild.equals(newChild)) {
9064 const newEventSnap = viewProcessor.filter.updateChild(oldEventSnap.getNode(), childKey, newChild, childChangePath, source, accumulator);
9065 newViewCache = viewCacheUpdateEventSnap(oldViewCache, newEventSnap, oldEventSnap.isFullyInitialized(), viewProcessor.filter.filtersNodes());
9066 }
9067 else {
9068 newViewCache = oldViewCache;
9069 }
9070 }
9071 }
9072 return newViewCache;
9073}
9074function viewProcessorCacheHasChild(viewCache, childKey) {
9075 return viewCache.eventCache.isCompleteForChild(childKey);
9076}
9077function viewProcessorApplyUserMerge(viewProcessor, viewCache, path, changedChildren, writesCache, serverCache, accumulator) {
9078 // HACK: In the case of a limit query, there may be some changes that bump things out of the
9079 // window leaving room for new items. It's important we process these changes first, so we
9080 // iterate the changes twice, first processing any that affect items currently in view.
9081 // TODO: I consider an item "in view" if cacheHasChild is true, which checks both the server
9082 // and event snap. I'm not sure if this will result in edge cases when a child is in one but
9083 // not the other.
9084 let curViewCache = viewCache;
9085 changedChildren.foreach((relativePath, childNode) => {
9086 const writePath = pathChild(path, relativePath);
9087 if (viewProcessorCacheHasChild(viewCache, pathGetFront(writePath))) {
9088 curViewCache = viewProcessorApplyUserOverwrite(viewProcessor, curViewCache, writePath, childNode, writesCache, serverCache, accumulator);
9089 }
9090 });
9091 changedChildren.foreach((relativePath, childNode) => {
9092 const writePath = pathChild(path, relativePath);
9093 if (!viewProcessorCacheHasChild(viewCache, pathGetFront(writePath))) {
9094 curViewCache = viewProcessorApplyUserOverwrite(viewProcessor, curViewCache, writePath, childNode, writesCache, serverCache, accumulator);
9095 }
9096 });
9097 return curViewCache;
9098}
9099function viewProcessorApplyMerge(viewProcessor, node, merge) {
9100 merge.foreach((relativePath, childNode) => {
9101 node = node.updateChild(relativePath, childNode);
9102 });
9103 return node;
9104}
9105function viewProcessorApplyServerMerge(viewProcessor, viewCache, path, changedChildren, writesCache, serverCache, filterServerNode, accumulator) {
9106 // If we don't have a cache yet, this merge was intended for a previously listen in the same location. Ignore it and
9107 // wait for the complete data update coming soon.
9108 if (viewCache.serverCache.getNode().isEmpty() &&
9109 !viewCache.serverCache.isFullyInitialized()) {
9110 return viewCache;
9111 }
9112 // HACK: In the case of a limit query, there may be some changes that bump things out of the
9113 // window leaving room for new items. It's important we process these changes first, so we
9114 // iterate the changes twice, first processing any that affect items currently in view.
9115 // TODO: I consider an item "in view" if cacheHasChild is true, which checks both the server
9116 // and event snap. I'm not sure if this will result in edge cases when a child is in one but
9117 // not the other.
9118 let curViewCache = viewCache;
9119 let viewMergeTree;
9120 if (pathIsEmpty(path)) {
9121 viewMergeTree = changedChildren;
9122 }
9123 else {
9124 viewMergeTree = new ImmutableTree(null).setTree(path, changedChildren);
9125 }
9126 const serverNode = viewCache.serverCache.getNode();
9127 viewMergeTree.children.inorderTraversal((childKey, childTree) => {
9128 if (serverNode.hasChild(childKey)) {
9129 const serverChild = viewCache.serverCache
9130 .getNode()
9131 .getImmediateChild(childKey);
9132 const newChild = viewProcessorApplyMerge(viewProcessor, serverChild, childTree);
9133 curViewCache = viewProcessorApplyServerOverwrite(viewProcessor, curViewCache, new Path(childKey), newChild, writesCache, serverCache, filterServerNode, accumulator);
9134 }
9135 });
9136 viewMergeTree.children.inorderTraversal((childKey, childMergeTree) => {
9137 const isUnknownDeepMerge = !viewCache.serverCache.isCompleteForChild(childKey) &&
9138 childMergeTree.value === undefined;
9139 if (!serverNode.hasChild(childKey) && !isUnknownDeepMerge) {
9140 const serverChild = viewCache.serverCache
9141 .getNode()
9142 .getImmediateChild(childKey);
9143 const newChild = viewProcessorApplyMerge(viewProcessor, serverChild, childMergeTree);
9144 curViewCache = viewProcessorApplyServerOverwrite(viewProcessor, curViewCache, new Path(childKey), newChild, writesCache, serverCache, filterServerNode, accumulator);
9145 }
9146 });
9147 return curViewCache;
9148}
9149function viewProcessorAckUserWrite(viewProcessor, viewCache, ackPath, affectedTree, writesCache, completeCache, accumulator) {
9150 if (writeTreeRefShadowingWrite(writesCache, ackPath) != null) {
9151 return viewCache;
9152 }
9153 // Only filter server node if it is currently filtered
9154 const filterServerNode = viewCache.serverCache.isFiltered();
9155 // Essentially we'll just get our existing server cache for the affected paths and re-apply it as a server update
9156 // now that it won't be shadowed.
9157 const serverCache = viewCache.serverCache;
9158 if (affectedTree.value != null) {
9159 // This is an overwrite.
9160 if ((pathIsEmpty(ackPath) && serverCache.isFullyInitialized()) ||
9161 serverCache.isCompleteForPath(ackPath)) {
9162 return viewProcessorApplyServerOverwrite(viewProcessor, viewCache, ackPath, serverCache.getNode().getChild(ackPath), writesCache, completeCache, filterServerNode, accumulator);
9163 }
9164 else if (pathIsEmpty(ackPath)) {
9165 // This is a goofy edge case where we are acking data at this location but don't have full data. We
9166 // should just re-apply whatever we have in our cache as a merge.
9167 let changedChildren = new ImmutableTree(null);
9168 serverCache.getNode().forEachChild(KEY_INDEX, (name, node) => {
9169 changedChildren = changedChildren.set(new Path(name), node);
9170 });
9171 return viewProcessorApplyServerMerge(viewProcessor, viewCache, ackPath, changedChildren, writesCache, completeCache, filterServerNode, accumulator);
9172 }
9173 else {
9174 return viewCache;
9175 }
9176 }
9177 else {
9178 // This is a merge.
9179 let changedChildren = new ImmutableTree(null);
9180 affectedTree.foreach((mergePath, value) => {
9181 const serverCachePath = pathChild(ackPath, mergePath);
9182 if (serverCache.isCompleteForPath(serverCachePath)) {
9183 changedChildren = changedChildren.set(mergePath, serverCache.getNode().getChild(serverCachePath));
9184 }
9185 });
9186 return viewProcessorApplyServerMerge(viewProcessor, viewCache, ackPath, changedChildren, writesCache, completeCache, filterServerNode, accumulator);
9187 }
9188}
9189function viewProcessorListenComplete(viewProcessor, viewCache, path, writesCache, accumulator) {
9190 const oldServerNode = viewCache.serverCache;
9191 const newViewCache = viewCacheUpdateServerSnap(viewCache, oldServerNode.getNode(), oldServerNode.isFullyInitialized() || pathIsEmpty(path), oldServerNode.isFiltered());
9192 return viewProcessorGenerateEventCacheAfterServerEvent(viewProcessor, newViewCache, path, writesCache, NO_COMPLETE_CHILD_SOURCE, accumulator);
9193}
9194function viewProcessorRevertUserWrite(viewProcessor, viewCache, path, writesCache, completeServerCache, accumulator) {
9195 let complete;
9196 if (writeTreeRefShadowingWrite(writesCache, path) != null) {
9197 return viewCache;
9198 }
9199 else {
9200 const source = new WriteTreeCompleteChildSource(writesCache, viewCache, completeServerCache);
9201 const oldEventCache = viewCache.eventCache.getNode();
9202 let newEventCache;
9203 if (pathIsEmpty(path) || pathGetFront(path) === '.priority') {
9204 let newNode;
9205 if (viewCache.serverCache.isFullyInitialized()) {
9206 newNode = writeTreeRefCalcCompleteEventCache(writesCache, viewCacheGetCompleteServerSnap(viewCache));
9207 }
9208 else {
9209 const serverChildren = viewCache.serverCache.getNode();
9210 assert(serverChildren instanceof ChildrenNode, 'serverChildren would be complete if leaf node');
9211 newNode = writeTreeRefCalcCompleteEventChildren(writesCache, serverChildren);
9212 }
9213 newNode = newNode;
9214 newEventCache = viewProcessor.filter.updateFullNode(oldEventCache, newNode, accumulator);
9215 }
9216 else {
9217 const childKey = pathGetFront(path);
9218 let newChild = writeTreeRefCalcCompleteChild(writesCache, childKey, viewCache.serverCache);
9219 if (newChild == null &&
9220 viewCache.serverCache.isCompleteForChild(childKey)) {
9221 newChild = oldEventCache.getImmediateChild(childKey);
9222 }
9223 if (newChild != null) {
9224 newEventCache = viewProcessor.filter.updateChild(oldEventCache, childKey, newChild, pathPopFront(path), source, accumulator);
9225 }
9226 else if (viewCache.eventCache.getNode().hasChild(childKey)) {
9227 // No complete child available, delete the existing one, if any
9228 newEventCache = viewProcessor.filter.updateChild(oldEventCache, childKey, ChildrenNode.EMPTY_NODE, pathPopFront(path), source, accumulator);
9229 }
9230 else {
9231 newEventCache = oldEventCache;
9232 }
9233 if (newEventCache.isEmpty() &&
9234 viewCache.serverCache.isFullyInitialized()) {
9235 // We might have reverted all child writes. Maybe the old event was a leaf node
9236 complete = writeTreeRefCalcCompleteEventCache(writesCache, viewCacheGetCompleteServerSnap(viewCache));
9237 if (complete.isLeafNode()) {
9238 newEventCache = viewProcessor.filter.updateFullNode(newEventCache, complete, accumulator);
9239 }
9240 }
9241 }
9242 complete =
9243 viewCache.serverCache.isFullyInitialized() ||
9244 writeTreeRefShadowingWrite(writesCache, newEmptyPath()) != null;
9245 return viewCacheUpdateEventSnap(viewCache, newEventCache, complete, viewProcessor.filter.filtersNodes());
9246 }
9247}
9248
9249/**
9250 * @license
9251 * Copyright 2017 Google LLC
9252 *
9253 * Licensed under the Apache License, Version 2.0 (the "License");
9254 * you may not use this file except in compliance with the License.
9255 * You may obtain a copy of the License at
9256 *
9257 * http://www.apache.org/licenses/LICENSE-2.0
9258 *
9259 * Unless required by applicable law or agreed to in writing, software
9260 * distributed under the License is distributed on an "AS IS" BASIS,
9261 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9262 * See the License for the specific language governing permissions and
9263 * limitations under the License.
9264 */
9265/**
9266 * A view represents a specific location and query that has 1 or more event registrations.
9267 *
9268 * It does several things:
9269 * - Maintains the list of event registrations for this location/query.
9270 * - Maintains a cache of the data visible for this location/query.
9271 * - Applies new operations (via applyOperation), updates the cache, and based on the event
9272 * registrations returns the set of events to be raised.
9273 */
9274class View {
9275 constructor(query_, initialViewCache) {
9276 this.query_ = query_;
9277 this.eventRegistrations_ = [];
9278 const params = this.query_._queryParams;
9279 const indexFilter = new IndexedFilter(params.getIndex());
9280 const filter = queryParamsGetNodeFilter(params);
9281 this.processor_ = newViewProcessor(filter);
9282 const initialServerCache = initialViewCache.serverCache;
9283 const initialEventCache = initialViewCache.eventCache;
9284 // Don't filter server node with other filter than index, wait for tagged listen
9285 const serverSnap = indexFilter.updateFullNode(ChildrenNode.EMPTY_NODE, initialServerCache.getNode(), null);
9286 const eventSnap = filter.updateFullNode(ChildrenNode.EMPTY_NODE, initialEventCache.getNode(), null);
9287 const newServerCache = new CacheNode(serverSnap, initialServerCache.isFullyInitialized(), indexFilter.filtersNodes());
9288 const newEventCache = new CacheNode(eventSnap, initialEventCache.isFullyInitialized(), filter.filtersNodes());
9289 this.viewCache_ = newViewCache(newEventCache, newServerCache);
9290 this.eventGenerator_ = new EventGenerator(this.query_);
9291 }
9292 get query() {
9293 return this.query_;
9294 }
9295}
9296function viewGetServerCache(view) {
9297 return view.viewCache_.serverCache.getNode();
9298}
9299function viewGetCompleteNode(view) {
9300 return viewCacheGetCompleteEventSnap(view.viewCache_);
9301}
9302function viewGetCompleteServerCache(view, path) {
9303 const cache = viewCacheGetCompleteServerSnap(view.viewCache_);
9304 if (cache) {
9305 // If this isn't a "loadsAllData" view, then cache isn't actually a complete cache and
9306 // we need to see if it contains the child we're interested in.
9307 if (view.query._queryParams.loadsAllData() ||
9308 (!pathIsEmpty(path) &&
9309 !cache.getImmediateChild(pathGetFront(path)).isEmpty())) {
9310 return cache.getChild(path);
9311 }
9312 }
9313 return null;
9314}
9315function viewIsEmpty(view) {
9316 return view.eventRegistrations_.length === 0;
9317}
9318function viewAddEventRegistration(view, eventRegistration) {
9319 view.eventRegistrations_.push(eventRegistration);
9320}
9321/**
9322 * @param eventRegistration - If null, remove all callbacks.
9323 * @param cancelError - If a cancelError is provided, appropriate cancel events will be returned.
9324 * @returns Cancel events, if cancelError was provided.
9325 */
9326function viewRemoveEventRegistration(view, eventRegistration, cancelError) {
9327 const cancelEvents = [];
9328 if (cancelError) {
9329 assert(eventRegistration == null, 'A cancel should cancel all event registrations.');
9330 const path = view.query._path;
9331 view.eventRegistrations_.forEach(registration => {
9332 const maybeEvent = registration.createCancelEvent(cancelError, path);
9333 if (maybeEvent) {
9334 cancelEvents.push(maybeEvent);
9335 }
9336 });
9337 }
9338 if (eventRegistration) {
9339 let remaining = [];
9340 for (let i = 0; i < view.eventRegistrations_.length; ++i) {
9341 const existing = view.eventRegistrations_[i];
9342 if (!existing.matches(eventRegistration)) {
9343 remaining.push(existing);
9344 }
9345 else if (eventRegistration.hasAnyCallback()) {
9346 // We're removing just this one
9347 remaining = remaining.concat(view.eventRegistrations_.slice(i + 1));
9348 break;
9349 }
9350 }
9351 view.eventRegistrations_ = remaining;
9352 }
9353 else {
9354 view.eventRegistrations_ = [];
9355 }
9356 return cancelEvents;
9357}
9358/**
9359 * Applies the given Operation, updates our cache, and returns the appropriate events.
9360 */
9361function viewApplyOperation(view, operation, writesCache, completeServerCache) {
9362 if (operation.type === OperationType.MERGE &&
9363 operation.source.queryId !== null) {
9364 assert(viewCacheGetCompleteServerSnap(view.viewCache_), 'We should always have a full cache before handling merges');
9365 assert(viewCacheGetCompleteEventSnap(view.viewCache_), 'Missing event cache, even though we have a server cache');
9366 }
9367 const oldViewCache = view.viewCache_;
9368 const result = viewProcessorApplyOperation(view.processor_, oldViewCache, operation, writesCache, completeServerCache);
9369 viewProcessorAssertIndexed(view.processor_, result.viewCache);
9370 assert(result.viewCache.serverCache.isFullyInitialized() ||
9371 !oldViewCache.serverCache.isFullyInitialized(), 'Once a server snap is complete, it should never go back');
9372 view.viewCache_ = result.viewCache;
9373 return viewGenerateEventsForChanges_(view, result.changes, result.viewCache.eventCache.getNode(), null);
9374}
9375function viewGetInitialEvents(view, registration) {
9376 const eventSnap = view.viewCache_.eventCache;
9377 const initialChanges = [];
9378 if (!eventSnap.getNode().isLeafNode()) {
9379 const eventNode = eventSnap.getNode();
9380 eventNode.forEachChild(PRIORITY_INDEX, (key, childNode) => {
9381 initialChanges.push(changeChildAdded(key, childNode));
9382 });
9383 }
9384 if (eventSnap.isFullyInitialized()) {
9385 initialChanges.push(changeValue(eventSnap.getNode()));
9386 }
9387 return viewGenerateEventsForChanges_(view, initialChanges, eventSnap.getNode(), registration);
9388}
9389function viewGenerateEventsForChanges_(view, changes, eventCache, eventRegistration) {
9390 const registrations = eventRegistration
9391 ? [eventRegistration]
9392 : view.eventRegistrations_;
9393 return eventGeneratorGenerateEventsForChanges(view.eventGenerator_, changes, eventCache, registrations);
9394}
9395
9396/**
9397 * @license
9398 * Copyright 2017 Google LLC
9399 *
9400 * Licensed under the Apache License, Version 2.0 (the "License");
9401 * you may not use this file except in compliance with the License.
9402 * You may obtain a copy of the License at
9403 *
9404 * http://www.apache.org/licenses/LICENSE-2.0
9405 *
9406 * Unless required by applicable law or agreed to in writing, software
9407 * distributed under the License is distributed on an "AS IS" BASIS,
9408 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9409 * See the License for the specific language governing permissions and
9410 * limitations under the License.
9411 */
9412let referenceConstructor$1;
9413/**
9414 * SyncPoint represents a single location in a SyncTree with 1 or more event registrations, meaning we need to
9415 * maintain 1 or more Views at this location to cache server data and raise appropriate events for server changes
9416 * and user writes (set, transaction, update).
9417 *
9418 * It's responsible for:
9419 * - Maintaining the set of 1 or more views necessary at this location (a SyncPoint with 0 views should be removed).
9420 * - Proxying user / server operations to the views as appropriate (i.e. applyServerOverwrite,
9421 * applyUserOverwrite, etc.)
9422 */
9423class SyncPoint {
9424 constructor() {
9425 /**
9426 * The Views being tracked at this location in the tree, stored as a map where the key is a
9427 * queryId and the value is the View for that query.
9428 *
9429 * NOTE: This list will be quite small (usually 1, but perhaps 2 or 3; any more is an odd use case).
9430 */
9431 this.views = new Map();
9432 }
9433}
9434function syncPointSetReferenceConstructor(val) {
9435 assert(!referenceConstructor$1, '__referenceConstructor has already been defined');
9436 referenceConstructor$1 = val;
9437}
9438function syncPointGetReferenceConstructor() {
9439 assert(referenceConstructor$1, 'Reference.ts has not been loaded');
9440 return referenceConstructor$1;
9441}
9442function syncPointIsEmpty(syncPoint) {
9443 return syncPoint.views.size === 0;
9444}
9445function syncPointApplyOperation(syncPoint, operation, writesCache, optCompleteServerCache) {
9446 const queryId = operation.source.queryId;
9447 if (queryId !== null) {
9448 const view = syncPoint.views.get(queryId);
9449 assert(view != null, 'SyncTree gave us an op for an invalid query.');
9450 return viewApplyOperation(view, operation, writesCache, optCompleteServerCache);
9451 }
9452 else {
9453 let events = [];
9454 for (const view of syncPoint.views.values()) {
9455 events = events.concat(viewApplyOperation(view, operation, writesCache, optCompleteServerCache));
9456 }
9457 return events;
9458 }
9459}
9460/**
9461 * Get a view for the specified query.
9462 *
9463 * @param query - The query to return a view for
9464 * @param writesCache
9465 * @param serverCache
9466 * @param serverCacheComplete
9467 * @returns Events to raise.
9468 */
9469function syncPointGetView(syncPoint, query, writesCache, serverCache, serverCacheComplete) {
9470 const queryId = query._queryIdentifier;
9471 const view = syncPoint.views.get(queryId);
9472 if (!view) {
9473 // TODO: make writesCache take flag for complete server node
9474 let eventCache = writeTreeRefCalcCompleteEventCache(writesCache, serverCacheComplete ? serverCache : null);
9475 let eventCacheComplete = false;
9476 if (eventCache) {
9477 eventCacheComplete = true;
9478 }
9479 else if (serverCache instanceof ChildrenNode) {
9480 eventCache = writeTreeRefCalcCompleteEventChildren(writesCache, serverCache);
9481 eventCacheComplete = false;
9482 }
9483 else {
9484 eventCache = ChildrenNode.EMPTY_NODE;
9485 eventCacheComplete = false;
9486 }
9487 const viewCache = newViewCache(new CacheNode(eventCache, eventCacheComplete, false), new CacheNode(serverCache, serverCacheComplete, false));
9488 return new View(query, viewCache);
9489 }
9490 return view;
9491}
9492/**
9493 * Add an event callback for the specified query.
9494 *
9495 * @param query
9496 * @param eventRegistration
9497 * @param writesCache
9498 * @param serverCache - Complete server cache, if we have it.
9499 * @param serverCacheComplete
9500 * @returns Events to raise.
9501 */
9502function syncPointAddEventRegistration(syncPoint, query, eventRegistration, writesCache, serverCache, serverCacheComplete) {
9503 const view = syncPointGetView(syncPoint, query, writesCache, serverCache, serverCacheComplete);
9504 if (!syncPoint.views.has(query._queryIdentifier)) {
9505 syncPoint.views.set(query._queryIdentifier, view);
9506 }
9507 // This is guaranteed to exist now, we just created anything that was missing
9508 viewAddEventRegistration(view, eventRegistration);
9509 return viewGetInitialEvents(view, eventRegistration);
9510}
9511/**
9512 * Remove event callback(s). Return cancelEvents if a cancelError is specified.
9513 *
9514 * If query is the default query, we'll check all views for the specified eventRegistration.
9515 * If eventRegistration is null, we'll remove all callbacks for the specified view(s).
9516 *
9517 * @param eventRegistration - If null, remove all callbacks.
9518 * @param cancelError - If a cancelError is provided, appropriate cancel events will be returned.
9519 * @returns removed queries and any cancel events
9520 */
9521function syncPointRemoveEventRegistration(syncPoint, query, eventRegistration, cancelError) {
9522 const queryId = query._queryIdentifier;
9523 const removed = [];
9524 let cancelEvents = [];
9525 const hadCompleteView = syncPointHasCompleteView(syncPoint);
9526 if (queryId === 'default') {
9527 // When you do ref.off(...), we search all views for the registration to remove.
9528 for (const [viewQueryId, view] of syncPoint.views.entries()) {
9529 cancelEvents = cancelEvents.concat(viewRemoveEventRegistration(view, eventRegistration, cancelError));
9530 if (viewIsEmpty(view)) {
9531 syncPoint.views.delete(viewQueryId);
9532 // We'll deal with complete views later.
9533 if (!view.query._queryParams.loadsAllData()) {
9534 removed.push(view.query);
9535 }
9536 }
9537 }
9538 }
9539 else {
9540 // remove the callback from the specific view.
9541 const view = syncPoint.views.get(queryId);
9542 if (view) {
9543 cancelEvents = cancelEvents.concat(viewRemoveEventRegistration(view, eventRegistration, cancelError));
9544 if (viewIsEmpty(view)) {
9545 syncPoint.views.delete(queryId);
9546 // We'll deal with complete views later.
9547 if (!view.query._queryParams.loadsAllData()) {
9548 removed.push(view.query);
9549 }
9550 }
9551 }
9552 }
9553 if (hadCompleteView && !syncPointHasCompleteView(syncPoint)) {
9554 // We removed our last complete view.
9555 removed.push(new (syncPointGetReferenceConstructor())(query._repo, query._path));
9556 }
9557 return { removed, events: cancelEvents };
9558}
9559function syncPointGetQueryViews(syncPoint) {
9560 const result = [];
9561 for (const view of syncPoint.views.values()) {
9562 if (!view.query._queryParams.loadsAllData()) {
9563 result.push(view);
9564 }
9565 }
9566 return result;
9567}
9568/**
9569 * @param path - The path to the desired complete snapshot
9570 * @returns A complete cache, if it exists
9571 */
9572function syncPointGetCompleteServerCache(syncPoint, path) {
9573 let serverCache = null;
9574 for (const view of syncPoint.views.values()) {
9575 serverCache = serverCache || viewGetCompleteServerCache(view, path);
9576 }
9577 return serverCache;
9578}
9579function syncPointViewForQuery(syncPoint, query) {
9580 const params = query._queryParams;
9581 if (params.loadsAllData()) {
9582 return syncPointGetCompleteView(syncPoint);
9583 }
9584 else {
9585 const queryId = query._queryIdentifier;
9586 return syncPoint.views.get(queryId);
9587 }
9588}
9589function syncPointViewExistsForQuery(syncPoint, query) {
9590 return syncPointViewForQuery(syncPoint, query) != null;
9591}
9592function syncPointHasCompleteView(syncPoint) {
9593 return syncPointGetCompleteView(syncPoint) != null;
9594}
9595function syncPointGetCompleteView(syncPoint) {
9596 for (const view of syncPoint.views.values()) {
9597 if (view.query._queryParams.loadsAllData()) {
9598 return view;
9599 }
9600 }
9601 return null;
9602}
9603
9604/**
9605 * @license
9606 * Copyright 2017 Google LLC
9607 *
9608 * Licensed under the Apache License, Version 2.0 (the "License");
9609 * you may not use this file except in compliance with the License.
9610 * You may obtain a copy of the License at
9611 *
9612 * http://www.apache.org/licenses/LICENSE-2.0
9613 *
9614 * Unless required by applicable law or agreed to in writing, software
9615 * distributed under the License is distributed on an "AS IS" BASIS,
9616 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9617 * See the License for the specific language governing permissions and
9618 * limitations under the License.
9619 */
9620let referenceConstructor;
9621function syncTreeSetReferenceConstructor(val) {
9622 assert(!referenceConstructor, '__referenceConstructor has already been defined');
9623 referenceConstructor = val;
9624}
9625function syncTreeGetReferenceConstructor() {
9626 assert(referenceConstructor, 'Reference.ts has not been loaded');
9627 return referenceConstructor;
9628}
9629/**
9630 * Static tracker for next query tag.
9631 */
9632let syncTreeNextQueryTag_ = 1;
9633/**
9634 * SyncTree is the central class for managing event callback registration, data caching, views
9635 * (query processing), and event generation. There are typically two SyncTree instances for
9636 * each Repo, one for the normal Firebase data, and one for the .info data.
9637 *
9638 * It has a number of responsibilities, including:
9639 * - Tracking all user event callbacks (registered via addEventRegistration() and removeEventRegistration()).
9640 * - Applying and caching data changes for user set(), transaction(), and update() calls
9641 * (applyUserOverwrite(), applyUserMerge()).
9642 * - Applying and caching data changes for server data changes (applyServerOverwrite(),
9643 * applyServerMerge()).
9644 * - Generating user-facing events for server and user changes (all of the apply* methods
9645 * return the set of events that need to be raised as a result).
9646 * - Maintaining the appropriate set of server listens to ensure we are always subscribed
9647 * to the correct set of paths and queries to satisfy the current set of user event
9648 * callbacks (listens are started/stopped using the provided listenProvider).
9649 *
9650 * NOTE: Although SyncTree tracks event callbacks and calculates events to raise, the actual
9651 * events are returned to the caller rather than raised synchronously.
9652 *
9653 */
9654class SyncTree {
9655 /**
9656 * @param listenProvider_ - Used by SyncTree to start / stop listening
9657 * to server data.
9658 */
9659 constructor(listenProvider_) {
9660 this.listenProvider_ = listenProvider_;
9661 /**
9662 * Tree of SyncPoints. There's a SyncPoint at any location that has 1 or more views.
9663 */
9664 this.syncPointTree_ = new ImmutableTree(null);
9665 /**
9666 * A tree of all pending user writes (user-initiated set()'s, transaction()'s, update()'s, etc.).
9667 */
9668 this.pendingWriteTree_ = newWriteTree();
9669 this.tagToQueryMap = new Map();
9670 this.queryToTagMap = new Map();
9671 }
9672}
9673/**
9674 * Apply the data changes for a user-generated set() or transaction() call.
9675 *
9676 * @returns Events to raise.
9677 */
9678function syncTreeApplyUserOverwrite(syncTree, path, newData, writeId, visible) {
9679 // Record pending write.
9680 writeTreeAddOverwrite(syncTree.pendingWriteTree_, path, newData, writeId, visible);
9681 if (!visible) {
9682 return [];
9683 }
9684 else {
9685 return syncTreeApplyOperationToSyncPoints_(syncTree, new Overwrite(newOperationSourceUser(), path, newData));
9686 }
9687}
9688/**
9689 * Apply the data from a user-generated update() call
9690 *
9691 * @returns Events to raise.
9692 */
9693function syncTreeApplyUserMerge(syncTree, path, changedChildren, writeId) {
9694 // Record pending merge.
9695 writeTreeAddMerge(syncTree.pendingWriteTree_, path, changedChildren, writeId);
9696 const changeTree = ImmutableTree.fromObject(changedChildren);
9697 return syncTreeApplyOperationToSyncPoints_(syncTree, new Merge(newOperationSourceUser(), path, changeTree));
9698}
9699/**
9700 * Acknowledge a pending user write that was previously registered with applyUserOverwrite() or applyUserMerge().
9701 *
9702 * @param revert - True if the given write failed and needs to be reverted
9703 * @returns Events to raise.
9704 */
9705function syncTreeAckUserWrite(syncTree, writeId, revert = false) {
9706 const write = writeTreeGetWrite(syncTree.pendingWriteTree_, writeId);
9707 const needToReevaluate = writeTreeRemoveWrite(syncTree.pendingWriteTree_, writeId);
9708 if (!needToReevaluate) {
9709 return [];
9710 }
9711 else {
9712 let affectedTree = new ImmutableTree(null);
9713 if (write.snap != null) {
9714 // overwrite
9715 affectedTree = affectedTree.set(newEmptyPath(), true);
9716 }
9717 else {
9718 each(write.children, (pathString) => {
9719 affectedTree = affectedTree.set(new Path(pathString), true);
9720 });
9721 }
9722 return syncTreeApplyOperationToSyncPoints_(syncTree, new AckUserWrite(write.path, affectedTree, revert));
9723 }
9724}
9725/**
9726 * Apply new server data for the specified path..
9727 *
9728 * @returns Events to raise.
9729 */
9730function syncTreeApplyServerOverwrite(syncTree, path, newData) {
9731 return syncTreeApplyOperationToSyncPoints_(syncTree, new Overwrite(newOperationSourceServer(), path, newData));
9732}
9733/**
9734 * Apply new server data to be merged in at the specified path.
9735 *
9736 * @returns Events to raise.
9737 */
9738function syncTreeApplyServerMerge(syncTree, path, changedChildren) {
9739 const changeTree = ImmutableTree.fromObject(changedChildren);
9740 return syncTreeApplyOperationToSyncPoints_(syncTree, new Merge(newOperationSourceServer(), path, changeTree));
9741}
9742/**
9743 * Apply a listen complete for a query
9744 *
9745 * @returns Events to raise.
9746 */
9747function syncTreeApplyListenComplete(syncTree, path) {
9748 return syncTreeApplyOperationToSyncPoints_(syncTree, new ListenComplete(newOperationSourceServer(), path));
9749}
9750/**
9751 * Apply a listen complete for a tagged query
9752 *
9753 * @returns Events to raise.
9754 */
9755function syncTreeApplyTaggedListenComplete(syncTree, path, tag) {
9756 const queryKey = syncTreeQueryKeyForTag_(syncTree, tag);
9757 if (queryKey) {
9758 const r = syncTreeParseQueryKey_(queryKey);
9759 const queryPath = r.path, queryId = r.queryId;
9760 const relativePath = newRelativePath(queryPath, path);
9761 const op = new ListenComplete(newOperationSourceServerTaggedQuery(queryId), relativePath);
9762 return syncTreeApplyTaggedOperation_(syncTree, queryPath, op);
9763 }
9764 else {
9765 // We've already removed the query. No big deal, ignore the update
9766 return [];
9767 }
9768}
9769/**
9770 * Remove event callback(s).
9771 *
9772 * If query is the default query, we'll check all queries for the specified eventRegistration.
9773 * If eventRegistration is null, we'll remove all callbacks for the specified query/queries.
9774 *
9775 * @param eventRegistration - If null, all callbacks are removed.
9776 * @param cancelError - If a cancelError is provided, appropriate cancel events will be returned.
9777 * @returns Cancel events, if cancelError was provided.
9778 */
9779function syncTreeRemoveEventRegistration(syncTree, query, eventRegistration, cancelError) {
9780 // Find the syncPoint first. Then deal with whether or not it has matching listeners
9781 const path = query._path;
9782 const maybeSyncPoint = syncTree.syncPointTree_.get(path);
9783 let cancelEvents = [];
9784 // A removal on a default query affects all queries at that location. A removal on an indexed query, even one without
9785 // other query constraints, does *not* affect all queries at that location. So this check must be for 'default', and
9786 // not loadsAllData().
9787 if (maybeSyncPoint &&
9788 (query._queryIdentifier === 'default' ||
9789 syncPointViewExistsForQuery(maybeSyncPoint, query))) {
9790 const removedAndEvents = syncPointRemoveEventRegistration(maybeSyncPoint, query, eventRegistration, cancelError);
9791 if (syncPointIsEmpty(maybeSyncPoint)) {
9792 syncTree.syncPointTree_ = syncTree.syncPointTree_.remove(path);
9793 }
9794 const removed = removedAndEvents.removed;
9795 cancelEvents = removedAndEvents.events;
9796 // We may have just removed one of many listeners and can short-circuit this whole process
9797 // We may also not have removed a default listener, in which case all of the descendant listeners should already be
9798 // properly set up.
9799 //
9800 // Since indexed queries can shadow if they don't have other query constraints, check for loadsAllData(), instead of
9801 // queryId === 'default'
9802 const removingDefault = -1 !==
9803 removed.findIndex(query => {
9804 return query._queryParams.loadsAllData();
9805 });
9806 const covered = syncTree.syncPointTree_.findOnPath(path, (relativePath, parentSyncPoint) => syncPointHasCompleteView(parentSyncPoint));
9807 if (removingDefault && !covered) {
9808 const subtree = syncTree.syncPointTree_.subtree(path);
9809 // There are potentially child listeners. Determine what if any listens we need to send before executing the
9810 // removal
9811 if (!subtree.isEmpty()) {
9812 // We need to fold over our subtree and collect the listeners to send
9813 const newViews = syncTreeCollectDistinctViewsForSubTree_(subtree);
9814 // Ok, we've collected all the listens we need. Set them up.
9815 for (let i = 0; i < newViews.length; ++i) {
9816 const view = newViews[i], newQuery = view.query;
9817 const listener = syncTreeCreateListenerForView_(syncTree, view);
9818 syncTree.listenProvider_.startListening(syncTreeQueryForListening_(newQuery), syncTreeTagForQuery_(syncTree, newQuery), listener.hashFn, listener.onComplete);
9819 }
9820 }
9821 }
9822 // If we removed anything and we're not covered by a higher up listen, we need to stop listening on this query
9823 // The above block has us covered in terms of making sure we're set up on listens lower in the tree.
9824 // Also, note that if we have a cancelError, it's already been removed at the provider level.
9825 if (!covered && removed.length > 0 && !cancelError) {
9826 // If we removed a default, then we weren't listening on any of the other queries here. Just cancel the one
9827 // default. Otherwise, we need to iterate through and cancel each individual query
9828 if (removingDefault) {
9829 // We don't tag default listeners
9830 const defaultTag = null;
9831 syncTree.listenProvider_.stopListening(syncTreeQueryForListening_(query), defaultTag);
9832 }
9833 else {
9834 removed.forEach((queryToRemove) => {
9835 const tagToRemove = syncTree.queryToTagMap.get(syncTreeMakeQueryKey_(queryToRemove));
9836 syncTree.listenProvider_.stopListening(syncTreeQueryForListening_(queryToRemove), tagToRemove);
9837 });
9838 }
9839 }
9840 // Now, clear all of the tags we're tracking for the removed listens
9841 syncTreeRemoveTags_(syncTree, removed);
9842 }
9843 return cancelEvents;
9844}
9845/**
9846 * This function was added to support non-listener queries,
9847 * specifically for use in repoGetValue. It sets up all the same
9848 * local cache data-structures (SyncPoint + View) that are
9849 * needed for listeners without installing an event registration.
9850 * If `query` is not `loadsAllData`, it will also provision a tag for
9851 * the query so that query results can be merged into the sync
9852 * tree using existing logic for tagged listener queries.
9853 *
9854 * @param syncTree - Synctree to add the query to.
9855 * @param query - Query to register
9856 * @returns tag as a string if query is not a default query, null if query is not.
9857 */
9858function syncTreeRegisterQuery(syncTree, query) {
9859 const { syncPoint, serverCache, writesCache, serverCacheComplete } = syncTreeRegisterSyncPoint(query, syncTree);
9860 const view = syncPointGetView(syncPoint, query, writesCache, serverCache, serverCacheComplete);
9861 if (!syncPoint.views.has(query._queryIdentifier)) {
9862 syncPoint.views.set(query._queryIdentifier, view);
9863 }
9864 if (!query._queryParams.loadsAllData()) {
9865 return syncTreeTagForQuery_(syncTree, query);
9866 }
9867 return null;
9868}
9869/**
9870 * Apply new server data for the specified tagged query.
9871 *
9872 * @returns Events to raise.
9873 */
9874function syncTreeApplyTaggedQueryOverwrite(syncTree, path, snap, tag) {
9875 const queryKey = syncTreeQueryKeyForTag_(syncTree, tag);
9876 if (queryKey != null) {
9877 const r = syncTreeParseQueryKey_(queryKey);
9878 const queryPath = r.path, queryId = r.queryId;
9879 const relativePath = newRelativePath(queryPath, path);
9880 const op = new Overwrite(newOperationSourceServerTaggedQuery(queryId), relativePath, snap);
9881 return syncTreeApplyTaggedOperation_(syncTree, queryPath, op);
9882 }
9883 else {
9884 // Query must have been removed already
9885 return [];
9886 }
9887}
9888/**
9889 * Apply server data to be merged in for the specified tagged query.
9890 *
9891 * @returns Events to raise.
9892 */
9893function syncTreeApplyTaggedQueryMerge(syncTree, path, changedChildren, tag) {
9894 const queryKey = syncTreeQueryKeyForTag_(syncTree, tag);
9895 if (queryKey) {
9896 const r = syncTreeParseQueryKey_(queryKey);
9897 const queryPath = r.path, queryId = r.queryId;
9898 const relativePath = newRelativePath(queryPath, path);
9899 const changeTree = ImmutableTree.fromObject(changedChildren);
9900 const op = new Merge(newOperationSourceServerTaggedQuery(queryId), relativePath, changeTree);
9901 return syncTreeApplyTaggedOperation_(syncTree, queryPath, op);
9902 }
9903 else {
9904 // We've already removed the query. No big deal, ignore the update
9905 return [];
9906 }
9907}
9908/**
9909 * Creates a new syncpoint for a query and creates a tag if the view doesn't exist.
9910 * Extracted from addEventRegistration to allow `repoGetValue` to properly set up the SyncTree
9911 * without actually listening on a query.
9912 */
9913function syncTreeRegisterSyncPoint(query, syncTree) {
9914 const path = query._path;
9915 let serverCache = null;
9916 let foundAncestorDefaultView = false;
9917 // Any covering writes will necessarily be at the root, so really all we need to find is the server cache.
9918 // Consider optimizing this once there's a better understanding of what actual behavior will be.
9919 syncTree.syncPointTree_.foreachOnPath(path, (pathToSyncPoint, sp) => {
9920 const relativePath = newRelativePath(pathToSyncPoint, path);
9921 serverCache =
9922 serverCache || syncPointGetCompleteServerCache(sp, relativePath);
9923 foundAncestorDefaultView =
9924 foundAncestorDefaultView || syncPointHasCompleteView(sp);
9925 });
9926 let syncPoint = syncTree.syncPointTree_.get(path);
9927 if (!syncPoint) {
9928 syncPoint = new SyncPoint();
9929 syncTree.syncPointTree_ = syncTree.syncPointTree_.set(path, syncPoint);
9930 }
9931 else {
9932 foundAncestorDefaultView =
9933 foundAncestorDefaultView || syncPointHasCompleteView(syncPoint);
9934 serverCache =
9935 serverCache || syncPointGetCompleteServerCache(syncPoint, newEmptyPath());
9936 }
9937 let serverCacheComplete;
9938 if (serverCache != null) {
9939 serverCacheComplete = true;
9940 }
9941 else {
9942 serverCacheComplete = false;
9943 serverCache = ChildrenNode.EMPTY_NODE;
9944 const subtree = syncTree.syncPointTree_.subtree(path);
9945 subtree.foreachChild((childName, childSyncPoint) => {
9946 const completeCache = syncPointGetCompleteServerCache(childSyncPoint, newEmptyPath());
9947 if (completeCache) {
9948 serverCache = serverCache.updateImmediateChild(childName, completeCache);
9949 }
9950 });
9951 }
9952 const viewAlreadyExists = syncPointViewExistsForQuery(syncPoint, query);
9953 if (!viewAlreadyExists && !query._queryParams.loadsAllData()) {
9954 // We need to track a tag for this query
9955 const queryKey = syncTreeMakeQueryKey_(query);
9956 assert(!syncTree.queryToTagMap.has(queryKey), 'View does not exist, but we have a tag');
9957 const tag = syncTreeGetNextQueryTag_();
9958 syncTree.queryToTagMap.set(queryKey, tag);
9959 syncTree.tagToQueryMap.set(tag, queryKey);
9960 }
9961 const writesCache = writeTreeChildWrites(syncTree.pendingWriteTree_, path);
9962 return {
9963 syncPoint,
9964 writesCache,
9965 serverCache,
9966 serverCacheComplete,
9967 foundAncestorDefaultView,
9968 viewAlreadyExists
9969 };
9970}
9971/**
9972 * Add an event callback for the specified query.
9973 *
9974 * @returns Events to raise.
9975 */
9976function syncTreeAddEventRegistration(syncTree, query, eventRegistration) {
9977 const { syncPoint, serverCache, writesCache, serverCacheComplete, viewAlreadyExists, foundAncestorDefaultView } = syncTreeRegisterSyncPoint(query, syncTree);
9978 let events = syncPointAddEventRegistration(syncPoint, query, eventRegistration, writesCache, serverCache, serverCacheComplete);
9979 if (!viewAlreadyExists && !foundAncestorDefaultView) {
9980 const view = syncPointViewForQuery(syncPoint, query);
9981 events = events.concat(syncTreeSetupListener_(syncTree, query, view));
9982 }
9983 return events;
9984}
9985/**
9986 * Returns a complete cache, if we have one, of the data at a particular path. If the location does not have a
9987 * listener above it, we will get a false "null". This shouldn't be a problem because transactions will always
9988 * have a listener above, and atomic operations would correctly show a jitter of <increment value> ->
9989 * <incremented total> as the write is applied locally and then acknowledged at the server.
9990 *
9991 * Note: this method will *include* hidden writes from transaction with applyLocally set to false.
9992 *
9993 * @param path - The path to the data we want
9994 * @param writeIdsToExclude - A specific set to be excluded
9995 */
9996function syncTreeCalcCompleteEventCache(syncTree, path, writeIdsToExclude) {
9997 const includeHiddenSets = true;
9998 const writeTree = syncTree.pendingWriteTree_;
9999 const serverCache = syncTree.syncPointTree_.findOnPath(path, (pathSoFar, syncPoint) => {
10000 const relativePath = newRelativePath(pathSoFar, path);
10001 const serverCache = syncPointGetCompleteServerCache(syncPoint, relativePath);
10002 if (serverCache) {
10003 return serverCache;
10004 }
10005 });
10006 return writeTreeCalcCompleteEventCache(writeTree, path, serverCache, writeIdsToExclude, includeHiddenSets);
10007}
10008function syncTreeGetServerValue(syncTree, query) {
10009 const path = query._path;
10010 let serverCache = null;
10011 // Any covering writes will necessarily be at the root, so really all we need to find is the server cache.
10012 // Consider optimizing this once there's a better understanding of what actual behavior will be.
10013 syncTree.syncPointTree_.foreachOnPath(path, (pathToSyncPoint, sp) => {
10014 const relativePath = newRelativePath(pathToSyncPoint, path);
10015 serverCache =
10016 serverCache || syncPointGetCompleteServerCache(sp, relativePath);
10017 });
10018 let syncPoint = syncTree.syncPointTree_.get(path);
10019 if (!syncPoint) {
10020 syncPoint = new SyncPoint();
10021 syncTree.syncPointTree_ = syncTree.syncPointTree_.set(path, syncPoint);
10022 }
10023 else {
10024 serverCache =
10025 serverCache || syncPointGetCompleteServerCache(syncPoint, newEmptyPath());
10026 }
10027 const serverCacheComplete = serverCache != null;
10028 const serverCacheNode = serverCacheComplete
10029 ? new CacheNode(serverCache, true, false)
10030 : null;
10031 const writesCache = writeTreeChildWrites(syncTree.pendingWriteTree_, query._path);
10032 const view = syncPointGetView(syncPoint, query, writesCache, serverCacheComplete ? serverCacheNode.getNode() : ChildrenNode.EMPTY_NODE, serverCacheComplete);
10033 return viewGetCompleteNode(view);
10034}
10035/**
10036 * A helper method that visits all descendant and ancestor SyncPoints, applying the operation.
10037 *
10038 * NOTES:
10039 * - Descendant SyncPoints will be visited first (since we raise events depth-first).
10040 *
10041 * - We call applyOperation() on each SyncPoint passing three things:
10042 * 1. A version of the Operation that has been made relative to the SyncPoint location.
10043 * 2. A WriteTreeRef of any writes we have cached at the SyncPoint location.
10044 * 3. A snapshot Node with cached server data, if we have it.
10045 *
10046 * - We concatenate all of the events returned by each SyncPoint and return the result.
10047 */
10048function syncTreeApplyOperationToSyncPoints_(syncTree, operation) {
10049 return syncTreeApplyOperationHelper_(operation, syncTree.syncPointTree_,
10050 /*serverCache=*/ null, writeTreeChildWrites(syncTree.pendingWriteTree_, newEmptyPath()));
10051}
10052/**
10053 * Recursive helper for applyOperationToSyncPoints_
10054 */
10055function syncTreeApplyOperationHelper_(operation, syncPointTree, serverCache, writesCache) {
10056 if (pathIsEmpty(operation.path)) {
10057 return syncTreeApplyOperationDescendantsHelper_(operation, syncPointTree, serverCache, writesCache);
10058 }
10059 else {
10060 const syncPoint = syncPointTree.get(newEmptyPath());
10061 // If we don't have cached server data, see if we can get it from this SyncPoint.
10062 if (serverCache == null && syncPoint != null) {
10063 serverCache = syncPointGetCompleteServerCache(syncPoint, newEmptyPath());
10064 }
10065 let events = [];
10066 const childName = pathGetFront(operation.path);
10067 const childOperation = operation.operationForChild(childName);
10068 const childTree = syncPointTree.children.get(childName);
10069 if (childTree && childOperation) {
10070 const childServerCache = serverCache
10071 ? serverCache.getImmediateChild(childName)
10072 : null;
10073 const childWritesCache = writeTreeRefChild(writesCache, childName);
10074 events = events.concat(syncTreeApplyOperationHelper_(childOperation, childTree, childServerCache, childWritesCache));
10075 }
10076 if (syncPoint) {
10077 events = events.concat(syncPointApplyOperation(syncPoint, operation, writesCache, serverCache));
10078 }
10079 return events;
10080 }
10081}
10082/**
10083 * Recursive helper for applyOperationToSyncPoints_
10084 */
10085function syncTreeApplyOperationDescendantsHelper_(operation, syncPointTree, serverCache, writesCache) {
10086 const syncPoint = syncPointTree.get(newEmptyPath());
10087 // If we don't have cached server data, see if we can get it from this SyncPoint.
10088 if (serverCache == null && syncPoint != null) {
10089 serverCache = syncPointGetCompleteServerCache(syncPoint, newEmptyPath());
10090 }
10091 let events = [];
10092 syncPointTree.children.inorderTraversal((childName, childTree) => {
10093 const childServerCache = serverCache
10094 ? serverCache.getImmediateChild(childName)
10095 : null;
10096 const childWritesCache = writeTreeRefChild(writesCache, childName);
10097 const childOperation = operation.operationForChild(childName);
10098 if (childOperation) {
10099 events = events.concat(syncTreeApplyOperationDescendantsHelper_(childOperation, childTree, childServerCache, childWritesCache));
10100 }
10101 });
10102 if (syncPoint) {
10103 events = events.concat(syncPointApplyOperation(syncPoint, operation, writesCache, serverCache));
10104 }
10105 return events;
10106}
10107function syncTreeCreateListenerForView_(syncTree, view) {
10108 const query = view.query;
10109 const tag = syncTreeTagForQuery_(syncTree, query);
10110 return {
10111 hashFn: () => {
10112 const cache = viewGetServerCache(view) || ChildrenNode.EMPTY_NODE;
10113 return cache.hash();
10114 },
10115 onComplete: (status) => {
10116 if (status === 'ok') {
10117 if (tag) {
10118 return syncTreeApplyTaggedListenComplete(syncTree, query._path, tag);
10119 }
10120 else {
10121 return syncTreeApplyListenComplete(syncTree, query._path);
10122 }
10123 }
10124 else {
10125 // If a listen failed, kill all of the listeners here, not just the one that triggered the error.
10126 // Note that this may need to be scoped to just this listener if we change permissions on filtered children
10127 const error = errorForServerCode(status, query);
10128 return syncTreeRemoveEventRegistration(syncTree, query,
10129 /*eventRegistration*/ null, error);
10130 }
10131 }
10132 };
10133}
10134/**
10135 * Return the tag associated with the given query.
10136 */
10137function syncTreeTagForQuery_(syncTree, query) {
10138 const queryKey = syncTreeMakeQueryKey_(query);
10139 return syncTree.queryToTagMap.get(queryKey);
10140}
10141/**
10142 * Given a query, computes a "queryKey" suitable for use in our queryToTagMap_.
10143 */
10144function syncTreeMakeQueryKey_(query) {
10145 return query._path.toString() + '$' + query._queryIdentifier;
10146}
10147/**
10148 * Return the query associated with the given tag, if we have one
10149 */
10150function syncTreeQueryKeyForTag_(syncTree, tag) {
10151 return syncTree.tagToQueryMap.get(tag);
10152}
10153/**
10154 * Given a queryKey (created by makeQueryKey), parse it back into a path and queryId.
10155 */
10156function syncTreeParseQueryKey_(queryKey) {
10157 const splitIndex = queryKey.indexOf('$');
10158 assert(splitIndex !== -1 && splitIndex < queryKey.length - 1, 'Bad queryKey.');
10159 return {
10160 queryId: queryKey.substr(splitIndex + 1),
10161 path: new Path(queryKey.substr(0, splitIndex))
10162 };
10163}
10164/**
10165 * A helper method to apply tagged operations
10166 */
10167function syncTreeApplyTaggedOperation_(syncTree, queryPath, operation) {
10168 const syncPoint = syncTree.syncPointTree_.get(queryPath);
10169 assert(syncPoint, "Missing sync point for query tag that we're tracking");
10170 const writesCache = writeTreeChildWrites(syncTree.pendingWriteTree_, queryPath);
10171 return syncPointApplyOperation(syncPoint, operation, writesCache, null);
10172}
10173/**
10174 * This collapses multiple unfiltered views into a single view, since we only need a single
10175 * listener for them.
10176 */
10177function syncTreeCollectDistinctViewsForSubTree_(subtree) {
10178 return subtree.fold((relativePath, maybeChildSyncPoint, childMap) => {
10179 if (maybeChildSyncPoint && syncPointHasCompleteView(maybeChildSyncPoint)) {
10180 const completeView = syncPointGetCompleteView(maybeChildSyncPoint);
10181 return [completeView];
10182 }
10183 else {
10184 // No complete view here, flatten any deeper listens into an array
10185 let views = [];
10186 if (maybeChildSyncPoint) {
10187 views = syncPointGetQueryViews(maybeChildSyncPoint);
10188 }
10189 each(childMap, (_key, childViews) => {
10190 views = views.concat(childViews);
10191 });
10192 return views;
10193 }
10194 });
10195}
10196/**
10197 * Normalizes a query to a query we send the server for listening
10198 *
10199 * @returns The normalized query
10200 */
10201function syncTreeQueryForListening_(query) {
10202 if (query._queryParams.loadsAllData() && !query._queryParams.isDefault()) {
10203 // We treat queries that load all data as default queries
10204 // Cast is necessary because ref() technically returns Firebase which is actually fb.api.Firebase which inherits
10205 // from Query
10206 return new (syncTreeGetReferenceConstructor())(query._repo, query._path);
10207 }
10208 else {
10209 return query;
10210 }
10211}
10212function syncTreeRemoveTags_(syncTree, queries) {
10213 for (let j = 0; j < queries.length; ++j) {
10214 const removedQuery = queries[j];
10215 if (!removedQuery._queryParams.loadsAllData()) {
10216 // We should have a tag for this
10217 const removedQueryKey = syncTreeMakeQueryKey_(removedQuery);
10218 const removedQueryTag = syncTree.queryToTagMap.get(removedQueryKey);
10219 syncTree.queryToTagMap.delete(removedQueryKey);
10220 syncTree.tagToQueryMap.delete(removedQueryTag);
10221 }
10222 }
10223}
10224/**
10225 * Static accessor for query tags.
10226 */
10227function syncTreeGetNextQueryTag_() {
10228 return syncTreeNextQueryTag_++;
10229}
10230/**
10231 * For a given new listen, manage the de-duplication of outstanding subscriptions.
10232 *
10233 * @returns This method can return events to support synchronous data sources
10234 */
10235function syncTreeSetupListener_(syncTree, query, view) {
10236 const path = query._path;
10237 const tag = syncTreeTagForQuery_(syncTree, query);
10238 const listener = syncTreeCreateListenerForView_(syncTree, view);
10239 const events = syncTree.listenProvider_.startListening(syncTreeQueryForListening_(query), tag, listener.hashFn, listener.onComplete);
10240 const subtree = syncTree.syncPointTree_.subtree(path);
10241 // The root of this subtree has our query. We're here because we definitely need to send a listen for that, but we
10242 // may need to shadow other listens as well.
10243 if (tag) {
10244 assert(!syncPointHasCompleteView(subtree.value), "If we're adding a query, it shouldn't be shadowed");
10245 }
10246 else {
10247 // Shadow everything at or below this location, this is a default listener.
10248 const queriesToStop = subtree.fold((relativePath, maybeChildSyncPoint, childMap) => {
10249 if (!pathIsEmpty(relativePath) &&
10250 maybeChildSyncPoint &&
10251 syncPointHasCompleteView(maybeChildSyncPoint)) {
10252 return [syncPointGetCompleteView(maybeChildSyncPoint).query];
10253 }
10254 else {
10255 // No default listener here, flatten any deeper queries into an array
10256 let queries = [];
10257 if (maybeChildSyncPoint) {
10258 queries = queries.concat(syncPointGetQueryViews(maybeChildSyncPoint).map(view => view.query));
10259 }
10260 each(childMap, (_key, childQueries) => {
10261 queries = queries.concat(childQueries);
10262 });
10263 return queries;
10264 }
10265 });
10266 for (let i = 0; i < queriesToStop.length; ++i) {
10267 const queryToStop = queriesToStop[i];
10268 syncTree.listenProvider_.stopListening(syncTreeQueryForListening_(queryToStop), syncTreeTagForQuery_(syncTree, queryToStop));
10269 }
10270 }
10271 return events;
10272}
10273
10274/**
10275 * @license
10276 * Copyright 2017 Google LLC
10277 *
10278 * Licensed under the Apache License, Version 2.0 (the "License");
10279 * you may not use this file except in compliance with the License.
10280 * You may obtain a copy of the License at
10281 *
10282 * http://www.apache.org/licenses/LICENSE-2.0
10283 *
10284 * Unless required by applicable law or agreed to in writing, software
10285 * distributed under the License is distributed on an "AS IS" BASIS,
10286 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10287 * See the License for the specific language governing permissions and
10288 * limitations under the License.
10289 */
10290class ExistingValueProvider {
10291 constructor(node_) {
10292 this.node_ = node_;
10293 }
10294 getImmediateChild(childName) {
10295 const child = this.node_.getImmediateChild(childName);
10296 return new ExistingValueProvider(child);
10297 }
10298 node() {
10299 return this.node_;
10300 }
10301}
10302class DeferredValueProvider {
10303 constructor(syncTree, path) {
10304 this.syncTree_ = syncTree;
10305 this.path_ = path;
10306 }
10307 getImmediateChild(childName) {
10308 const childPath = pathChild(this.path_, childName);
10309 return new DeferredValueProvider(this.syncTree_, childPath);
10310 }
10311 node() {
10312 return syncTreeCalcCompleteEventCache(this.syncTree_, this.path_);
10313 }
10314}
10315/**
10316 * Generate placeholders for deferred values.
10317 */
10318const generateWithValues = function (values) {
10319 values = values || {};
10320 values['timestamp'] = values['timestamp'] || new Date().getTime();
10321 return values;
10322};
10323/**
10324 * Value to use when firing local events. When writing server values, fire
10325 * local events with an approximate value, otherwise return value as-is.
10326 */
10327const resolveDeferredLeafValue = function (value, existingVal, serverValues) {
10328 if (!value || typeof value !== 'object') {
10329 return value;
10330 }
10331 assert('.sv' in value, 'Unexpected leaf node or priority contents');
10332 if (typeof value['.sv'] === 'string') {
10333 return resolveScalarDeferredValue(value['.sv'], existingVal, serverValues);
10334 }
10335 else if (typeof value['.sv'] === 'object') {
10336 return resolveComplexDeferredValue(value['.sv'], existingVal);
10337 }
10338 else {
10339 assert(false, 'Unexpected server value: ' + JSON.stringify(value, null, 2));
10340 }
10341};
10342const resolveScalarDeferredValue = function (op, existing, serverValues) {
10343 switch (op) {
10344 case 'timestamp':
10345 return serverValues['timestamp'];
10346 default:
10347 assert(false, 'Unexpected server value: ' + op);
10348 }
10349};
10350const resolveComplexDeferredValue = function (op, existing, unused) {
10351 if (!op.hasOwnProperty('increment')) {
10352 assert(false, 'Unexpected server value: ' + JSON.stringify(op, null, 2));
10353 }
10354 const delta = op['increment'];
10355 if (typeof delta !== 'number') {
10356 assert(false, 'Unexpected increment value: ' + delta);
10357 }
10358 const existingNode = existing.node();
10359 assert(existingNode !== null && typeof existingNode !== 'undefined', 'Expected ChildrenNode.EMPTY_NODE for nulls');
10360 // Incrementing a non-number sets the value to the incremented amount
10361 if (!existingNode.isLeafNode()) {
10362 return delta;
10363 }
10364 const leaf = existingNode;
10365 const existingVal = leaf.getValue();
10366 if (typeof existingVal !== 'number') {
10367 return delta;
10368 }
10369 // No need to do over/underflow arithmetic here because JS only handles floats under the covers
10370 return existingVal + delta;
10371};
10372/**
10373 * Recursively replace all deferred values and priorities in the tree with the
10374 * specified generated replacement values.
10375 * @param path - path to which write is relative
10376 * @param node - new data written at path
10377 * @param syncTree - current data
10378 */
10379const resolveDeferredValueTree = function (path, node, syncTree, serverValues) {
10380 return resolveDeferredValue(node, new DeferredValueProvider(syncTree, path), serverValues);
10381};
10382/**
10383 * Recursively replace all deferred values and priorities in the node with the
10384 * specified generated replacement values. If there are no server values in the node,
10385 * it'll be returned as-is.
10386 */
10387const resolveDeferredValueSnapshot = function (node, existing, serverValues) {
10388 return resolveDeferredValue(node, new ExistingValueProvider(existing), serverValues);
10389};
10390function resolveDeferredValue(node, existingVal, serverValues) {
10391 const rawPri = node.getPriority().val();
10392 const priority = resolveDeferredLeafValue(rawPri, existingVal.getImmediateChild('.priority'), serverValues);
10393 let newNode;
10394 if (node.isLeafNode()) {
10395 const leafNode = node;
10396 const value = resolveDeferredLeafValue(leafNode.getValue(), existingVal, serverValues);
10397 if (value !== leafNode.getValue() ||
10398 priority !== leafNode.getPriority().val()) {
10399 return new LeafNode(value, nodeFromJSON(priority));
10400 }
10401 else {
10402 return node;
10403 }
10404 }
10405 else {
10406 const childrenNode = node;
10407 newNode = childrenNode;
10408 if (priority !== childrenNode.getPriority().val()) {
10409 newNode = newNode.updatePriority(new LeafNode(priority));
10410 }
10411 childrenNode.forEachChild(PRIORITY_INDEX, (childName, childNode) => {
10412 const newChildNode = resolveDeferredValue(childNode, existingVal.getImmediateChild(childName), serverValues);
10413 if (newChildNode !== childNode) {
10414 newNode = newNode.updateImmediateChild(childName, newChildNode);
10415 }
10416 });
10417 return newNode;
10418 }
10419}
10420
10421/**
10422 * @license
10423 * Copyright 2017 Google LLC
10424 *
10425 * Licensed under the Apache License, Version 2.0 (the "License");
10426 * you may not use this file except in compliance with the License.
10427 * You may obtain a copy of the License at
10428 *
10429 * http://www.apache.org/licenses/LICENSE-2.0
10430 *
10431 * Unless required by applicable law or agreed to in writing, software
10432 * distributed under the License is distributed on an "AS IS" BASIS,
10433 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10434 * See the License for the specific language governing permissions and
10435 * limitations under the License.
10436 */
10437/**
10438 * A light-weight tree, traversable by path. Nodes can have both values and children.
10439 * Nodes are not enumerated (by forEachChild) unless they have a value or non-empty
10440 * children.
10441 */
10442class Tree {
10443 /**
10444 * @param name - Optional name of the node.
10445 * @param parent - Optional parent node.
10446 * @param node - Optional node to wrap.
10447 */
10448 constructor(name = '', parent = null, node = { children: {}, childCount: 0 }) {
10449 this.name = name;
10450 this.parent = parent;
10451 this.node = node;
10452 }
10453}
10454/**
10455 * Returns a sub-Tree for the given path.
10456 *
10457 * @param pathObj - Path to look up.
10458 * @returns Tree for path.
10459 */
10460function treeSubTree(tree, pathObj) {
10461 // TODO: Require pathObj to be Path?
10462 let path = pathObj instanceof Path ? pathObj : new Path(pathObj);
10463 let child = tree, next = pathGetFront(path);
10464 while (next !== null) {
10465 const childNode = safeGet(child.node.children, next) || {
10466 children: {},
10467 childCount: 0
10468 };
10469 child = new Tree(next, child, childNode);
10470 path = pathPopFront(path);
10471 next = pathGetFront(path);
10472 }
10473 return child;
10474}
10475/**
10476 * Returns the data associated with this tree node.
10477 *
10478 * @returns The data or null if no data exists.
10479 */
10480function treeGetValue(tree) {
10481 return tree.node.value;
10482}
10483/**
10484 * Sets data to this tree node.
10485 *
10486 * @param value - Value to set.
10487 */
10488function treeSetValue(tree, value) {
10489 tree.node.value = value;
10490 treeUpdateParents(tree);
10491}
10492/**
10493 * @returns Whether the tree has any children.
10494 */
10495function treeHasChildren(tree) {
10496 return tree.node.childCount > 0;
10497}
10498/**
10499 * @returns Whethe rthe tree is empty (no value or children).
10500 */
10501function treeIsEmpty(tree) {
10502 return treeGetValue(tree) === undefined && !treeHasChildren(tree);
10503}
10504/**
10505 * Calls action for each child of this tree node.
10506 *
10507 * @param action - Action to be called for each child.
10508 */
10509function treeForEachChild(tree, action) {
10510 each(tree.node.children, (child, childTree) => {
10511 action(new Tree(child, tree, childTree));
10512 });
10513}
10514/**
10515 * Does a depth-first traversal of this node's descendants, calling action for each one.
10516 *
10517 * @param action - Action to be called for each child.
10518 * @param includeSelf - Whether to call action on this node as well. Defaults to
10519 * false.
10520 * @param childrenFirst - Whether to call action on children before calling it on
10521 * parent.
10522 */
10523function treeForEachDescendant(tree, action, includeSelf, childrenFirst) {
10524 if (includeSelf && !childrenFirst) {
10525 action(tree);
10526 }
10527 treeForEachChild(tree, child => {
10528 treeForEachDescendant(child, action, true, childrenFirst);
10529 });
10530 if (includeSelf && childrenFirst) {
10531 action(tree);
10532 }
10533}
10534/**
10535 * Calls action on each ancestor node.
10536 *
10537 * @param action - Action to be called on each parent; return
10538 * true to abort.
10539 * @param includeSelf - Whether to call action on this node as well.
10540 * @returns true if the action callback returned true.
10541 */
10542function treeForEachAncestor(tree, action, includeSelf) {
10543 let node = includeSelf ? tree : tree.parent;
10544 while (node !== null) {
10545 if (action(node)) {
10546 return true;
10547 }
10548 node = node.parent;
10549 }
10550 return false;
10551}
10552/**
10553 * @returns The path of this tree node, as a Path.
10554 */
10555function treeGetPath(tree) {
10556 return new Path(tree.parent === null
10557 ? tree.name
10558 : treeGetPath(tree.parent) + '/' + tree.name);
10559}
10560/**
10561 * Adds or removes this child from its parent based on whether it's empty or not.
10562 */
10563function treeUpdateParents(tree) {
10564 if (tree.parent !== null) {
10565 treeUpdateChild(tree.parent, tree.name, tree);
10566 }
10567}
10568/**
10569 * Adds or removes the passed child to this tree node, depending on whether it's empty.
10570 *
10571 * @param childName - The name of the child to update.
10572 * @param child - The child to update.
10573 */
10574function treeUpdateChild(tree, childName, child) {
10575 const childEmpty = treeIsEmpty(child);
10576 const childExists = contains(tree.node.children, childName);
10577 if (childEmpty && childExists) {
10578 delete tree.node.children[childName];
10579 tree.node.childCount--;
10580 treeUpdateParents(tree);
10581 }
10582 else if (!childEmpty && !childExists) {
10583 tree.node.children[childName] = child.node;
10584 tree.node.childCount++;
10585 treeUpdateParents(tree);
10586 }
10587}
10588
10589/**
10590 * @license
10591 * Copyright 2017 Google LLC
10592 *
10593 * Licensed under the Apache License, Version 2.0 (the "License");
10594 * you may not use this file except in compliance with the License.
10595 * You may obtain a copy of the License at
10596 *
10597 * http://www.apache.org/licenses/LICENSE-2.0
10598 *
10599 * Unless required by applicable law or agreed to in writing, software
10600 * distributed under the License is distributed on an "AS IS" BASIS,
10601 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10602 * See the License for the specific language governing permissions and
10603 * limitations under the License.
10604 */
10605/**
10606 * True for invalid Firebase keys
10607 */
10608const INVALID_KEY_REGEX_ = /[\[\].#$\/\u0000-\u001F\u007F]/;
10609/**
10610 * True for invalid Firebase paths.
10611 * Allows '/' in paths.
10612 */
10613const INVALID_PATH_REGEX_ = /[\[\].#$\u0000-\u001F\u007F]/;
10614/**
10615 * Maximum number of characters to allow in leaf value
10616 */
10617const MAX_LEAF_SIZE_ = 10 * 1024 * 1024;
10618const isValidKey = function (key) {
10619 return (typeof key === 'string' && key.length !== 0 && !INVALID_KEY_REGEX_.test(key));
10620};
10621const isValidPathString = function (pathString) {
10622 return (typeof pathString === 'string' &&
10623 pathString.length !== 0 &&
10624 !INVALID_PATH_REGEX_.test(pathString));
10625};
10626const isValidRootPathString = function (pathString) {
10627 if (pathString) {
10628 // Allow '/.info/' at the beginning.
10629 pathString = pathString.replace(/^\/*\.info(\/|$)/, '/');
10630 }
10631 return isValidPathString(pathString);
10632};
10633const isValidPriority = function (priority) {
10634 return (priority === null ||
10635 typeof priority === 'string' ||
10636 (typeof priority === 'number' && !isInvalidJSONNumber(priority)) ||
10637 (priority &&
10638 typeof priority === 'object' &&
10639 // eslint-disable-next-line @typescript-eslint/no-explicit-any
10640 contains(priority, '.sv')));
10641};
10642/**
10643 * Pre-validate a datum passed as an argument to Firebase function.
10644 */
10645const validateFirebaseDataArg = function (fnName, value, path, optional) {
10646 if (optional && value === undefined) {
10647 return;
10648 }
10649 validateFirebaseData(errorPrefix(fnName, 'value'), value, path);
10650};
10651/**
10652 * Validate a data object client-side before sending to server.
10653 */
10654const validateFirebaseData = function (errorPrefix, data, path_) {
10655 const path = path_ instanceof Path ? new ValidationPath(path_, errorPrefix) : path_;
10656 if (data === undefined) {
10657 throw new Error(errorPrefix + 'contains undefined ' + validationPathToErrorString(path));
10658 }
10659 if (typeof data === 'function') {
10660 throw new Error(errorPrefix +
10661 'contains a function ' +
10662 validationPathToErrorString(path) +
10663 ' with contents = ' +
10664 data.toString());
10665 }
10666 if (isInvalidJSONNumber(data)) {
10667 throw new Error(errorPrefix +
10668 'contains ' +
10669 data.toString() +
10670 ' ' +
10671 validationPathToErrorString(path));
10672 }
10673 // Check max leaf size, but try to avoid the utf8 conversion if we can.
10674 if (typeof data === 'string' &&
10675 data.length > MAX_LEAF_SIZE_ / 3 &&
10676 stringLength(data) > MAX_LEAF_SIZE_) {
10677 throw new Error(errorPrefix +
10678 'contains a string greater than ' +
10679 MAX_LEAF_SIZE_ +
10680 ' utf8 bytes ' +
10681 validationPathToErrorString(path) +
10682 " ('" +
10683 data.substring(0, 50) +
10684 "...')");
10685 }
10686 // TODO = Perf = Consider combining the recursive validation of keys into NodeFromJSON
10687 // to save extra walking of large objects.
10688 if (data && typeof data === 'object') {
10689 let hasDotValue = false;
10690 let hasActualChild = false;
10691 each(data, (key, value) => {
10692 if (key === '.value') {
10693 hasDotValue = true;
10694 }
10695 else if (key !== '.priority' && key !== '.sv') {
10696 hasActualChild = true;
10697 if (!isValidKey(key)) {
10698 throw new Error(errorPrefix +
10699 ' contains an invalid key (' +
10700 key +
10701 ') ' +
10702 validationPathToErrorString(path) +
10703 '. Keys must be non-empty strings ' +
10704 'and can\'t contain ".", "#", "$", "/", "[", or "]"');
10705 }
10706 }
10707 validationPathPush(path, key);
10708 validateFirebaseData(errorPrefix, value, path);
10709 validationPathPop(path);
10710 });
10711 if (hasDotValue && hasActualChild) {
10712 throw new Error(errorPrefix +
10713 ' contains ".value" child ' +
10714 validationPathToErrorString(path) +
10715 ' in addition to actual children.');
10716 }
10717 }
10718};
10719/**
10720 * Pre-validate paths passed in the firebase function.
10721 */
10722const validateFirebaseMergePaths = function (errorPrefix, mergePaths) {
10723 let i, curPath;
10724 for (i = 0; i < mergePaths.length; i++) {
10725 curPath = mergePaths[i];
10726 const keys = pathSlice(curPath);
10727 for (let j = 0; j < keys.length; j++) {
10728 if (keys[j] === '.priority' && j === keys.length - 1) ;
10729 else if (!isValidKey(keys[j])) {
10730 throw new Error(errorPrefix +
10731 'contains an invalid key (' +
10732 keys[j] +
10733 ') in path ' +
10734 curPath.toString() +
10735 '. Keys must be non-empty strings ' +
10736 'and can\'t contain ".", "#", "$", "/", "[", or "]"');
10737 }
10738 }
10739 }
10740 // Check that update keys are not descendants of each other.
10741 // We rely on the property that sorting guarantees that ancestors come
10742 // right before descendants.
10743 mergePaths.sort(pathCompare);
10744 let prevPath = null;
10745 for (i = 0; i < mergePaths.length; i++) {
10746 curPath = mergePaths[i];
10747 if (prevPath !== null && pathContains(prevPath, curPath)) {
10748 throw new Error(errorPrefix +
10749 'contains a path ' +
10750 prevPath.toString() +
10751 ' that is ancestor of another path ' +
10752 curPath.toString());
10753 }
10754 prevPath = curPath;
10755 }
10756};
10757/**
10758 * pre-validate an object passed as an argument to firebase function (
10759 * must be an object - e.g. for firebase.update()).
10760 */
10761const validateFirebaseMergeDataArg = function (fnName, data, path, optional) {
10762 if (optional && data === undefined) {
10763 return;
10764 }
10765 const errorPrefix$1 = errorPrefix(fnName, 'values');
10766 if (!(data && typeof data === 'object') || Array.isArray(data)) {
10767 throw new Error(errorPrefix$1 + ' must be an object containing the children to replace.');
10768 }
10769 const mergePaths = [];
10770 each(data, (key, value) => {
10771 const curPath = new Path(key);
10772 validateFirebaseData(errorPrefix$1, value, pathChild(path, curPath));
10773 if (pathGetBack(curPath) === '.priority') {
10774 if (!isValidPriority(value)) {
10775 throw new Error(errorPrefix$1 +
10776 "contains an invalid value for '" +
10777 curPath.toString() +
10778 "', which must be a valid " +
10779 'Firebase priority (a string, finite number, server value, or null).');
10780 }
10781 }
10782 mergePaths.push(curPath);
10783 });
10784 validateFirebaseMergePaths(errorPrefix$1, mergePaths);
10785};
10786const validatePriority = function (fnName, priority, optional) {
10787 if (optional && priority === undefined) {
10788 return;
10789 }
10790 if (isInvalidJSONNumber(priority)) {
10791 throw new Error(errorPrefix(fnName, 'priority') +
10792 'is ' +
10793 priority.toString() +
10794 ', but must be a valid Firebase priority (a string, finite number, ' +
10795 'server value, or null).');
10796 }
10797 // Special case to allow importing data with a .sv.
10798 if (!isValidPriority(priority)) {
10799 throw new Error(errorPrefix(fnName, 'priority') +
10800 'must be a valid Firebase priority ' +
10801 '(a string, finite number, server value, or null).');
10802 }
10803};
10804const validateKey = function (fnName, argumentName, key, optional) {
10805 if (optional && key === undefined) {
10806 return;
10807 }
10808 if (!isValidKey(key)) {
10809 throw new Error(errorPrefix(fnName, argumentName) +
10810 'was an invalid key = "' +
10811 key +
10812 '". Firebase keys must be non-empty strings and ' +
10813 'can\'t contain ".", "#", "$", "/", "[", or "]").');
10814 }
10815};
10816/**
10817 * @internal
10818 */
10819const validatePathString = function (fnName, argumentName, pathString, optional) {
10820 if (optional && pathString === undefined) {
10821 return;
10822 }
10823 if (!isValidPathString(pathString)) {
10824 throw new Error(errorPrefix(fnName, argumentName) +
10825 'was an invalid path = "' +
10826 pathString +
10827 '". Paths must be non-empty strings and ' +
10828 'can\'t contain ".", "#", "$", "[", or "]"');
10829 }
10830};
10831const validateRootPathString = function (fnName, argumentName, pathString, optional) {
10832 if (pathString) {
10833 // Allow '/.info/' at the beginning.
10834 pathString = pathString.replace(/^\/*\.info(\/|$)/, '/');
10835 }
10836 validatePathString(fnName, argumentName, pathString, optional);
10837};
10838/**
10839 * @internal
10840 */
10841const validateWritablePath = function (fnName, path) {
10842 if (pathGetFront(path) === '.info') {
10843 throw new Error(fnName + " failed = Can't modify data under /.info/");
10844 }
10845};
10846const validateUrl = function (fnName, parsedUrl) {
10847 // TODO = Validate server better.
10848 const pathString = parsedUrl.path.toString();
10849 if (!(typeof parsedUrl.repoInfo.host === 'string') ||
10850 parsedUrl.repoInfo.host.length === 0 ||
10851 (!isValidKey(parsedUrl.repoInfo.namespace) &&
10852 parsedUrl.repoInfo.host.split(':')[0] !== 'localhost') ||
10853 (pathString.length !== 0 && !isValidRootPathString(pathString))) {
10854 throw new Error(errorPrefix(fnName, 'url') +
10855 'must be a valid firebase URL and ' +
10856 'the path can\'t contain ".", "#", "$", "[", or "]".');
10857 }
10858};
10859
10860/**
10861 * @license
10862 * Copyright 2017 Google LLC
10863 *
10864 * Licensed under the Apache License, Version 2.0 (the "License");
10865 * you may not use this file except in compliance with the License.
10866 * You may obtain a copy of the License at
10867 *
10868 * http://www.apache.org/licenses/LICENSE-2.0
10869 *
10870 * Unless required by applicable law or agreed to in writing, software
10871 * distributed under the License is distributed on an "AS IS" BASIS,
10872 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10873 * See the License for the specific language governing permissions and
10874 * limitations under the License.
10875 */
10876/**
10877 * The event queue serves a few purposes:
10878 * 1. It ensures we maintain event order in the face of event callbacks doing operations that result in more
10879 * events being queued.
10880 * 2. raiseQueuedEvents() handles being called reentrantly nicely. That is, if in the course of raising events,
10881 * raiseQueuedEvents() is called again, the "inner" call will pick up raising events where the "outer" call
10882 * left off, ensuring that the events are still raised synchronously and in order.
10883 * 3. You can use raiseEventsAtPath and raiseEventsForChangedPath to ensure only relevant previously-queued
10884 * events are raised synchronously.
10885 *
10886 * NOTE: This can all go away if/when we move to async events.
10887 *
10888 */
10889class EventQueue {
10890 constructor() {
10891 this.eventLists_ = [];
10892 /**
10893 * Tracks recursion depth of raiseQueuedEvents_, for debugging purposes.
10894 */
10895 this.recursionDepth_ = 0;
10896 }
10897}
10898/**
10899 * @param eventDataList - The new events to queue.
10900 */
10901function eventQueueQueueEvents(eventQueue, eventDataList) {
10902 // We group events by path, storing them in a single EventList, to make it easier to skip over them quickly.
10903 let currList = null;
10904 for (let i = 0; i < eventDataList.length; i++) {
10905 const data = eventDataList[i];
10906 const path = data.getPath();
10907 if (currList !== null && !pathEquals(path, currList.path)) {
10908 eventQueue.eventLists_.push(currList);
10909 currList = null;
10910 }
10911 if (currList === null) {
10912 currList = { events: [], path };
10913 }
10914 currList.events.push(data);
10915 }
10916 if (currList) {
10917 eventQueue.eventLists_.push(currList);
10918 }
10919}
10920/**
10921 * Queues the specified events and synchronously raises all events (including previously queued ones)
10922 * for the specified path.
10923 *
10924 * It is assumed that the new events are all for the specified path.
10925 *
10926 * @param path - The path to raise events for.
10927 * @param eventDataList - The new events to raise.
10928 */
10929function eventQueueRaiseEventsAtPath(eventQueue, path, eventDataList) {
10930 eventQueueQueueEvents(eventQueue, eventDataList);
10931 eventQueueRaiseQueuedEventsMatchingPredicate(eventQueue, eventPath => pathEquals(eventPath, path));
10932}
10933/**
10934 * Queues the specified events and synchronously raises all events (including previously queued ones) for
10935 * locations related to the specified change path (i.e. all ancestors and descendants).
10936 *
10937 * It is assumed that the new events are all related (ancestor or descendant) to the specified path.
10938 *
10939 * @param changedPath - The path to raise events for.
10940 * @param eventDataList - The events to raise
10941 */
10942function eventQueueRaiseEventsForChangedPath(eventQueue, changedPath, eventDataList) {
10943 eventQueueQueueEvents(eventQueue, eventDataList);
10944 eventQueueRaiseQueuedEventsMatchingPredicate(eventQueue, eventPath => pathContains(eventPath, changedPath) ||
10945 pathContains(changedPath, eventPath));
10946}
10947function eventQueueRaiseQueuedEventsMatchingPredicate(eventQueue, predicate) {
10948 eventQueue.recursionDepth_++;
10949 let sentAll = true;
10950 for (let i = 0; i < eventQueue.eventLists_.length; i++) {
10951 const eventList = eventQueue.eventLists_[i];
10952 if (eventList) {
10953 const eventPath = eventList.path;
10954 if (predicate(eventPath)) {
10955 eventListRaise(eventQueue.eventLists_[i]);
10956 eventQueue.eventLists_[i] = null;
10957 }
10958 else {
10959 sentAll = false;
10960 }
10961 }
10962 }
10963 if (sentAll) {
10964 eventQueue.eventLists_ = [];
10965 }
10966 eventQueue.recursionDepth_--;
10967}
10968/**
10969 * Iterates through the list and raises each event
10970 */
10971function eventListRaise(eventList) {
10972 for (let i = 0; i < eventList.events.length; i++) {
10973 const eventData = eventList.events[i];
10974 if (eventData !== null) {
10975 eventList.events[i] = null;
10976 const eventFn = eventData.getEventRunner();
10977 if (logger) {
10978 log('event: ' + eventData.toString());
10979 }
10980 exceptionGuard(eventFn);
10981 }
10982 }
10983}
10984
10985/**
10986 * @license
10987 * Copyright 2017 Google LLC
10988 *
10989 * Licensed under the Apache License, Version 2.0 (the "License");
10990 * you may not use this file except in compliance with the License.
10991 * You may obtain a copy of the License at
10992 *
10993 * http://www.apache.org/licenses/LICENSE-2.0
10994 *
10995 * Unless required by applicable law or agreed to in writing, software
10996 * distributed under the License is distributed on an "AS IS" BASIS,
10997 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10998 * See the License for the specific language governing permissions and
10999 * limitations under the License.
11000 */
11001const INTERRUPT_REASON = 'repo_interrupt';
11002/**
11003 * If a transaction does not succeed after 25 retries, we abort it. Among other
11004 * things this ensure that if there's ever a bug causing a mismatch between
11005 * client / server hashes for some data, we won't retry indefinitely.
11006 */
11007const MAX_TRANSACTION_RETRIES = 25;
11008/**
11009 * A connection to a single data repository.
11010 */
11011class Repo {
11012 constructor(repoInfo_, forceRestClient_, authTokenProvider_, appCheckProvider_) {
11013 this.repoInfo_ = repoInfo_;
11014 this.forceRestClient_ = forceRestClient_;
11015 this.authTokenProvider_ = authTokenProvider_;
11016 this.appCheckProvider_ = appCheckProvider_;
11017 this.dataUpdateCount = 0;
11018 this.statsListener_ = null;
11019 this.eventQueue_ = new EventQueue();
11020 this.nextWriteId_ = 1;
11021 this.interceptServerDataCallback_ = null;
11022 /** A list of data pieces and paths to be set when this client disconnects. */
11023 this.onDisconnect_ = newSparseSnapshotTree();
11024 /** Stores queues of outstanding transactions for Firebase locations. */
11025 this.transactionQueueTree_ = new Tree();
11026 // TODO: This should be @private but it's used by test_access.js and internal.js
11027 this.persistentConnection_ = null;
11028 // This key is intentionally not updated if RepoInfo is later changed or replaced
11029 this.key = this.repoInfo_.toURLString();
11030 }
11031 /**
11032 * @returns The URL corresponding to the root of this Firebase.
11033 */
11034 toString() {
11035 return ((this.repoInfo_.secure ? 'https://' : 'http://') + this.repoInfo_.host);
11036 }
11037}
11038function repoStart(repo, appId, authOverride) {
11039 repo.stats_ = statsManagerGetCollection(repo.repoInfo_);
11040 if (repo.forceRestClient_ || beingCrawled()) {
11041 repo.server_ = new ReadonlyRestClient(repo.repoInfo_, (pathString, data, isMerge, tag) => {
11042 repoOnDataUpdate(repo, pathString, data, isMerge, tag);
11043 }, repo.authTokenProvider_, repo.appCheckProvider_);
11044 // Minor hack: Fire onConnect immediately, since there's no actual connection.
11045 setTimeout(() => repoOnConnectStatus(repo, /* connectStatus= */ true), 0);
11046 }
11047 else {
11048 // Validate authOverride
11049 if (typeof authOverride !== 'undefined' && authOverride !== null) {
11050 if (typeof authOverride !== 'object') {
11051 throw new Error('Only objects are supported for option databaseAuthVariableOverride');
11052 }
11053 try {
11054 stringify(authOverride);
11055 }
11056 catch (e) {
11057 throw new Error('Invalid authOverride provided: ' + e);
11058 }
11059 }
11060 repo.persistentConnection_ = new PersistentConnection(repo.repoInfo_, appId, (pathString, data, isMerge, tag) => {
11061 repoOnDataUpdate(repo, pathString, data, isMerge, tag);
11062 }, (connectStatus) => {
11063 repoOnConnectStatus(repo, connectStatus);
11064 }, (updates) => {
11065 repoOnServerInfoUpdate(repo, updates);
11066 }, repo.authTokenProvider_, repo.appCheckProvider_, authOverride);
11067 repo.server_ = repo.persistentConnection_;
11068 }
11069 repo.authTokenProvider_.addTokenChangeListener(token => {
11070 repo.server_.refreshAuthToken(token);
11071 });
11072 repo.appCheckProvider_.addTokenChangeListener(result => {
11073 repo.server_.refreshAppCheckToken(result.token);
11074 });
11075 // In the case of multiple Repos for the same repoInfo (i.e. there are multiple Firebase.Contexts being used),
11076 // we only want to create one StatsReporter. As such, we'll report stats over the first Repo created.
11077 repo.statsReporter_ = statsManagerGetOrCreateReporter(repo.repoInfo_, () => new StatsReporter(repo.stats_, repo.server_));
11078 // Used for .info.
11079 repo.infoData_ = new SnapshotHolder();
11080 repo.infoSyncTree_ = new SyncTree({
11081 startListening: (query, tag, currentHashFn, onComplete) => {
11082 let infoEvents = [];
11083 const node = repo.infoData_.getNode(query._path);
11084 // This is possibly a hack, but we have different semantics for .info endpoints. We don't raise null events
11085 // on initial data...
11086 if (!node.isEmpty()) {
11087 infoEvents = syncTreeApplyServerOverwrite(repo.infoSyncTree_, query._path, node);
11088 setTimeout(() => {
11089 onComplete('ok');
11090 }, 0);
11091 }
11092 return infoEvents;
11093 },
11094 stopListening: () => { }
11095 });
11096 repoUpdateInfo(repo, 'connected', false);
11097 repo.serverSyncTree_ = new SyncTree({
11098 startListening: (query, tag, currentHashFn, onComplete) => {
11099 repo.server_.listen(query, currentHashFn, tag, (status, data) => {
11100 const events = onComplete(status, data);
11101 eventQueueRaiseEventsForChangedPath(repo.eventQueue_, query._path, events);
11102 });
11103 // No synchronous events for network-backed sync trees
11104 return [];
11105 },
11106 stopListening: (query, tag) => {
11107 repo.server_.unlisten(query, tag);
11108 }
11109 });
11110}
11111/**
11112 * @returns The time in milliseconds, taking the server offset into account if we have one.
11113 */
11114function repoServerTime(repo) {
11115 const offsetNode = repo.infoData_.getNode(new Path('.info/serverTimeOffset'));
11116 const offset = offsetNode.val() || 0;
11117 return new Date().getTime() + offset;
11118}
11119/**
11120 * Generate ServerValues using some variables from the repo object.
11121 */
11122function repoGenerateServerValues(repo) {
11123 return generateWithValues({
11124 timestamp: repoServerTime(repo)
11125 });
11126}
11127/**
11128 * Called by realtime when we get new messages from the server.
11129 */
11130function repoOnDataUpdate(repo, pathString, data, isMerge, tag) {
11131 // For testing.
11132 repo.dataUpdateCount++;
11133 const path = new Path(pathString);
11134 data = repo.interceptServerDataCallback_
11135 ? repo.interceptServerDataCallback_(pathString, data)
11136 : data;
11137 let events = [];
11138 if (tag) {
11139 if (isMerge) {
11140 const taggedChildren = map(data, (raw) => nodeFromJSON(raw));
11141 events = syncTreeApplyTaggedQueryMerge(repo.serverSyncTree_, path, taggedChildren, tag);
11142 }
11143 else {
11144 const taggedSnap = nodeFromJSON(data);
11145 events = syncTreeApplyTaggedQueryOverwrite(repo.serverSyncTree_, path, taggedSnap, tag);
11146 }
11147 }
11148 else if (isMerge) {
11149 const changedChildren = map(data, (raw) => nodeFromJSON(raw));
11150 events = syncTreeApplyServerMerge(repo.serverSyncTree_, path, changedChildren);
11151 }
11152 else {
11153 const snap = nodeFromJSON(data);
11154 events = syncTreeApplyServerOverwrite(repo.serverSyncTree_, path, snap);
11155 }
11156 let affectedPath = path;
11157 if (events.length > 0) {
11158 // Since we have a listener outstanding for each transaction, receiving any events
11159 // is a proxy for some change having occurred.
11160 affectedPath = repoRerunTransactions(repo, path);
11161 }
11162 eventQueueRaiseEventsForChangedPath(repo.eventQueue_, affectedPath, events);
11163}
11164function repoOnConnectStatus(repo, connectStatus) {
11165 repoUpdateInfo(repo, 'connected', connectStatus);
11166 if (connectStatus === false) {
11167 repoRunOnDisconnectEvents(repo);
11168 }
11169}
11170function repoOnServerInfoUpdate(repo, updates) {
11171 each(updates, (key, value) => {
11172 repoUpdateInfo(repo, key, value);
11173 });
11174}
11175function repoUpdateInfo(repo, pathString, value) {
11176 const path = new Path('/.info/' + pathString);
11177 const newNode = nodeFromJSON(value);
11178 repo.infoData_.updateSnapshot(path, newNode);
11179 const events = syncTreeApplyServerOverwrite(repo.infoSyncTree_, path, newNode);
11180 eventQueueRaiseEventsForChangedPath(repo.eventQueue_, path, events);
11181}
11182function repoGetNextWriteId(repo) {
11183 return repo.nextWriteId_++;
11184}
11185/**
11186 * The purpose of `getValue` is to return the latest known value
11187 * satisfying `query`.
11188 *
11189 * This method will first check for in-memory cached values
11190 * belonging to active listeners. If they are found, such values
11191 * are considered to be the most up-to-date.
11192 *
11193 * If the client is not connected, this method will try to
11194 * establish a connection and request the value for `query`. If
11195 * the client is not able to retrieve the query result, it reports
11196 * an error.
11197 *
11198 * @param query - The query to surface a value for.
11199 */
11200function repoGetValue(repo, query) {
11201 // Only active queries are cached. There is no persisted cache.
11202 const cached = syncTreeGetServerValue(repo.serverSyncTree_, query);
11203 if (cached != null) {
11204 return Promise.resolve(cached);
11205 }
11206 return repo.server_.get(query).then(payload => {
11207 const node = nodeFromJSON(payload).withIndex(query._queryParams.getIndex());
11208 // if this is a filtered query, then overwrite at path
11209 if (query._queryParams.loadsAllData()) {
11210 syncTreeApplyServerOverwrite(repo.serverSyncTree_, query._path, node);
11211 }
11212 else {
11213 // Simulate `syncTreeAddEventRegistration` without events/listener setup.
11214 // We do this (along with the syncTreeRemoveEventRegistration` below) so that
11215 // `repoGetValue` results have the same cache effects as initial listener(s)
11216 // updates.
11217 const tag = syncTreeRegisterQuery(repo.serverSyncTree_, query);
11218 syncTreeApplyTaggedQueryOverwrite(repo.serverSyncTree_, query._path, node, tag);
11219 // Call `syncTreeRemoveEventRegistration` with a null event registration, since there is none.
11220 // Note: The below code essentially unregisters the query and cleans up any views/syncpoints temporarily created above.
11221 }
11222 const cancels = syncTreeRemoveEventRegistration(repo.serverSyncTree_, query, null);
11223 if (cancels.length > 0) {
11224 repoLog(repo, 'unexpected cancel events in repoGetValue');
11225 }
11226 return node;
11227 }, err => {
11228 repoLog(repo, 'get for query ' + stringify(query) + ' failed: ' + err);
11229 return Promise.reject(new Error(err));
11230 });
11231}
11232function repoSetWithPriority(repo, path, newVal, newPriority, onComplete) {
11233 repoLog(repo, 'set', {
11234 path: path.toString(),
11235 value: newVal,
11236 priority: newPriority
11237 });
11238 // TODO: Optimize this behavior to either (a) store flag to skip resolving where possible and / or
11239 // (b) store unresolved paths on JSON parse
11240 const serverValues = repoGenerateServerValues(repo);
11241 const newNodeUnresolved = nodeFromJSON(newVal, newPriority);
11242 const existing = syncTreeCalcCompleteEventCache(repo.serverSyncTree_, path);
11243 const newNode = resolveDeferredValueSnapshot(newNodeUnresolved, existing, serverValues);
11244 const writeId = repoGetNextWriteId(repo);
11245 const events = syncTreeApplyUserOverwrite(repo.serverSyncTree_, path, newNode, writeId, true);
11246 eventQueueQueueEvents(repo.eventQueue_, events);
11247 repo.server_.put(path.toString(), newNodeUnresolved.val(/*export=*/ true), (status, errorReason) => {
11248 const success = status === 'ok';
11249 if (!success) {
11250 warn('set at ' + path + ' failed: ' + status);
11251 }
11252 const clearEvents = syncTreeAckUserWrite(repo.serverSyncTree_, writeId, !success);
11253 eventQueueRaiseEventsForChangedPath(repo.eventQueue_, path, clearEvents);
11254 repoCallOnCompleteCallback(repo, onComplete, status, errorReason);
11255 });
11256 const affectedPath = repoAbortTransactions(repo, path);
11257 repoRerunTransactions(repo, affectedPath);
11258 // We queued the events above, so just flush the queue here
11259 eventQueueRaiseEventsForChangedPath(repo.eventQueue_, affectedPath, []);
11260}
11261function repoUpdate(repo, path, childrenToMerge, onComplete) {
11262 repoLog(repo, 'update', { path: path.toString(), value: childrenToMerge });
11263 // Start with our existing data and merge each child into it.
11264 let empty = true;
11265 const serverValues = repoGenerateServerValues(repo);
11266 const changedChildren = {};
11267 each(childrenToMerge, (changedKey, changedValue) => {
11268 empty = false;
11269 changedChildren[changedKey] = resolveDeferredValueTree(pathChild(path, changedKey), nodeFromJSON(changedValue), repo.serverSyncTree_, serverValues);
11270 });
11271 if (!empty) {
11272 const writeId = repoGetNextWriteId(repo);
11273 const events = syncTreeApplyUserMerge(repo.serverSyncTree_, path, changedChildren, writeId);
11274 eventQueueQueueEvents(repo.eventQueue_, events);
11275 repo.server_.merge(path.toString(), childrenToMerge, (status, errorReason) => {
11276 const success = status === 'ok';
11277 if (!success) {
11278 warn('update at ' + path + ' failed: ' + status);
11279 }
11280 const clearEvents = syncTreeAckUserWrite(repo.serverSyncTree_, writeId, !success);
11281 const affectedPath = clearEvents.length > 0 ? repoRerunTransactions(repo, path) : path;
11282 eventQueueRaiseEventsForChangedPath(repo.eventQueue_, affectedPath, clearEvents);
11283 repoCallOnCompleteCallback(repo, onComplete, status, errorReason);
11284 });
11285 each(childrenToMerge, (changedPath) => {
11286 const affectedPath = repoAbortTransactions(repo, pathChild(path, changedPath));
11287 repoRerunTransactions(repo, affectedPath);
11288 });
11289 // We queued the events above, so just flush the queue here
11290 eventQueueRaiseEventsForChangedPath(repo.eventQueue_, path, []);
11291 }
11292 else {
11293 log("update() called with empty data. Don't do anything.");
11294 repoCallOnCompleteCallback(repo, onComplete, 'ok', undefined);
11295 }
11296}
11297/**
11298 * Applies all of the changes stored up in the onDisconnect_ tree.
11299 */
11300function repoRunOnDisconnectEvents(repo) {
11301 repoLog(repo, 'onDisconnectEvents');
11302 const serverValues = repoGenerateServerValues(repo);
11303 const resolvedOnDisconnectTree = newSparseSnapshotTree();
11304 sparseSnapshotTreeForEachTree(repo.onDisconnect_, newEmptyPath(), (path, node) => {
11305 const resolved = resolveDeferredValueTree(path, node, repo.serverSyncTree_, serverValues);
11306 sparseSnapshotTreeRemember(resolvedOnDisconnectTree, path, resolved);
11307 });
11308 let events = [];
11309 sparseSnapshotTreeForEachTree(resolvedOnDisconnectTree, newEmptyPath(), (path, snap) => {
11310 events = events.concat(syncTreeApplyServerOverwrite(repo.serverSyncTree_, path, snap));
11311 const affectedPath = repoAbortTransactions(repo, path);
11312 repoRerunTransactions(repo, affectedPath);
11313 });
11314 repo.onDisconnect_ = newSparseSnapshotTree();
11315 eventQueueRaiseEventsForChangedPath(repo.eventQueue_, newEmptyPath(), events);
11316}
11317function repoOnDisconnectCancel(repo, path, onComplete) {
11318 repo.server_.onDisconnectCancel(path.toString(), (status, errorReason) => {
11319 if (status === 'ok') {
11320 sparseSnapshotTreeForget(repo.onDisconnect_, path);
11321 }
11322 repoCallOnCompleteCallback(repo, onComplete, status, errorReason);
11323 });
11324}
11325function repoOnDisconnectSet(repo, path, value, onComplete) {
11326 const newNode = nodeFromJSON(value);
11327 repo.server_.onDisconnectPut(path.toString(), newNode.val(/*export=*/ true), (status, errorReason) => {
11328 if (status === 'ok') {
11329 sparseSnapshotTreeRemember(repo.onDisconnect_, path, newNode);
11330 }
11331 repoCallOnCompleteCallback(repo, onComplete, status, errorReason);
11332 });
11333}
11334function repoOnDisconnectSetWithPriority(repo, path, value, priority, onComplete) {
11335 const newNode = nodeFromJSON(value, priority);
11336 repo.server_.onDisconnectPut(path.toString(), newNode.val(/*export=*/ true), (status, errorReason) => {
11337 if (status === 'ok') {
11338 sparseSnapshotTreeRemember(repo.onDisconnect_, path, newNode);
11339 }
11340 repoCallOnCompleteCallback(repo, onComplete, status, errorReason);
11341 });
11342}
11343function repoOnDisconnectUpdate(repo, path, childrenToMerge, onComplete) {
11344 if (isEmpty(childrenToMerge)) {
11345 log("onDisconnect().update() called with empty data. Don't do anything.");
11346 repoCallOnCompleteCallback(repo, onComplete, 'ok', undefined);
11347 return;
11348 }
11349 repo.server_.onDisconnectMerge(path.toString(), childrenToMerge, (status, errorReason) => {
11350 if (status === 'ok') {
11351 each(childrenToMerge, (childName, childNode) => {
11352 const newChildNode = nodeFromJSON(childNode);
11353 sparseSnapshotTreeRemember(repo.onDisconnect_, pathChild(path, childName), newChildNode);
11354 });
11355 }
11356 repoCallOnCompleteCallback(repo, onComplete, status, errorReason);
11357 });
11358}
11359function repoAddEventCallbackForQuery(repo, query, eventRegistration) {
11360 let events;
11361 if (pathGetFront(query._path) === '.info') {
11362 events = syncTreeAddEventRegistration(repo.infoSyncTree_, query, eventRegistration);
11363 }
11364 else {
11365 events = syncTreeAddEventRegistration(repo.serverSyncTree_, query, eventRegistration);
11366 }
11367 eventQueueRaiseEventsAtPath(repo.eventQueue_, query._path, events);
11368}
11369function repoRemoveEventCallbackForQuery(repo, query, eventRegistration) {
11370 // These are guaranteed not to raise events, since we're not passing in a cancelError. However, we can future-proof
11371 // a little bit by handling the return values anyways.
11372 let events;
11373 if (pathGetFront(query._path) === '.info') {
11374 events = syncTreeRemoveEventRegistration(repo.infoSyncTree_, query, eventRegistration);
11375 }
11376 else {
11377 events = syncTreeRemoveEventRegistration(repo.serverSyncTree_, query, eventRegistration);
11378 }
11379 eventQueueRaiseEventsAtPath(repo.eventQueue_, query._path, events);
11380}
11381function repoInterrupt(repo) {
11382 if (repo.persistentConnection_) {
11383 repo.persistentConnection_.interrupt(INTERRUPT_REASON);
11384 }
11385}
11386function repoResume(repo) {
11387 if (repo.persistentConnection_) {
11388 repo.persistentConnection_.resume(INTERRUPT_REASON);
11389 }
11390}
11391function repoLog(repo, ...varArgs) {
11392 let prefix = '';
11393 if (repo.persistentConnection_) {
11394 prefix = repo.persistentConnection_.id + ':';
11395 }
11396 log(prefix, ...varArgs);
11397}
11398function repoCallOnCompleteCallback(repo, callback, status, errorReason) {
11399 if (callback) {
11400 exceptionGuard(() => {
11401 if (status === 'ok') {
11402 callback(null);
11403 }
11404 else {
11405 const code = (status || 'error').toUpperCase();
11406 let message = code;
11407 if (errorReason) {
11408 message += ': ' + errorReason;
11409 }
11410 const error = new Error(message);
11411 // eslint-disable-next-line @typescript-eslint/no-explicit-any
11412 error.code = code;
11413 callback(error);
11414 }
11415 });
11416 }
11417}
11418/**
11419 * Creates a new transaction, adds it to the transactions we're tracking, and
11420 * sends it to the server if possible.
11421 *
11422 * @param path - Path at which to do transaction.
11423 * @param transactionUpdate - Update callback.
11424 * @param onComplete - Completion callback.
11425 * @param unwatcher - Function that will be called when the transaction no longer
11426 * need data updates for `path`.
11427 * @param applyLocally - Whether or not to make intermediate results visible
11428 */
11429function repoStartTransaction(repo, path, transactionUpdate, onComplete, unwatcher, applyLocally) {
11430 repoLog(repo, 'transaction on ' + path);
11431 // Initialize transaction.
11432 const transaction = {
11433 path,
11434 update: transactionUpdate,
11435 onComplete,
11436 // One of TransactionStatus enums.
11437 status: null,
11438 // Used when combining transactions at different locations to figure out
11439 // which one goes first.
11440 order: LUIDGenerator(),
11441 // Whether to raise local events for this transaction.
11442 applyLocally,
11443 // Count of how many times we've retried the transaction.
11444 retryCount: 0,
11445 // Function to call to clean up our .on() listener.
11446 unwatcher,
11447 // Stores why a transaction was aborted.
11448 abortReason: null,
11449 currentWriteId: null,
11450 currentInputSnapshot: null,
11451 currentOutputSnapshotRaw: null,
11452 currentOutputSnapshotResolved: null
11453 };
11454 // Run transaction initially.
11455 const currentState = repoGetLatestState(repo, path, undefined);
11456 transaction.currentInputSnapshot = currentState;
11457 const newVal = transaction.update(currentState.val());
11458 if (newVal === undefined) {
11459 // Abort transaction.
11460 transaction.unwatcher();
11461 transaction.currentOutputSnapshotRaw = null;
11462 transaction.currentOutputSnapshotResolved = null;
11463 if (transaction.onComplete) {
11464 transaction.onComplete(null, false, transaction.currentInputSnapshot);
11465 }
11466 }
11467 else {
11468 validateFirebaseData('transaction failed: Data returned ', newVal, transaction.path);
11469 // Mark as run and add to our queue.
11470 transaction.status = 0 /* RUN */;
11471 const queueNode = treeSubTree(repo.transactionQueueTree_, path);
11472 const nodeQueue = treeGetValue(queueNode) || [];
11473 nodeQueue.push(transaction);
11474 treeSetValue(queueNode, nodeQueue);
11475 // Update visibleData and raise events
11476 // Note: We intentionally raise events after updating all of our
11477 // transaction state, since the user could start new transactions from the
11478 // event callbacks.
11479 let priorityForNode;
11480 if (typeof newVal === 'object' &&
11481 newVal !== null &&
11482 contains(newVal, '.priority')) {
11483 // eslint-disable-next-line @typescript-eslint/no-explicit-any
11484 priorityForNode = safeGet(newVal, '.priority');
11485 assert(isValidPriority(priorityForNode), 'Invalid priority returned by transaction. ' +
11486 'Priority must be a valid string, finite number, server value, or null.');
11487 }
11488 else {
11489 const currentNode = syncTreeCalcCompleteEventCache(repo.serverSyncTree_, path) ||
11490 ChildrenNode.EMPTY_NODE;
11491 priorityForNode = currentNode.getPriority().val();
11492 }
11493 const serverValues = repoGenerateServerValues(repo);
11494 const newNodeUnresolved = nodeFromJSON(newVal, priorityForNode);
11495 const newNode = resolveDeferredValueSnapshot(newNodeUnresolved, currentState, serverValues);
11496 transaction.currentOutputSnapshotRaw = newNodeUnresolved;
11497 transaction.currentOutputSnapshotResolved = newNode;
11498 transaction.currentWriteId = repoGetNextWriteId(repo);
11499 const events = syncTreeApplyUserOverwrite(repo.serverSyncTree_, path, newNode, transaction.currentWriteId, transaction.applyLocally);
11500 eventQueueRaiseEventsForChangedPath(repo.eventQueue_, path, events);
11501 repoSendReadyTransactions(repo, repo.transactionQueueTree_);
11502 }
11503}
11504/**
11505 * @param excludeSets - A specific set to exclude
11506 */
11507function repoGetLatestState(repo, path, excludeSets) {
11508 return (syncTreeCalcCompleteEventCache(repo.serverSyncTree_, path, excludeSets) ||
11509 ChildrenNode.EMPTY_NODE);
11510}
11511/**
11512 * Sends any already-run transactions that aren't waiting for outstanding
11513 * transactions to complete.
11514 *
11515 * Externally it's called with no arguments, but it calls itself recursively
11516 * with a particular transactionQueueTree node to recurse through the tree.
11517 *
11518 * @param node - transactionQueueTree node to start at.
11519 */
11520function repoSendReadyTransactions(repo, node = repo.transactionQueueTree_) {
11521 // Before recursing, make sure any completed transactions are removed.
11522 if (!node) {
11523 repoPruneCompletedTransactionsBelowNode(repo, node);
11524 }
11525 if (treeGetValue(node)) {
11526 const queue = repoBuildTransactionQueue(repo, node);
11527 assert(queue.length > 0, 'Sending zero length transaction queue');
11528 const allRun = queue.every((transaction) => transaction.status === 0 /* RUN */);
11529 // If they're all run (and not sent), we can send them. Else, we must wait.
11530 if (allRun) {
11531 repoSendTransactionQueue(repo, treeGetPath(node), queue);
11532 }
11533 }
11534 else if (treeHasChildren(node)) {
11535 treeForEachChild(node, childNode => {
11536 repoSendReadyTransactions(repo, childNode);
11537 });
11538 }
11539}
11540/**
11541 * Given a list of run transactions, send them to the server and then handle
11542 * the result (success or failure).
11543 *
11544 * @param path - The location of the queue.
11545 * @param queue - Queue of transactions under the specified location.
11546 */
11547function repoSendTransactionQueue(repo, path, queue) {
11548 // Mark transactions as sent and increment retry count!
11549 const setsToIgnore = queue.map(txn => {
11550 return txn.currentWriteId;
11551 });
11552 const latestState = repoGetLatestState(repo, path, setsToIgnore);
11553 let snapToSend = latestState;
11554 const latestHash = latestState.hash();
11555 for (let i = 0; i < queue.length; i++) {
11556 const txn = queue[i];
11557 assert(txn.status === 0 /* RUN */, 'tryToSendTransactionQueue_: items in queue should all be run.');
11558 txn.status = 1 /* SENT */;
11559 txn.retryCount++;
11560 const relativePath = newRelativePath(path, txn.path);
11561 // If we've gotten to this point, the output snapshot must be defined.
11562 snapToSend = snapToSend.updateChild(relativePath /** @type {!Node} */, txn.currentOutputSnapshotRaw);
11563 }
11564 const dataToSend = snapToSend.val(true);
11565 const pathToSend = path;
11566 // Send the put.
11567 repo.server_.put(pathToSend.toString(), dataToSend, (status) => {
11568 repoLog(repo, 'transaction put response', {
11569 path: pathToSend.toString(),
11570 status
11571 });
11572 let events = [];
11573 if (status === 'ok') {
11574 // Queue up the callbacks and fire them after cleaning up all of our
11575 // transaction state, since the callback could trigger more
11576 // transactions or sets.
11577 const callbacks = [];
11578 for (let i = 0; i < queue.length; i++) {
11579 queue[i].status = 2 /* COMPLETED */;
11580 events = events.concat(syncTreeAckUserWrite(repo.serverSyncTree_, queue[i].currentWriteId));
11581 if (queue[i].onComplete) {
11582 // We never unset the output snapshot, and given that this
11583 // transaction is complete, it should be set
11584 callbacks.push(() => queue[i].onComplete(null, true, queue[i].currentOutputSnapshotResolved));
11585 }
11586 queue[i].unwatcher();
11587 }
11588 // Now remove the completed transactions.
11589 repoPruneCompletedTransactionsBelowNode(repo, treeSubTree(repo.transactionQueueTree_, path));
11590 // There may be pending transactions that we can now send.
11591 repoSendReadyTransactions(repo, repo.transactionQueueTree_);
11592 eventQueueRaiseEventsForChangedPath(repo.eventQueue_, path, events);
11593 // Finally, trigger onComplete callbacks.
11594 for (let i = 0; i < callbacks.length; i++) {
11595 exceptionGuard(callbacks[i]);
11596 }
11597 }
11598 else {
11599 // transactions are no longer sent. Update their status appropriately.
11600 if (status === 'datastale') {
11601 for (let i = 0; i < queue.length; i++) {
11602 if (queue[i].status === 3 /* SENT_NEEDS_ABORT */) {
11603 queue[i].status = 4 /* NEEDS_ABORT */;
11604 }
11605 else {
11606 queue[i].status = 0 /* RUN */;
11607 }
11608 }
11609 }
11610 else {
11611 warn('transaction at ' + pathToSend.toString() + ' failed: ' + status);
11612 for (let i = 0; i < queue.length; i++) {
11613 queue[i].status = 4 /* NEEDS_ABORT */;
11614 queue[i].abortReason = status;
11615 }
11616 }
11617 repoRerunTransactions(repo, path);
11618 }
11619 }, latestHash);
11620}
11621/**
11622 * Finds all transactions dependent on the data at changedPath and reruns them.
11623 *
11624 * Should be called any time cached data changes.
11625 *
11626 * Return the highest path that was affected by rerunning transactions. This
11627 * is the path at which events need to be raised for.
11628 *
11629 * @param changedPath - The path in mergedData that changed.
11630 * @returns The rootmost path that was affected by rerunning transactions.
11631 */
11632function repoRerunTransactions(repo, changedPath) {
11633 const rootMostTransactionNode = repoGetAncestorTransactionNode(repo, changedPath);
11634 const path = treeGetPath(rootMostTransactionNode);
11635 const queue = repoBuildTransactionQueue(repo, rootMostTransactionNode);
11636 repoRerunTransactionQueue(repo, queue, path);
11637 return path;
11638}
11639/**
11640 * Does all the work of rerunning transactions (as well as cleans up aborted
11641 * transactions and whatnot).
11642 *
11643 * @param queue - The queue of transactions to run.
11644 * @param path - The path the queue is for.
11645 */
11646function repoRerunTransactionQueue(repo, queue, path) {
11647 if (queue.length === 0) {
11648 return; // Nothing to do!
11649 }
11650 // Queue up the callbacks and fire them after cleaning up all of our
11651 // transaction state, since the callback could trigger more transactions or
11652 // sets.
11653 const callbacks = [];
11654 let events = [];
11655 // Ignore all of the sets we're going to re-run.
11656 const txnsToRerun = queue.filter(q => {
11657 return q.status === 0 /* RUN */;
11658 });
11659 const setsToIgnore = txnsToRerun.map(q => {
11660 return q.currentWriteId;
11661 });
11662 for (let i = 0; i < queue.length; i++) {
11663 const transaction = queue[i];
11664 const relativePath = newRelativePath(path, transaction.path);
11665 let abortTransaction = false, abortReason;
11666 assert(relativePath !== null, 'rerunTransactionsUnderNode_: relativePath should not be null.');
11667 if (transaction.status === 4 /* NEEDS_ABORT */) {
11668 abortTransaction = true;
11669 abortReason = transaction.abortReason;
11670 events = events.concat(syncTreeAckUserWrite(repo.serverSyncTree_, transaction.currentWriteId, true));
11671 }
11672 else if (transaction.status === 0 /* RUN */) {
11673 if (transaction.retryCount >= MAX_TRANSACTION_RETRIES) {
11674 abortTransaction = true;
11675 abortReason = 'maxretry';
11676 events = events.concat(syncTreeAckUserWrite(repo.serverSyncTree_, transaction.currentWriteId, true));
11677 }
11678 else {
11679 // This code reruns a transaction
11680 const currentNode = repoGetLatestState(repo, transaction.path, setsToIgnore);
11681 transaction.currentInputSnapshot = currentNode;
11682 const newData = queue[i].update(currentNode.val());
11683 if (newData !== undefined) {
11684 validateFirebaseData('transaction failed: Data returned ', newData, transaction.path);
11685 let newDataNode = nodeFromJSON(newData);
11686 const hasExplicitPriority = typeof newData === 'object' &&
11687 newData != null &&
11688 contains(newData, '.priority');
11689 if (!hasExplicitPriority) {
11690 // Keep the old priority if there wasn't a priority explicitly specified.
11691 newDataNode = newDataNode.updatePriority(currentNode.getPriority());
11692 }
11693 const oldWriteId = transaction.currentWriteId;
11694 const serverValues = repoGenerateServerValues(repo);
11695 const newNodeResolved = resolveDeferredValueSnapshot(newDataNode, currentNode, serverValues);
11696 transaction.currentOutputSnapshotRaw = newDataNode;
11697 transaction.currentOutputSnapshotResolved = newNodeResolved;
11698 transaction.currentWriteId = repoGetNextWriteId(repo);
11699 // Mutates setsToIgnore in place
11700 setsToIgnore.splice(setsToIgnore.indexOf(oldWriteId), 1);
11701 events = events.concat(syncTreeApplyUserOverwrite(repo.serverSyncTree_, transaction.path, newNodeResolved, transaction.currentWriteId, transaction.applyLocally));
11702 events = events.concat(syncTreeAckUserWrite(repo.serverSyncTree_, oldWriteId, true));
11703 }
11704 else {
11705 abortTransaction = true;
11706 abortReason = 'nodata';
11707 events = events.concat(syncTreeAckUserWrite(repo.serverSyncTree_, transaction.currentWriteId, true));
11708 }
11709 }
11710 }
11711 eventQueueRaiseEventsForChangedPath(repo.eventQueue_, path, events);
11712 events = [];
11713 if (abortTransaction) {
11714 // Abort.
11715 queue[i].status = 2 /* COMPLETED */;
11716 // Removing a listener can trigger pruning which can muck with
11717 // mergedData/visibleData (as it prunes data). So defer the unwatcher
11718 // until we're done.
11719 (function (unwatcher) {
11720 setTimeout(unwatcher, Math.floor(0));
11721 })(queue[i].unwatcher);
11722 if (queue[i].onComplete) {
11723 if (abortReason === 'nodata') {
11724 callbacks.push(() => queue[i].onComplete(null, false, queue[i].currentInputSnapshot));
11725 }
11726 else {
11727 callbacks.push(() => queue[i].onComplete(new Error(abortReason), false, null));
11728 }
11729 }
11730 }
11731 }
11732 // Clean up completed transactions.
11733 repoPruneCompletedTransactionsBelowNode(repo, repo.transactionQueueTree_);
11734 // Now fire callbacks, now that we're in a good, known state.
11735 for (let i = 0; i < callbacks.length; i++) {
11736 exceptionGuard(callbacks[i]);
11737 }
11738 // Try to send the transaction result to the server.
11739 repoSendReadyTransactions(repo, repo.transactionQueueTree_);
11740}
11741/**
11742 * Returns the rootmost ancestor node of the specified path that has a pending
11743 * transaction on it, or just returns the node for the given path if there are
11744 * no pending transactions on any ancestor.
11745 *
11746 * @param path - The location to start at.
11747 * @returns The rootmost node with a transaction.
11748 */
11749function repoGetAncestorTransactionNode(repo, path) {
11750 let front;
11751 // Start at the root and walk deeper into the tree towards path until we
11752 // find a node with pending transactions.
11753 let transactionNode = repo.transactionQueueTree_;
11754 front = pathGetFront(path);
11755 while (front !== null && treeGetValue(transactionNode) === undefined) {
11756 transactionNode = treeSubTree(transactionNode, front);
11757 path = pathPopFront(path);
11758 front = pathGetFront(path);
11759 }
11760 return transactionNode;
11761}
11762/**
11763 * Builds the queue of all transactions at or below the specified
11764 * transactionNode.
11765 *
11766 * @param transactionNode
11767 * @returns The generated queue.
11768 */
11769function repoBuildTransactionQueue(repo, transactionNode) {
11770 // Walk any child transaction queues and aggregate them into a single queue.
11771 const transactionQueue = [];
11772 repoAggregateTransactionQueuesForNode(repo, transactionNode, transactionQueue);
11773 // Sort them by the order the transactions were created.
11774 transactionQueue.sort((a, b) => a.order - b.order);
11775 return transactionQueue;
11776}
11777function repoAggregateTransactionQueuesForNode(repo, node, queue) {
11778 const nodeQueue = treeGetValue(node);
11779 if (nodeQueue) {
11780 for (let i = 0; i < nodeQueue.length; i++) {
11781 queue.push(nodeQueue[i]);
11782 }
11783 }
11784 treeForEachChild(node, child => {
11785 repoAggregateTransactionQueuesForNode(repo, child, queue);
11786 });
11787}
11788/**
11789 * Remove COMPLETED transactions at or below this node in the transactionQueueTree_.
11790 */
11791function repoPruneCompletedTransactionsBelowNode(repo, node) {
11792 const queue = treeGetValue(node);
11793 if (queue) {
11794 let to = 0;
11795 for (let from = 0; from < queue.length; from++) {
11796 if (queue[from].status !== 2 /* COMPLETED */) {
11797 queue[to] = queue[from];
11798 to++;
11799 }
11800 }
11801 queue.length = to;
11802 treeSetValue(node, queue.length > 0 ? queue : undefined);
11803 }
11804 treeForEachChild(node, childNode => {
11805 repoPruneCompletedTransactionsBelowNode(repo, childNode);
11806 });
11807}
11808/**
11809 * Aborts all transactions on ancestors or descendants of the specified path.
11810 * Called when doing a set() or update() since we consider them incompatible
11811 * with transactions.
11812 *
11813 * @param path - Path for which we want to abort related transactions.
11814 */
11815function repoAbortTransactions(repo, path) {
11816 const affectedPath = treeGetPath(repoGetAncestorTransactionNode(repo, path));
11817 const transactionNode = treeSubTree(repo.transactionQueueTree_, path);
11818 treeForEachAncestor(transactionNode, (node) => {
11819 repoAbortTransactionsOnNode(repo, node);
11820 });
11821 repoAbortTransactionsOnNode(repo, transactionNode);
11822 treeForEachDescendant(transactionNode, (node) => {
11823 repoAbortTransactionsOnNode(repo, node);
11824 });
11825 return affectedPath;
11826}
11827/**
11828 * Abort transactions stored in this transaction queue node.
11829 *
11830 * @param node - Node to abort transactions for.
11831 */
11832function repoAbortTransactionsOnNode(repo, node) {
11833 const queue = treeGetValue(node);
11834 if (queue) {
11835 // Queue up the callbacks and fire them after cleaning up all of our
11836 // transaction state, since the callback could trigger more transactions
11837 // or sets.
11838 const callbacks = [];
11839 // Go through queue. Any already-sent transactions must be marked for
11840 // abort, while the unsent ones can be immediately aborted and removed.
11841 let events = [];
11842 let lastSent = -1;
11843 for (let i = 0; i < queue.length; i++) {
11844 if (queue[i].status === 3 /* SENT_NEEDS_ABORT */) ;
11845 else if (queue[i].status === 1 /* SENT */) {
11846 assert(lastSent === i - 1, 'All SENT items should be at beginning of queue.');
11847 lastSent = i;
11848 // Mark transaction for abort when it comes back.
11849 queue[i].status = 3 /* SENT_NEEDS_ABORT */;
11850 queue[i].abortReason = 'set';
11851 }
11852 else {
11853 assert(queue[i].status === 0 /* RUN */, 'Unexpected transaction status in abort');
11854 // We can abort it immediately.
11855 queue[i].unwatcher();
11856 events = events.concat(syncTreeAckUserWrite(repo.serverSyncTree_, queue[i].currentWriteId, true));
11857 if (queue[i].onComplete) {
11858 callbacks.push(queue[i].onComplete.bind(null, new Error('set'), false, null));
11859 }
11860 }
11861 }
11862 if (lastSent === -1) {
11863 // We're not waiting for any sent transactions. We can clear the queue.
11864 treeSetValue(node, undefined);
11865 }
11866 else {
11867 // Remove the transactions we aborted.
11868 queue.length = lastSent + 1;
11869 }
11870 // Now fire the callbacks.
11871 eventQueueRaiseEventsForChangedPath(repo.eventQueue_, treeGetPath(node), events);
11872 for (let i = 0; i < callbacks.length; i++) {
11873 exceptionGuard(callbacks[i]);
11874 }
11875 }
11876}
11877
11878/**
11879 * @license
11880 * Copyright 2017 Google LLC
11881 *
11882 * Licensed under the Apache License, Version 2.0 (the "License");
11883 * you may not use this file except in compliance with the License.
11884 * You may obtain a copy of the License at
11885 *
11886 * http://www.apache.org/licenses/LICENSE-2.0
11887 *
11888 * Unless required by applicable law or agreed to in writing, software
11889 * distributed under the License is distributed on an "AS IS" BASIS,
11890 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11891 * See the License for the specific language governing permissions and
11892 * limitations under the License.
11893 */
11894function decodePath(pathString) {
11895 let pathStringDecoded = '';
11896 const pieces = pathString.split('/');
11897 for (let i = 0; i < pieces.length; i++) {
11898 if (pieces[i].length > 0) {
11899 let piece = pieces[i];
11900 try {
11901 piece = decodeURIComponent(piece.replace(/\+/g, ' '));
11902 }
11903 catch (e) { }
11904 pathStringDecoded += '/' + piece;
11905 }
11906 }
11907 return pathStringDecoded;
11908}
11909/**
11910 * @returns key value hash
11911 */
11912function decodeQuery(queryString) {
11913 const results = {};
11914 if (queryString.charAt(0) === '?') {
11915 queryString = queryString.substring(1);
11916 }
11917 for (const segment of queryString.split('&')) {
11918 if (segment.length === 0) {
11919 continue;
11920 }
11921 const kv = segment.split('=');
11922 if (kv.length === 2) {
11923 results[decodeURIComponent(kv[0])] = decodeURIComponent(kv[1]);
11924 }
11925 else {
11926 warn(`Invalid query segment '${segment}' in query '${queryString}'`);
11927 }
11928 }
11929 return results;
11930}
11931const parseRepoInfo = function (dataURL, nodeAdmin) {
11932 const parsedUrl = parseDatabaseURL(dataURL), namespace = parsedUrl.namespace;
11933 if (parsedUrl.domain === 'firebase.com') {
11934 fatal(parsedUrl.host +
11935 ' is no longer supported. ' +
11936 'Please use <YOUR FIREBASE>.firebaseio.com instead');
11937 }
11938 // Catch common error of uninitialized namespace value.
11939 if ((!namespace || namespace === 'undefined') &&
11940 parsedUrl.domain !== 'localhost') {
11941 fatal('Cannot parse Firebase url. Please use https://<YOUR FIREBASE>.firebaseio.com');
11942 }
11943 if (!parsedUrl.secure) {
11944 warnIfPageIsSecure();
11945 }
11946 const webSocketOnly = parsedUrl.scheme === 'ws' || parsedUrl.scheme === 'wss';
11947 return {
11948 repoInfo: new RepoInfo(parsedUrl.host, parsedUrl.secure, namespace, webSocketOnly, nodeAdmin,
11949 /*persistenceKey=*/ '',
11950 /*includeNamespaceInQueryParams=*/ namespace !== parsedUrl.subdomain),
11951 path: new Path(parsedUrl.pathString)
11952 };
11953};
11954const parseDatabaseURL = function (dataURL) {
11955 // Default to empty strings in the event of a malformed string.
11956 let host = '', domain = '', subdomain = '', pathString = '', namespace = '';
11957 // Always default to SSL, unless otherwise specified.
11958 let secure = true, scheme = 'https', port = 443;
11959 // Don't do any validation here. The caller is responsible for validating the result of parsing.
11960 if (typeof dataURL === 'string') {
11961 // Parse scheme.
11962 let colonInd = dataURL.indexOf('//');
11963 if (colonInd >= 0) {
11964 scheme = dataURL.substring(0, colonInd - 1);
11965 dataURL = dataURL.substring(colonInd + 2);
11966 }
11967 // Parse host, path, and query string.
11968 let slashInd = dataURL.indexOf('/');
11969 if (slashInd === -1) {
11970 slashInd = dataURL.length;
11971 }
11972 let questionMarkInd = dataURL.indexOf('?');
11973 if (questionMarkInd === -1) {
11974 questionMarkInd = dataURL.length;
11975 }
11976 host = dataURL.substring(0, Math.min(slashInd, questionMarkInd));
11977 if (slashInd < questionMarkInd) {
11978 // For pathString, questionMarkInd will always come after slashInd
11979 pathString = decodePath(dataURL.substring(slashInd, questionMarkInd));
11980 }
11981 const queryParams = decodeQuery(dataURL.substring(Math.min(dataURL.length, questionMarkInd)));
11982 // If we have a port, use scheme for determining if it's secure.
11983 colonInd = host.indexOf(':');
11984 if (colonInd >= 0) {
11985 secure = scheme === 'https' || scheme === 'wss';
11986 port = parseInt(host.substring(colonInd + 1), 10);
11987 }
11988 else {
11989 colonInd = host.length;
11990 }
11991 const hostWithoutPort = host.slice(0, colonInd);
11992 if (hostWithoutPort.toLowerCase() === 'localhost') {
11993 domain = 'localhost';
11994 }
11995 else if (hostWithoutPort.split('.').length <= 2) {
11996 domain = hostWithoutPort;
11997 }
11998 else {
11999 // Interpret the subdomain of a 3 or more component URL as the namespace name.
12000 const dotInd = host.indexOf('.');
12001 subdomain = host.substring(0, dotInd).toLowerCase();
12002 domain = host.substring(dotInd + 1);
12003 // Normalize namespaces to lowercase to share storage / connection.
12004 namespace = subdomain;
12005 }
12006 // Always treat the value of the `ns` as the namespace name if it is present.
12007 if ('ns' in queryParams) {
12008 namespace = queryParams['ns'];
12009 }
12010 }
12011 return {
12012 host,
12013 port,
12014 domain,
12015 subdomain,
12016 secure,
12017 scheme,
12018 pathString,
12019 namespace
12020 };
12021};
12022
12023/**
12024 * @license
12025 * Copyright 2017 Google LLC
12026 *
12027 * Licensed under the Apache License, Version 2.0 (the "License");
12028 * you may not use this file except in compliance with the License.
12029 * You may obtain a copy of the License at
12030 *
12031 * http://www.apache.org/licenses/LICENSE-2.0
12032 *
12033 * Unless required by applicable law or agreed to in writing, software
12034 * distributed under the License is distributed on an "AS IS" BASIS,
12035 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12036 * See the License for the specific language governing permissions and
12037 * limitations under the License.
12038 */
12039/**
12040 * Encapsulates the data needed to raise an event
12041 */
12042class DataEvent {
12043 /**
12044 * @param eventType - One of: value, child_added, child_changed, child_moved, child_removed
12045 * @param eventRegistration - The function to call to with the event data. User provided
12046 * @param snapshot - The data backing the event
12047 * @param prevName - Optional, the name of the previous child for child_* events.
12048 */
12049 constructor(eventType, eventRegistration, snapshot, prevName) {
12050 this.eventType = eventType;
12051 this.eventRegistration = eventRegistration;
12052 this.snapshot = snapshot;
12053 this.prevName = prevName;
12054 }
12055 getPath() {
12056 const ref = this.snapshot.ref;
12057 if (this.eventType === 'value') {
12058 return ref._path;
12059 }
12060 else {
12061 return ref.parent._path;
12062 }
12063 }
12064 getEventType() {
12065 return this.eventType;
12066 }
12067 getEventRunner() {
12068 return this.eventRegistration.getEventRunner(this);
12069 }
12070 toString() {
12071 return (this.getPath().toString() +
12072 ':' +
12073 this.eventType +
12074 ':' +
12075 stringify(this.snapshot.exportVal()));
12076 }
12077}
12078class CancelEvent {
12079 constructor(eventRegistration, error, path) {
12080 this.eventRegistration = eventRegistration;
12081 this.error = error;
12082 this.path = path;
12083 }
12084 getPath() {
12085 return this.path;
12086 }
12087 getEventType() {
12088 return 'cancel';
12089 }
12090 getEventRunner() {
12091 return this.eventRegistration.getEventRunner(this);
12092 }
12093 toString() {
12094 return this.path.toString() + ':cancel';
12095 }
12096}
12097
12098/**
12099 * @license
12100 * Copyright 2017 Google LLC
12101 *
12102 * Licensed under the Apache License, Version 2.0 (the "License");
12103 * you may not use this file except in compliance with the License.
12104 * You may obtain a copy of the License at
12105 *
12106 * http://www.apache.org/licenses/LICENSE-2.0
12107 *
12108 * Unless required by applicable law or agreed to in writing, software
12109 * distributed under the License is distributed on an "AS IS" BASIS,
12110 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12111 * See the License for the specific language governing permissions and
12112 * limitations under the License.
12113 */
12114/**
12115 * A wrapper class that converts events from the database@exp SDK to the legacy
12116 * Database SDK. Events are not converted directly as event registration relies
12117 * on reference comparison of the original user callback (see `matches()`) and
12118 * relies on equality of the legacy SDK's `context` object.
12119 */
12120class CallbackContext {
12121 constructor(snapshotCallback, cancelCallback) {
12122 this.snapshotCallback = snapshotCallback;
12123 this.cancelCallback = cancelCallback;
12124 }
12125 onValue(expDataSnapshot, previousChildName) {
12126 this.snapshotCallback.call(null, expDataSnapshot, previousChildName);
12127 }
12128 onCancel(error) {
12129 assert(this.hasCancelCallback, 'Raising a cancel event on a listener with no cancel callback');
12130 return this.cancelCallback.call(null, error);
12131 }
12132 get hasCancelCallback() {
12133 return !!this.cancelCallback;
12134 }
12135 matches(other) {
12136 return (this.snapshotCallback === other.snapshotCallback ||
12137 (this.snapshotCallback.userCallback !== undefined &&
12138 this.snapshotCallback.userCallback ===
12139 other.snapshotCallback.userCallback &&
12140 this.snapshotCallback.context === other.snapshotCallback.context));
12141 }
12142}
12143
12144/**
12145 * @license
12146 * Copyright 2021 Google LLC
12147 *
12148 * Licensed under the Apache License, Version 2.0 (the "License");
12149 * you may not use this file except in compliance with the License.
12150 * You may obtain a copy of the License at
12151 *
12152 * http://www.apache.org/licenses/LICENSE-2.0
12153 *
12154 * Unless required by applicable law or agreed to in writing, software
12155 * distributed under the License is distributed on an "AS IS" BASIS,
12156 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12157 * See the License for the specific language governing permissions and
12158 * limitations under the License.
12159 */
12160/**
12161 * The `onDisconnect` class allows you to write or clear data when your client
12162 * disconnects from the Database server. These updates occur whether your
12163 * client disconnects cleanly or not, so you can rely on them to clean up data
12164 * even if a connection is dropped or a client crashes.
12165 *
12166 * The `onDisconnect` class is most commonly used to manage presence in
12167 * applications where it is useful to detect how many clients are connected and
12168 * when other clients disconnect. See
12169 * {@link https://firebase.google.com/docs/database/web/offline-capabilities | Enabling Offline Capabilities in JavaScript}
12170 * for more information.
12171 *
12172 * To avoid problems when a connection is dropped before the requests can be
12173 * transferred to the Database server, these functions should be called before
12174 * writing any data.
12175 *
12176 * Note that `onDisconnect` operations are only triggered once. If you want an
12177 * operation to occur each time a disconnect occurs, you'll need to re-establish
12178 * the `onDisconnect` operations each time you reconnect.
12179 */
12180class OnDisconnect {
12181 /** @hideconstructor */
12182 constructor(_repo, _path) {
12183 this._repo = _repo;
12184 this._path = _path;
12185 }
12186 /**
12187 * Cancels all previously queued `onDisconnect()` set or update events for this
12188 * location and all children.
12189 *
12190 * If a write has been queued for this location via a `set()` or `update()` at a
12191 * parent location, the write at this location will be canceled, though writes
12192 * to sibling locations will still occur.
12193 *
12194 * @returns Resolves when synchronization to the server is complete.
12195 */
12196 cancel() {
12197 const deferred = new Deferred();
12198 repoOnDisconnectCancel(this._repo, this._path, deferred.wrapCallback(() => { }));
12199 return deferred.promise;
12200 }
12201 /**
12202 * Ensures the data at this location is deleted when the client is disconnected
12203 * (due to closing the browser, navigating to a new page, or network issues).
12204 *
12205 * @returns Resolves when synchronization to the server is complete.
12206 */
12207 remove() {
12208 validateWritablePath('OnDisconnect.remove', this._path);
12209 const deferred = new Deferred();
12210 repoOnDisconnectSet(this._repo, this._path, null, deferred.wrapCallback(() => { }));
12211 return deferred.promise;
12212 }
12213 /**
12214 * Ensures the data at this location is set to the specified value when the
12215 * client is disconnected (due to closing the browser, navigating to a new page,
12216 * or network issues).
12217 *
12218 * `set()` is especially useful for implementing "presence" systems, where a
12219 * value should be changed or cleared when a user disconnects so that they
12220 * appear "offline" to other users. See
12221 * {@link https://firebase.google.com/docs/database/web/offline-capabilities | Enabling Offline Capabilities in JavaScript}
12222 * for more information.
12223 *
12224 * Note that `onDisconnect` operations are only triggered once. If you want an
12225 * operation to occur each time a disconnect occurs, you'll need to re-establish
12226 * the `onDisconnect` operations each time.
12227 *
12228 * @param value - The value to be written to this location on disconnect (can
12229 * be an object, array, string, number, boolean, or null).
12230 * @returns Resolves when synchronization to the Database is complete.
12231 */
12232 set(value) {
12233 validateWritablePath('OnDisconnect.set', this._path);
12234 validateFirebaseDataArg('OnDisconnect.set', value, this._path, false);
12235 const deferred = new Deferred();
12236 repoOnDisconnectSet(this._repo, this._path, value, deferred.wrapCallback(() => { }));
12237 return deferred.promise;
12238 }
12239 /**
12240 * Ensures the data at this location is set to the specified value and priority
12241 * when the client is disconnected (due to closing the browser, navigating to a
12242 * new page, or network issues).
12243 *
12244 * @param value - The value to be written to this location on disconnect (can
12245 * be an object, array, string, number, boolean, or null).
12246 * @param priority - The priority to be written (string, number, or null).
12247 * @returns Resolves when synchronization to the Database is complete.
12248 */
12249 setWithPriority(value, priority) {
12250 validateWritablePath('OnDisconnect.setWithPriority', this._path);
12251 validateFirebaseDataArg('OnDisconnect.setWithPriority', value, this._path, false);
12252 validatePriority('OnDisconnect.setWithPriority', priority, false);
12253 const deferred = new Deferred();
12254 repoOnDisconnectSetWithPriority(this._repo, this._path, value, priority, deferred.wrapCallback(() => { }));
12255 return deferred.promise;
12256 }
12257 /**
12258 * Writes multiple values at this location when the client is disconnected (due
12259 * to closing the browser, navigating to a new page, or network issues).
12260 *
12261 * The `values` argument contains multiple property-value pairs that will be
12262 * written to the Database together. Each child property can either be a simple
12263 * property (for example, "name") or a relative path (for example, "name/first")
12264 * from the current location to the data to update.
12265 *
12266 * As opposed to the `set()` method, `update()` can be use to selectively update
12267 * only the referenced properties at the current location (instead of replacing
12268 * all the child properties at the current location).
12269 *
12270 * @param values - Object containing multiple values.
12271 * @returns Resolves when synchronization to the Database is complete.
12272 */
12273 update(values) {
12274 validateWritablePath('OnDisconnect.update', this._path);
12275 validateFirebaseMergeDataArg('OnDisconnect.update', values, this._path, false);
12276 const deferred = new Deferred();
12277 repoOnDisconnectUpdate(this._repo, this._path, values, deferred.wrapCallback(() => { }));
12278 return deferred.promise;
12279 }
12280}
12281
12282/**
12283 * @license
12284 * Copyright 2020 Google LLC
12285 *
12286 * Licensed under the Apache License, Version 2.0 (the "License");
12287 * you may not use this file except in compliance with the License.
12288 * You may obtain a copy of the License at
12289 *
12290 * http://www.apache.org/licenses/LICENSE-2.0
12291 *
12292 * Unless required by applicable law or agreed to in writing, software
12293 * distributed under the License is distributed on an "AS IS" BASIS,
12294 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12295 * See the License for the specific language governing permissions and
12296 * limitations under the License.
12297 */
12298/**
12299 * @internal
12300 */
12301class QueryImpl {
12302 /**
12303 * @hideconstructor
12304 */
12305 constructor(_repo, _path, _queryParams, _orderByCalled) {
12306 this._repo = _repo;
12307 this._path = _path;
12308 this._queryParams = _queryParams;
12309 this._orderByCalled = _orderByCalled;
12310 }
12311 get key() {
12312 if (pathIsEmpty(this._path)) {
12313 return null;
12314 }
12315 else {
12316 return pathGetBack(this._path);
12317 }
12318 }
12319 get ref() {
12320 return new ReferenceImpl(this._repo, this._path);
12321 }
12322 get _queryIdentifier() {
12323 const obj = queryParamsGetQueryObject(this._queryParams);
12324 const id = ObjectToUniqueKey(obj);
12325 return id === '{}' ? 'default' : id;
12326 }
12327 /**
12328 * An object representation of the query parameters used by this Query.
12329 */
12330 get _queryObject() {
12331 return queryParamsGetQueryObject(this._queryParams);
12332 }
12333 isEqual(other) {
12334 other = getModularInstance(other);
12335 if (!(other instanceof QueryImpl)) {
12336 return false;
12337 }
12338 const sameRepo = this._repo === other._repo;
12339 const samePath = pathEquals(this._path, other._path);
12340 const sameQueryIdentifier = this._queryIdentifier === other._queryIdentifier;
12341 return sameRepo && samePath && sameQueryIdentifier;
12342 }
12343 toJSON() {
12344 return this.toString();
12345 }
12346 toString() {
12347 return this._repo.toString() + pathToUrlEncodedString(this._path);
12348 }
12349}
12350/**
12351 * Validates that no other order by call has been made
12352 */
12353function validateNoPreviousOrderByCall(query, fnName) {
12354 if (query._orderByCalled === true) {
12355 throw new Error(fnName + ": You can't combine multiple orderBy calls.");
12356 }
12357}
12358/**
12359 * Validates start/end values for queries.
12360 */
12361function validateQueryEndpoints(params) {
12362 let startNode = null;
12363 let endNode = null;
12364 if (params.hasStart()) {
12365 startNode = params.getIndexStartValue();
12366 }
12367 if (params.hasEnd()) {
12368 endNode = params.getIndexEndValue();
12369 }
12370 if (params.getIndex() === KEY_INDEX) {
12371 const tooManyArgsError = 'Query: When ordering by key, you may only pass one argument to ' +
12372 'startAt(), endAt(), or equalTo().';
12373 const wrongArgTypeError = 'Query: When ordering by key, the argument passed to startAt(), startAfter(), ' +
12374 'endAt(), endBefore(), or equalTo() must be a string.';
12375 if (params.hasStart()) {
12376 const startName = params.getIndexStartName();
12377 if (startName !== MIN_NAME) {
12378 throw new Error(tooManyArgsError);
12379 }
12380 else if (typeof startNode !== 'string') {
12381 throw new Error(wrongArgTypeError);
12382 }
12383 }
12384 if (params.hasEnd()) {
12385 const endName = params.getIndexEndName();
12386 if (endName !== MAX_NAME) {
12387 throw new Error(tooManyArgsError);
12388 }
12389 else if (typeof endNode !== 'string') {
12390 throw new Error(wrongArgTypeError);
12391 }
12392 }
12393 }
12394 else if (params.getIndex() === PRIORITY_INDEX) {
12395 if ((startNode != null && !isValidPriority(startNode)) ||
12396 (endNode != null && !isValidPriority(endNode))) {
12397 throw new Error('Query: When ordering by priority, the first argument passed to startAt(), ' +
12398 'startAfter() endAt(), endBefore(), or equalTo() must be a valid priority value ' +
12399 '(null, a number, or a string).');
12400 }
12401 }
12402 else {
12403 assert(params.getIndex() instanceof PathIndex ||
12404 params.getIndex() === VALUE_INDEX, 'unknown index type.');
12405 if ((startNode != null && typeof startNode === 'object') ||
12406 (endNode != null && typeof endNode === 'object')) {
12407 throw new Error('Query: First argument passed to startAt(), startAfter(), endAt(), endBefore(), or ' +
12408 'equalTo() cannot be an object.');
12409 }
12410 }
12411}
12412/**
12413 * Validates that limit* has been called with the correct combination of parameters
12414 */
12415function validateLimit(params) {
12416 if (params.hasStart() &&
12417 params.hasEnd() &&
12418 params.hasLimit() &&
12419 !params.hasAnchoredLimit()) {
12420 throw new Error("Query: Can't combine startAt(), startAfter(), endAt(), endBefore(), and limit(). Use " +
12421 'limitToFirst() or limitToLast() instead.');
12422 }
12423}
12424/**
12425 * @internal
12426 */
12427class ReferenceImpl extends QueryImpl {
12428 /** @hideconstructor */
12429 constructor(repo, path) {
12430 super(repo, path, new QueryParams(), false);
12431 }
12432 get parent() {
12433 const parentPath = pathParent(this._path);
12434 return parentPath === null
12435 ? null
12436 : new ReferenceImpl(this._repo, parentPath);
12437 }
12438 get root() {
12439 let ref = this;
12440 while (ref.parent !== null) {
12441 ref = ref.parent;
12442 }
12443 return ref;
12444 }
12445}
12446/**
12447 * A `DataSnapshot` contains data from a Database location.
12448 *
12449 * Any time you read data from the Database, you receive the data as a
12450 * `DataSnapshot`. A `DataSnapshot` is passed to the event callbacks you attach
12451 * with `on()` or `once()`. You can extract the contents of the snapshot as a
12452 * JavaScript object by calling the `val()` method. Alternatively, you can
12453 * traverse into the snapshot by calling `child()` to return child snapshots
12454 * (which you could then call `val()` on).
12455 *
12456 * A `DataSnapshot` is an efficiently generated, immutable copy of the data at
12457 * a Database location. It cannot be modified and will never change (to modify
12458 * data, you always call the `set()` method on a `Reference` directly).
12459 */
12460class DataSnapshot {
12461 /**
12462 * @param _node - A SnapshotNode to wrap.
12463 * @param ref - The location this snapshot came from.
12464 * @param _index - The iteration order for this snapshot
12465 * @hideconstructor
12466 */
12467 constructor(_node,
12468 /**
12469 * The location of this DataSnapshot.
12470 */
12471 ref, _index) {
12472 this._node = _node;
12473 this.ref = ref;
12474 this._index = _index;
12475 }
12476 /**
12477 * Gets the priority value of the data in this `DataSnapshot`.
12478 *
12479 * Applications need not use priority but can order collections by
12480 * ordinary properties (see
12481 * {@link https://firebase.google.com/docs/database/web/lists-of-data#sorting_and_filtering_data |Sorting and filtering data}
12482 * ).
12483 */
12484 get priority() {
12485 // typecast here because we never return deferred values or internal priorities (MAX_PRIORITY)
12486 return this._node.getPriority().val();
12487 }
12488 /**
12489 * The key (last part of the path) of the location of this `DataSnapshot`.
12490 *
12491 * The last token in a Database location is considered its key. For example,
12492 * "ada" is the key for the /users/ada/ node. Accessing the key on any
12493 * `DataSnapshot` will return the key for the location that generated it.
12494 * However, accessing the key on the root URL of a Database will return
12495 * `null`.
12496 */
12497 get key() {
12498 return this.ref.key;
12499 }
12500 /** Returns the number of child properties of this `DataSnapshot`. */
12501 get size() {
12502 return this._node.numChildren();
12503 }
12504 /**
12505 * Gets another `DataSnapshot` for the location at the specified relative path.
12506 *
12507 * Passing a relative path to the `child()` method of a DataSnapshot returns
12508 * another `DataSnapshot` for the location at the specified relative path. The
12509 * relative path can either be a simple child name (for example, "ada") or a
12510 * deeper, slash-separated path (for example, "ada/name/first"). If the child
12511 * location has no data, an empty `DataSnapshot` (that is, a `DataSnapshot`
12512 * whose value is `null`) is returned.
12513 *
12514 * @param path - A relative path to the location of child data.
12515 */
12516 child(path) {
12517 const childPath = new Path(path);
12518 const childRef = child(this.ref, path);
12519 return new DataSnapshot(this._node.getChild(childPath), childRef, PRIORITY_INDEX);
12520 }
12521 /**
12522 * Returns true if this `DataSnapshot` contains any data. It is slightly more
12523 * efficient than using `snapshot.val() !== null`.
12524 */
12525 exists() {
12526 return !this._node.isEmpty();
12527 }
12528 /**
12529 * Exports the entire contents of the DataSnapshot as a JavaScript object.
12530 *
12531 * The `exportVal()` method is similar to `val()`, except priority information
12532 * is included (if available), making it suitable for backing up your data.
12533 *
12534 * @returns The DataSnapshot's contents as a JavaScript value (Object,
12535 * Array, string, number, boolean, or `null`).
12536 */
12537 // eslint-disable-next-line @typescript-eslint/no-explicit-any
12538 exportVal() {
12539 return this._node.val(true);
12540 }
12541 /**
12542 * Enumerates the top-level children in the `DataSnapshot`.
12543 *
12544 * Because of the way JavaScript objects work, the ordering of data in the
12545 * JavaScript object returned by `val()` is not guaranteed to match the
12546 * ordering on the server nor the ordering of `onChildAdded()` events. That is
12547 * where `forEach()` comes in handy. It guarantees the children of a
12548 * `DataSnapshot` will be iterated in their query order.
12549 *
12550 * If no explicit `orderBy*()` method is used, results are returned
12551 * ordered by key (unless priorities are used, in which case, results are
12552 * returned by priority).
12553 *
12554 * @param action - A function that will be called for each child DataSnapshot.
12555 * The callback can return true to cancel further enumeration.
12556 * @returns true if enumeration was canceled due to your callback returning
12557 * true.
12558 */
12559 forEach(action) {
12560 if (this._node.isLeafNode()) {
12561 return false;
12562 }
12563 const childrenNode = this._node;
12564 // Sanitize the return value to a boolean. ChildrenNode.forEachChild has a weird return type...
12565 return !!childrenNode.forEachChild(this._index, (key, node) => {
12566 return action(new DataSnapshot(node, child(this.ref, key), PRIORITY_INDEX));
12567 });
12568 }
12569 /**
12570 * Returns true if the specified child path has (non-null) data.
12571 *
12572 * @param path - A relative path to the location of a potential child.
12573 * @returns `true` if data exists at the specified child path; else
12574 * `false`.
12575 */
12576 hasChild(path) {
12577 const childPath = new Path(path);
12578 return !this._node.getChild(childPath).isEmpty();
12579 }
12580 /**
12581 * Returns whether or not the `DataSnapshot` has any non-`null` child
12582 * properties.
12583 *
12584 * You can use `hasChildren()` to determine if a `DataSnapshot` has any
12585 * children. If it does, you can enumerate them using `forEach()`. If it
12586 * doesn't, then either this snapshot contains a primitive value (which can be
12587 * retrieved with `val()`) or it is empty (in which case, `val()` will return
12588 * `null`).
12589 *
12590 * @returns true if this snapshot has any children; else false.
12591 */
12592 hasChildren() {
12593 if (this._node.isLeafNode()) {
12594 return false;
12595 }
12596 else {
12597 return !this._node.isEmpty();
12598 }
12599 }
12600 /**
12601 * Returns a JSON-serializable representation of this object.
12602 */
12603 toJSON() {
12604 return this.exportVal();
12605 }
12606 /**
12607 * Extracts a JavaScript value from a `DataSnapshot`.
12608 *
12609 * Depending on the data in a `DataSnapshot`, the `val()` method may return a
12610 * scalar type (string, number, or boolean), an array, or an object. It may
12611 * also return null, indicating that the `DataSnapshot` is empty (contains no
12612 * data).
12613 *
12614 * @returns The DataSnapshot's contents as a JavaScript value (Object,
12615 * Array, string, number, boolean, or `null`).
12616 */
12617 // eslint-disable-next-line @typescript-eslint/no-explicit-any
12618 val() {
12619 return this._node.val();
12620 }
12621}
12622/**
12623 *
12624 * Returns a `Reference` representing the location in the Database
12625 * corresponding to the provided path. If no path is provided, the `Reference`
12626 * will point to the root of the Database.
12627 *
12628 * @param db - The database instance to obtain a reference for.
12629 * @param path - Optional path representing the location the returned
12630 * `Reference` will point. If not provided, the returned `Reference` will
12631 * point to the root of the Database.
12632 * @returns If a path is provided, a `Reference`
12633 * pointing to the provided path. Otherwise, a `Reference` pointing to the
12634 * root of the Database.
12635 */
12636function ref(db, path) {
12637 db = getModularInstance(db);
12638 db._checkNotDeleted('ref');
12639 return path !== undefined ? child(db._root, path) : db._root;
12640}
12641/**
12642 * Returns a `Reference` representing the location in the Database
12643 * corresponding to the provided Firebase URL.
12644 *
12645 * An exception is thrown if the URL is not a valid Firebase Database URL or it
12646 * has a different domain than the current `Database` instance.
12647 *
12648 * Note that all query parameters (`orderBy`, `limitToLast`, etc.) are ignored
12649 * and are not applied to the returned `Reference`.
12650 *
12651 * @param db - The database instance to obtain a reference for.
12652 * @param url - The Firebase URL at which the returned `Reference` will
12653 * point.
12654 * @returns A `Reference` pointing to the provided
12655 * Firebase URL.
12656 */
12657function refFromURL(db, url) {
12658 db = getModularInstance(db);
12659 db._checkNotDeleted('refFromURL');
12660 const parsedURL = parseRepoInfo(url, db._repo.repoInfo_.nodeAdmin);
12661 validateUrl('refFromURL', parsedURL);
12662 const repoInfo = parsedURL.repoInfo;
12663 if (!db._repo.repoInfo_.isCustomHost() &&
12664 repoInfo.host !== db._repo.repoInfo_.host) {
12665 fatal('refFromURL' +
12666 ': Host name does not match the current database: ' +
12667 '(found ' +
12668 repoInfo.host +
12669 ' but expected ' +
12670 db._repo.repoInfo_.host +
12671 ')');
12672 }
12673 return ref(db, parsedURL.path.toString());
12674}
12675/**
12676 * Gets a `Reference` for the location at the specified relative path.
12677 *
12678 * The relative path can either be a simple child name (for example, "ada") or
12679 * a deeper slash-separated path (for example, "ada/name/first").
12680 *
12681 * @param parent - The parent location.
12682 * @param path - A relative path from this location to the desired child
12683 * location.
12684 * @returns The specified child location.
12685 */
12686function child(parent, path) {
12687 parent = getModularInstance(parent);
12688 if (pathGetFront(parent._path) === null) {
12689 validateRootPathString('child', 'path', path, false);
12690 }
12691 else {
12692 validatePathString('child', 'path', path, false);
12693 }
12694 return new ReferenceImpl(parent._repo, pathChild(parent._path, path));
12695}
12696/**
12697 * Returns an `OnDisconnect` object - see
12698 * {@link https://firebase.google.com/docs/database/web/offline-capabilities | Enabling Offline Capabilities in JavaScript}
12699 * for more information on how to use it.
12700 *
12701 * @param ref - The reference to add OnDisconnect triggers for.
12702 */
12703function onDisconnect(ref) {
12704 ref = getModularInstance(ref);
12705 return new OnDisconnect(ref._repo, ref._path);
12706}
12707/**
12708 * Generates a new child location using a unique key and returns its
12709 * `Reference`.
12710 *
12711 * This is the most common pattern for adding data to a collection of items.
12712 *
12713 * If you provide a value to `push()`, the value is written to the
12714 * generated location. If you don't pass a value, nothing is written to the
12715 * database and the child remains empty (but you can use the `Reference`
12716 * elsewhere).
12717 *
12718 * The unique keys generated by `push()` are ordered by the current time, so the
12719 * resulting list of items is chronologically sorted. The keys are also
12720 * designed to be unguessable (they contain 72 random bits of entropy).
12721 *
12722 * See {@link https://firebase.google.com/docs/database/web/lists-of-data#append_to_a_list_of_data | Append to a list of data}
12723 * </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}
12724 *
12725 * @param parent - The parent location.
12726 * @param value - Optional value to be written at the generated location.
12727 * @returns Combined `Promise` and `Reference`; resolves when write is complete,
12728 * but can be used immediately as the `Reference` to the child location.
12729 */
12730function push(parent, value) {
12731 parent = getModularInstance(parent);
12732 validateWritablePath('push', parent._path);
12733 validateFirebaseDataArg('push', value, parent._path, true);
12734 const now = repoServerTime(parent._repo);
12735 const name = nextPushId(now);
12736 // push() returns a ThennableReference whose promise is fulfilled with a
12737 // regular Reference. We use child() to create handles to two different
12738 // references. The first is turned into a ThennableReference below by adding
12739 // then() and catch() methods and is used as the return value of push(). The
12740 // second remains a regular Reference and is used as the fulfilled value of
12741 // the first ThennableReference.
12742 const thennablePushRef = child(parent, name);
12743 const pushRef = child(parent, name);
12744 let promise;
12745 if (value != null) {
12746 promise = set(pushRef, value).then(() => pushRef);
12747 }
12748 else {
12749 promise = Promise.resolve(pushRef);
12750 }
12751 thennablePushRef.then = promise.then.bind(promise);
12752 thennablePushRef.catch = promise.then.bind(promise, undefined);
12753 return thennablePushRef;
12754}
12755/**
12756 * Removes the data at this Database location.
12757 *
12758 * Any data at child locations will also be deleted.
12759 *
12760 * The effect of the remove will be visible immediately and the corresponding
12761 * event 'value' will be triggered. Synchronization of the remove to the
12762 * Firebase servers will also be started, and the returned Promise will resolve
12763 * when complete. If provided, the onComplete callback will be called
12764 * asynchronously after synchronization has finished.
12765 *
12766 * @param ref - The location to remove.
12767 * @returns Resolves when remove on server is complete.
12768 */
12769function remove(ref) {
12770 validateWritablePath('remove', ref._path);
12771 return set(ref, null);
12772}
12773/**
12774 * Writes data to this Database location.
12775 *
12776 * This will overwrite any data at this location and all child locations.
12777 *
12778 * The effect of the write will be visible immediately, and the corresponding
12779 * events ("value", "child_added", etc.) will be triggered. Synchronization of
12780 * the data to the Firebase servers will also be started, and the returned
12781 * Promise will resolve when complete. If provided, the `onComplete` callback
12782 * will be called asynchronously after synchronization has finished.
12783 *
12784 * Passing `null` for the new value is equivalent to calling `remove()`; namely,
12785 * all data at this location and all child locations will be deleted.
12786 *
12787 * `set()` will remove any priority stored at this location, so if priority is
12788 * meant to be preserved, you need to use `setWithPriority()` instead.
12789 *
12790 * Note that modifying data with `set()` will cancel any pending transactions
12791 * at that location, so extreme care should be taken if mixing `set()` and
12792 * `transaction()` to modify the same data.
12793 *
12794 * A single `set()` will generate a single "value" event at the location where
12795 * the `set()` was performed.
12796 *
12797 * @param ref - The location to write to.
12798 * @param value - The value to be written (string, number, boolean, object,
12799 * array, or null).
12800 * @returns Resolves when write to server is complete.
12801 */
12802function set(ref, value) {
12803 ref = getModularInstance(ref);
12804 validateWritablePath('set', ref._path);
12805 validateFirebaseDataArg('set', value, ref._path, false);
12806 const deferred = new Deferred();
12807 repoSetWithPriority(ref._repo, ref._path, value,
12808 /*priority=*/ null, deferred.wrapCallback(() => { }));
12809 return deferred.promise;
12810}
12811/**
12812 * Sets a priority for the data at this Database location.
12813 *
12814 * Applications need not use priority but can order collections by
12815 * ordinary properties (see
12816 * {@link https://firebase.google.com/docs/database/web/lists-of-data#sorting_and_filtering_data | Sorting and filtering data}
12817 * ).
12818 *
12819 * @param ref - The location to write to.
12820 * @param priority - The priority to be written (string, number, or null).
12821 * @returns Resolves when write to server is complete.
12822 */
12823function setPriority(ref, priority) {
12824 ref = getModularInstance(ref);
12825 validateWritablePath('setPriority', ref._path);
12826 validatePriority('setPriority', priority, false);
12827 const deferred = new Deferred();
12828 repoSetWithPriority(ref._repo, pathChild(ref._path, '.priority'), priority, null, deferred.wrapCallback(() => { }));
12829 return deferred.promise;
12830}
12831/**
12832 * Writes data the Database location. Like `set()` but also specifies the
12833 * priority for that data.
12834 *
12835 * Applications need not use priority but can order collections by
12836 * ordinary properties (see
12837 * {@link https://firebase.google.com/docs/database/web/lists-of-data#sorting_and_filtering_data | Sorting and filtering data}
12838 * ).
12839 *
12840 * @param ref - The location to write to.
12841 * @param value - The value to be written (string, number, boolean, object,
12842 * array, or null).
12843 * @param priority - The priority to be written (string, number, or null).
12844 * @returns Resolves when write to server is complete.
12845 */
12846function setWithPriority(ref, value, priority) {
12847 validateWritablePath('setWithPriority', ref._path);
12848 validateFirebaseDataArg('setWithPriority', value, ref._path, false);
12849 validatePriority('setWithPriority', priority, false);
12850 if (ref.key === '.length' || ref.key === '.keys') {
12851 throw 'setWithPriority failed: ' + ref.key + ' is a read-only object.';
12852 }
12853 const deferred = new Deferred();
12854 repoSetWithPriority(ref._repo, ref._path, value, priority, deferred.wrapCallback(() => { }));
12855 return deferred.promise;
12856}
12857/**
12858 * Writes multiple values to the Database at once.
12859 *
12860 * The `values` argument contains multiple property-value pairs that will be
12861 * written to the Database together. Each child property can either be a simple
12862 * property (for example, "name") or a relative path (for example,
12863 * "name/first") from the current location to the data to update.
12864 *
12865 * As opposed to the `set()` method, `update()` can be use to selectively update
12866 * only the referenced properties at the current location (instead of replacing
12867 * all the child properties at the current location).
12868 *
12869 * The effect of the write will be visible immediately, and the corresponding
12870 * events ('value', 'child_added', etc.) will be triggered. Synchronization of
12871 * the data to the Firebase servers will also be started, and the returned
12872 * Promise will resolve when complete. If provided, the `onComplete` callback
12873 * will be called asynchronously after synchronization has finished.
12874 *
12875 * A single `update()` will generate a single "value" event at the location
12876 * where the `update()` was performed, regardless of how many children were
12877 * modified.
12878 *
12879 * Note that modifying data with `update()` will cancel any pending
12880 * transactions at that location, so extreme care should be taken if mixing
12881 * `update()` and `transaction()` to modify the same data.
12882 *
12883 * Passing `null` to `update()` will remove the data at this location.
12884 *
12885 * See
12886 * {@link https://firebase.googleblog.com/2015/09/introducing-multi-location-updates-and_86.html | Introducing multi-location updates and more}.
12887 *
12888 * @param ref - The location to write to.
12889 * @param values - Object containing multiple values.
12890 * @returns Resolves when update on server is complete.
12891 */
12892function update(ref, values) {
12893 validateFirebaseMergeDataArg('update', values, ref._path, false);
12894 const deferred = new Deferred();
12895 repoUpdate(ref._repo, ref._path, values, deferred.wrapCallback(() => { }));
12896 return deferred.promise;
12897}
12898/**
12899 * Gets the most up-to-date result for this query.
12900 *
12901 * @param query - The query to run.
12902 * @returns A `Promise` which resolves to the resulting DataSnapshot if a value is
12903 * available, or rejects if the client is unable to return a value (e.g., if the
12904 * server is unreachable and there is nothing cached).
12905 */
12906function get(query) {
12907 query = getModularInstance(query);
12908 return repoGetValue(query._repo, query).then(node => {
12909 return new DataSnapshot(node, new ReferenceImpl(query._repo, query._path), query._queryParams.getIndex());
12910 });
12911}
12912/**
12913 * Represents registration for 'value' events.
12914 */
12915class ValueEventRegistration {
12916 constructor(callbackContext) {
12917 this.callbackContext = callbackContext;
12918 }
12919 respondsTo(eventType) {
12920 return eventType === 'value';
12921 }
12922 createEvent(change, query) {
12923 const index = query._queryParams.getIndex();
12924 return new DataEvent('value', this, new DataSnapshot(change.snapshotNode, new ReferenceImpl(query._repo, query._path), index));
12925 }
12926 getEventRunner(eventData) {
12927 if (eventData.getEventType() === 'cancel') {
12928 return () => this.callbackContext.onCancel(eventData.error);
12929 }
12930 else {
12931 return () => this.callbackContext.onValue(eventData.snapshot, null);
12932 }
12933 }
12934 createCancelEvent(error, path) {
12935 if (this.callbackContext.hasCancelCallback) {
12936 return new CancelEvent(this, error, path);
12937 }
12938 else {
12939 return null;
12940 }
12941 }
12942 matches(other) {
12943 if (!(other instanceof ValueEventRegistration)) {
12944 return false;
12945 }
12946 else if (!other.callbackContext || !this.callbackContext) {
12947 // If no callback specified, we consider it to match any callback.
12948 return true;
12949 }
12950 else {
12951 return other.callbackContext.matches(this.callbackContext);
12952 }
12953 }
12954 hasAnyCallback() {
12955 return this.callbackContext !== null;
12956 }
12957}
12958/**
12959 * Represents the registration of a child_x event.
12960 */
12961class ChildEventRegistration {
12962 constructor(eventType, callbackContext) {
12963 this.eventType = eventType;
12964 this.callbackContext = callbackContext;
12965 }
12966 respondsTo(eventType) {
12967 let eventToCheck = eventType === 'children_added' ? 'child_added' : eventType;
12968 eventToCheck =
12969 eventToCheck === 'children_removed' ? 'child_removed' : eventToCheck;
12970 return this.eventType === eventToCheck;
12971 }
12972 createCancelEvent(error, path) {
12973 if (this.callbackContext.hasCancelCallback) {
12974 return new CancelEvent(this, error, path);
12975 }
12976 else {
12977 return null;
12978 }
12979 }
12980 createEvent(change, query) {
12981 assert(change.childName != null, 'Child events should have a childName.');
12982 const childRef = child(new ReferenceImpl(query._repo, query._path), change.childName);
12983 const index = query._queryParams.getIndex();
12984 return new DataEvent(change.type, this, new DataSnapshot(change.snapshotNode, childRef, index), change.prevName);
12985 }
12986 getEventRunner(eventData) {
12987 if (eventData.getEventType() === 'cancel') {
12988 return () => this.callbackContext.onCancel(eventData.error);
12989 }
12990 else {
12991 return () => this.callbackContext.onValue(eventData.snapshot, eventData.prevName);
12992 }
12993 }
12994 matches(other) {
12995 if (other instanceof ChildEventRegistration) {
12996 return (this.eventType === other.eventType &&
12997 (!this.callbackContext ||
12998 !other.callbackContext ||
12999 this.callbackContext.matches(other.callbackContext)));
13000 }
13001 return false;
13002 }
13003 hasAnyCallback() {
13004 return !!this.callbackContext;
13005 }
13006}
13007function addEventListener(query, eventType, callback, cancelCallbackOrListenOptions, options) {
13008 let cancelCallback;
13009 if (typeof cancelCallbackOrListenOptions === 'object') {
13010 cancelCallback = undefined;
13011 options = cancelCallbackOrListenOptions;
13012 }
13013 if (typeof cancelCallbackOrListenOptions === 'function') {
13014 cancelCallback = cancelCallbackOrListenOptions;
13015 }
13016 if (options && options.onlyOnce) {
13017 const userCallback = callback;
13018 const onceCallback = (dataSnapshot, previousChildName) => {
13019 repoRemoveEventCallbackForQuery(query._repo, query, container);
13020 userCallback(dataSnapshot, previousChildName);
13021 };
13022 onceCallback.userCallback = callback.userCallback;
13023 onceCallback.context = callback.context;
13024 callback = onceCallback;
13025 }
13026 const callbackContext = new CallbackContext(callback, cancelCallback || undefined);
13027 const container = eventType === 'value'
13028 ? new ValueEventRegistration(callbackContext)
13029 : new ChildEventRegistration(eventType, callbackContext);
13030 repoAddEventCallbackForQuery(query._repo, query, container);
13031 return () => repoRemoveEventCallbackForQuery(query._repo, query, container);
13032}
13033function onValue(query, callback, cancelCallbackOrListenOptions, options) {
13034 return addEventListener(query, 'value', callback, cancelCallbackOrListenOptions, options);
13035}
13036function onChildAdded(query, callback, cancelCallbackOrListenOptions, options) {
13037 return addEventListener(query, 'child_added', callback, cancelCallbackOrListenOptions, options);
13038}
13039function onChildChanged(query, callback, cancelCallbackOrListenOptions, options) {
13040 return addEventListener(query, 'child_changed', callback, cancelCallbackOrListenOptions, options);
13041}
13042function onChildMoved(query, callback, cancelCallbackOrListenOptions, options) {
13043 return addEventListener(query, 'child_moved', callback, cancelCallbackOrListenOptions, options);
13044}
13045function onChildRemoved(query, callback, cancelCallbackOrListenOptions, options) {
13046 return addEventListener(query, 'child_removed', callback, cancelCallbackOrListenOptions, options);
13047}
13048/**
13049 * Detaches a callback previously attached with the corresponding `on*()` (`onValue`, `onChildAdded`) listener.
13050 * Note: This is not the recommended way to remove a listener. Instead, please use the returned callback function from
13051 * the respective `on*` callbacks.
13052 *
13053 * Detach a callback previously attached with `on*()`. Calling `off()` on a parent listener
13054 * will not automatically remove listeners registered on child nodes, `off()`
13055 * must also be called on any child listeners to remove the callback.
13056 *
13057 * If a callback is not specified, all callbacks for the specified eventType
13058 * will be removed. Similarly, if no eventType is specified, all callbacks
13059 * for the `Reference` will be removed.
13060 *
13061 * Individual listeners can also be removed by invoking their unsubscribe
13062 * callbacks.
13063 *
13064 * @param query - The query that the listener was registered with.
13065 * @param eventType - One of the following strings: "value", "child_added",
13066 * "child_changed", "child_removed", or "child_moved." If omitted, all callbacks
13067 * for the `Reference` will be removed.
13068 * @param callback - The callback function that was passed to `on()` or
13069 * `undefined` to remove all callbacks.
13070 */
13071function off(query, eventType, callback) {
13072 let container = null;
13073 const expCallback = callback ? new CallbackContext(callback) : null;
13074 if (eventType === 'value') {
13075 container = new ValueEventRegistration(expCallback);
13076 }
13077 else if (eventType) {
13078 container = new ChildEventRegistration(eventType, expCallback);
13079 }
13080 repoRemoveEventCallbackForQuery(query._repo, query, container);
13081}
13082/**
13083 * A `QueryConstraint` is used to narrow the set of documents returned by a
13084 * Database query. `QueryConstraint`s are created by invoking {@link endAt},
13085 * {@link endBefore}, {@link startAt}, {@link startAfter}, {@link
13086 * limitToFirst}, {@link limitToLast}, {@link orderByChild},
13087 * {@link orderByChild}, {@link orderByKey} , {@link orderByPriority} ,
13088 * {@link orderByValue} or {@link equalTo} and
13089 * can then be passed to {@link query} to create a new query instance that
13090 * also contains this `QueryConstraint`.
13091 */
13092class QueryConstraint {
13093}
13094class QueryEndAtConstraint extends QueryConstraint {
13095 constructor(_value, _key) {
13096 super();
13097 this._value = _value;
13098 this._key = _key;
13099 }
13100 _apply(query) {
13101 validateFirebaseDataArg('endAt', this._value, query._path, true);
13102 const newParams = queryParamsEndAt(query._queryParams, this._value, this._key);
13103 validateLimit(newParams);
13104 validateQueryEndpoints(newParams);
13105 if (query._queryParams.hasEnd()) {
13106 throw new Error('endAt: Starting point was already set (by another call to endAt, ' +
13107 'endBefore or equalTo).');
13108 }
13109 return new QueryImpl(query._repo, query._path, newParams, query._orderByCalled);
13110 }
13111}
13112/**
13113 * Creates a `QueryConstraint` with the specified ending point.
13114 *
13115 * Using `startAt()`, `startAfter()`, `endBefore()`, `endAt()` and `equalTo()`
13116 * allows you to choose arbitrary starting and ending points for your queries.
13117 *
13118 * The ending point is inclusive, so children with exactly the specified value
13119 * will be included in the query. The optional key argument can be used to
13120 * further limit the range of the query. If it is specified, then children that
13121 * have exactly the specified value must also have a key name less than or equal
13122 * to the specified key.
13123 *
13124 * You can read more about `endAt()` in
13125 * {@link https://firebase.google.com/docs/database/web/lists-of-data#filtering_data | Filtering data}.
13126 *
13127 * @param value - The value to end at. The argument type depends on which
13128 * `orderBy*()` function was used in this query. Specify a value that matches
13129 * the `orderBy*()` type. When used in combination with `orderByKey()`, the
13130 * value must be a string.
13131 * @param key - The child key to end at, among the children with the previously
13132 * specified priority. This argument is only allowed if ordering by child,
13133 * value, or priority.
13134 */
13135function endAt(value, key) {
13136 validateKey('endAt', 'key', key, true);
13137 return new QueryEndAtConstraint(value, key);
13138}
13139class QueryEndBeforeConstraint extends QueryConstraint {
13140 constructor(_value, _key) {
13141 super();
13142 this._value = _value;
13143 this._key = _key;
13144 }
13145 _apply(query) {
13146 validateFirebaseDataArg('endBefore', this._value, query._path, false);
13147 const newParams = queryParamsEndBefore(query._queryParams, this._value, this._key);
13148 validateLimit(newParams);
13149 validateQueryEndpoints(newParams);
13150 if (query._queryParams.hasEnd()) {
13151 throw new Error('endBefore: Starting point was already set (by another call to endAt, ' +
13152 'endBefore or equalTo).');
13153 }
13154 return new QueryImpl(query._repo, query._path, newParams, query._orderByCalled);
13155 }
13156}
13157/**
13158 * Creates a `QueryConstraint` with the specified ending point (exclusive).
13159 *
13160 * Using `startAt()`, `startAfter()`, `endBefore()`, `endAt()` and `equalTo()`
13161 * allows you to choose arbitrary starting and ending points for your queries.
13162 *
13163 * The ending point is exclusive. If only a value is provided, children
13164 * with a value less than the specified value will be included in the query.
13165 * If a key is specified, then children must have a value lesss than or equal
13166 * to the specified value and a a key name less than the specified key.
13167 *
13168 * @param value - The value to end before. The argument type depends on which
13169 * `orderBy*()` function was used in this query. Specify a value that matches
13170 * the `orderBy*()` type. When used in combination with `orderByKey()`, the
13171 * value must be a string.
13172 * @param key - The child key to end before, among the children with the
13173 * previously specified priority. This argument is only allowed if ordering by
13174 * child, value, or priority.
13175 */
13176function endBefore(value, key) {
13177 validateKey('endBefore', 'key', key, true);
13178 return new QueryEndBeforeConstraint(value, key);
13179}
13180class QueryStartAtConstraint extends QueryConstraint {
13181 constructor(_value, _key) {
13182 super();
13183 this._value = _value;
13184 this._key = _key;
13185 }
13186 _apply(query) {
13187 validateFirebaseDataArg('startAt', this._value, query._path, true);
13188 const newParams = queryParamsStartAt(query._queryParams, this._value, this._key);
13189 validateLimit(newParams);
13190 validateQueryEndpoints(newParams);
13191 if (query._queryParams.hasStart()) {
13192 throw new Error('startAt: Starting point was already set (by another call to startAt, ' +
13193 'startBefore or equalTo).');
13194 }
13195 return new QueryImpl(query._repo, query._path, newParams, query._orderByCalled);
13196 }
13197}
13198/**
13199 * Creates a `QueryConstraint` with the specified starting point.
13200 *
13201 * Using `startAt()`, `startAfter()`, `endBefore()`, `endAt()` and `equalTo()`
13202 * allows you to choose arbitrary starting and ending points for your queries.
13203 *
13204 * The starting point is inclusive, so children with exactly the specified value
13205 * will be included in the query. The optional key argument can be used to
13206 * further limit the range of the query. If it is specified, then children that
13207 * have exactly the specified value must also have a key name greater than or
13208 * equal to the specified key.
13209 *
13210 * You can read more about `startAt()` in
13211 * {@link https://firebase.google.com/docs/database/web/lists-of-data#filtering_data | Filtering data}.
13212 *
13213 * @param value - The value to start at. The argument type depends on which
13214 * `orderBy*()` function was used in this query. Specify a value that matches
13215 * the `orderBy*()` type. When used in combination with `orderByKey()`, the
13216 * value must be a string.
13217 * @param key - The child key to start at. This argument is only allowed if
13218 * ordering by child, value, or priority.
13219 */
13220function startAt(value = null, key) {
13221 validateKey('startAt', 'key', key, true);
13222 return new QueryStartAtConstraint(value, key);
13223}
13224class QueryStartAfterConstraint extends QueryConstraint {
13225 constructor(_value, _key) {
13226 super();
13227 this._value = _value;
13228 this._key = _key;
13229 }
13230 _apply(query) {
13231 validateFirebaseDataArg('startAfter', this._value, query._path, false);
13232 const newParams = queryParamsStartAfter(query._queryParams, this._value, this._key);
13233 validateLimit(newParams);
13234 validateQueryEndpoints(newParams);
13235 if (query._queryParams.hasStart()) {
13236 throw new Error('startAfter: Starting point was already set (by another call to startAt, ' +
13237 'startAfter, or equalTo).');
13238 }
13239 return new QueryImpl(query._repo, query._path, newParams, query._orderByCalled);
13240 }
13241}
13242/**
13243 * Creates a `QueryConstraint` with the specified starting point (exclusive).
13244 *
13245 * Using `startAt()`, `startAfter()`, `endBefore()`, `endAt()` and `equalTo()`
13246 * allows you to choose arbitrary starting and ending points for your queries.
13247 *
13248 * The starting point is exclusive. If only a value is provided, children
13249 * with a value greater than the specified value will be included in the query.
13250 * If a key is specified, then children must have a value greater than or equal
13251 * to the specified value and a a key name greater than the specified key.
13252 *
13253 * @param value - The value to start after. The argument type depends on which
13254 * `orderBy*()` function was used in this query. Specify a value that matches
13255 * the `orderBy*()` type. When used in combination with `orderByKey()`, the
13256 * value must be a string.
13257 * @param key - The child key to start after. This argument is only allowed if
13258 * ordering by child, value, or priority.
13259 */
13260function startAfter(value, key) {
13261 validateKey('startAfter', 'key', key, true);
13262 return new QueryStartAfterConstraint(value, key);
13263}
13264class QueryLimitToFirstConstraint extends QueryConstraint {
13265 constructor(_limit) {
13266 super();
13267 this._limit = _limit;
13268 }
13269 _apply(query) {
13270 if (query._queryParams.hasLimit()) {
13271 throw new Error('limitToFirst: Limit was already set (by another call to limitToFirst ' +
13272 'or limitToLast).');
13273 }
13274 return new QueryImpl(query._repo, query._path, queryParamsLimitToFirst(query._queryParams, this._limit), query._orderByCalled);
13275 }
13276}
13277/**
13278 * Creates a new `QueryConstraint` that if limited to the first specific number
13279 * of children.
13280 *
13281 * The `limitToFirst()` method is used to set a maximum number of children to be
13282 * synced for a given callback. If we set a limit of 100, we will initially only
13283 * receive up to 100 `child_added` events. If we have fewer than 100 messages
13284 * stored in our Database, a `child_added` event will fire for each message.
13285 * However, if we have over 100 messages, we will only receive a `child_added`
13286 * event for the first 100 ordered messages. As items change, we will receive
13287 * `child_removed` events for each item that drops out of the active list so
13288 * that the total number stays at 100.
13289 *
13290 * You can read more about `limitToFirst()` in
13291 * {@link https://firebase.google.com/docs/database/web/lists-of-data#filtering_data | Filtering data}.
13292 *
13293 * @param limit - The maximum number of nodes to include in this query.
13294 */
13295function limitToFirst(limit) {
13296 if (typeof limit !== 'number' || Math.floor(limit) !== limit || limit <= 0) {
13297 throw new Error('limitToFirst: First argument must be a positive integer.');
13298 }
13299 return new QueryLimitToFirstConstraint(limit);
13300}
13301class QueryLimitToLastConstraint extends QueryConstraint {
13302 constructor(_limit) {
13303 super();
13304 this._limit = _limit;
13305 }
13306 _apply(query) {
13307 if (query._queryParams.hasLimit()) {
13308 throw new Error('limitToLast: Limit was already set (by another call to limitToFirst ' +
13309 'or limitToLast).');
13310 }
13311 return new QueryImpl(query._repo, query._path, queryParamsLimitToLast(query._queryParams, this._limit), query._orderByCalled);
13312 }
13313}
13314/**
13315 * Creates a new `QueryConstraint` that is limited to return only the last
13316 * specified number of children.
13317 *
13318 * The `limitToLast()` method is used to set a maximum number of children to be
13319 * synced for a given callback. If we set a limit of 100, we will initially only
13320 * receive up to 100 `child_added` events. If we have fewer than 100 messages
13321 * stored in our Database, a `child_added` event will fire for each message.
13322 * However, if we have over 100 messages, we will only receive a `child_added`
13323 * event for the last 100 ordered messages. As items change, we will receive
13324 * `child_removed` events for each item that drops out of the active list so
13325 * that the total number stays at 100.
13326 *
13327 * You can read more about `limitToLast()` in
13328 * {@link https://firebase.google.com/docs/database/web/lists-of-data#filtering_data | Filtering data}.
13329 *
13330 * @param limit - The maximum number of nodes to include in this query.
13331 */
13332function limitToLast(limit) {
13333 if (typeof limit !== 'number' || Math.floor(limit) !== limit || limit <= 0) {
13334 throw new Error('limitToLast: First argument must be a positive integer.');
13335 }
13336 return new QueryLimitToLastConstraint(limit);
13337}
13338class QueryOrderByChildConstraint extends QueryConstraint {
13339 constructor(_path) {
13340 super();
13341 this._path = _path;
13342 }
13343 _apply(query) {
13344 validateNoPreviousOrderByCall(query, 'orderByChild');
13345 const parsedPath = new Path(this._path);
13346 if (pathIsEmpty(parsedPath)) {
13347 throw new Error('orderByChild: cannot pass in empty path. Use orderByValue() instead.');
13348 }
13349 const index = new PathIndex(parsedPath);
13350 const newParams = queryParamsOrderBy(query._queryParams, index);
13351 validateQueryEndpoints(newParams);
13352 return new QueryImpl(query._repo, query._path, newParams,
13353 /*orderByCalled=*/ true);
13354 }
13355}
13356/**
13357 * Creates a new `QueryConstraint` that orders by the specified child key.
13358 *
13359 * Queries can only order by one key at a time. Calling `orderByChild()`
13360 * multiple times on the same query is an error.
13361 *
13362 * Firebase queries allow you to order your data by any child key on the fly.
13363 * However, if you know in advance what your indexes will be, you can define
13364 * them via the .indexOn rule in your Security Rules for better performance. See
13365 * the{@link https://firebase.google.com/docs/database/security/indexing-data}
13366 * rule for more information.
13367 *
13368 * You can read more about `orderByChild()` in
13369 * {@link https://firebase.google.com/docs/database/web/lists-of-data#sort_data | Sort data}.
13370 *
13371 * @param path - The path to order by.
13372 */
13373function orderByChild(path) {
13374 if (path === '$key') {
13375 throw new Error('orderByChild: "$key" is invalid. Use orderByKey() instead.');
13376 }
13377 else if (path === '$priority') {
13378 throw new Error('orderByChild: "$priority" is invalid. Use orderByPriority() instead.');
13379 }
13380 else if (path === '$value') {
13381 throw new Error('orderByChild: "$value" is invalid. Use orderByValue() instead.');
13382 }
13383 validatePathString('orderByChild', 'path', path, false);
13384 return new QueryOrderByChildConstraint(path);
13385}
13386class QueryOrderByKeyConstraint extends QueryConstraint {
13387 _apply(query) {
13388 validateNoPreviousOrderByCall(query, 'orderByKey');
13389 const newParams = queryParamsOrderBy(query._queryParams, KEY_INDEX);
13390 validateQueryEndpoints(newParams);
13391 return new QueryImpl(query._repo, query._path, newParams,
13392 /*orderByCalled=*/ true);
13393 }
13394}
13395/**
13396 * Creates a new `QueryConstraint` that orders by the key.
13397 *
13398 * Sorts the results of a query by their (ascending) key values.
13399 *
13400 * You can read more about `orderByKey()` in
13401 * {@link https://firebase.google.com/docs/database/web/lists-of-data#sort_data | Sort data}.
13402 */
13403function orderByKey() {
13404 return new QueryOrderByKeyConstraint();
13405}
13406class QueryOrderByPriorityConstraint extends QueryConstraint {
13407 _apply(query) {
13408 validateNoPreviousOrderByCall(query, 'orderByPriority');
13409 const newParams = queryParamsOrderBy(query._queryParams, PRIORITY_INDEX);
13410 validateQueryEndpoints(newParams);
13411 return new QueryImpl(query._repo, query._path, newParams,
13412 /*orderByCalled=*/ true);
13413 }
13414}
13415/**
13416 * Creates a new `QueryConstraint` that orders by priority.
13417 *
13418 * Applications need not use priority but can order collections by
13419 * ordinary properties (see
13420 * {@link https://firebase.google.com/docs/database/web/lists-of-data#sort_data | Sort data}
13421 * for alternatives to priority.
13422 */
13423function orderByPriority() {
13424 return new QueryOrderByPriorityConstraint();
13425}
13426class QueryOrderByValueConstraint extends QueryConstraint {
13427 _apply(query) {
13428 validateNoPreviousOrderByCall(query, 'orderByValue');
13429 const newParams = queryParamsOrderBy(query._queryParams, VALUE_INDEX);
13430 validateQueryEndpoints(newParams);
13431 return new QueryImpl(query._repo, query._path, newParams,
13432 /*orderByCalled=*/ true);
13433 }
13434}
13435/**
13436 * Creates a new `QueryConstraint` that orders by value.
13437 *
13438 * If the children of a query are all scalar values (string, number, or
13439 * boolean), you can order the results by their (ascending) values.
13440 *
13441 * You can read more about `orderByValue()` in
13442 * {@link https://firebase.google.com/docs/database/web/lists-of-data#sort_data | Sort data}.
13443 */
13444function orderByValue() {
13445 return new QueryOrderByValueConstraint();
13446}
13447class QueryEqualToValueConstraint extends QueryConstraint {
13448 constructor(_value, _key) {
13449 super();
13450 this._value = _value;
13451 this._key = _key;
13452 }
13453 _apply(query) {
13454 validateFirebaseDataArg('equalTo', this._value, query._path, false);
13455 if (query._queryParams.hasStart()) {
13456 throw new Error('equalTo: Starting point was already set (by another call to startAt/startAfter or ' +
13457 'equalTo).');
13458 }
13459 if (query._queryParams.hasEnd()) {
13460 throw new Error('equalTo: Ending point was already set (by another call to endAt/endBefore or ' +
13461 'equalTo).');
13462 }
13463 return new QueryEndAtConstraint(this._value, this._key)._apply(new QueryStartAtConstraint(this._value, this._key)._apply(query));
13464 }
13465}
13466/**
13467 * Creates a `QueryConstraint` that includes children that match the specified
13468 * value.
13469 *
13470 * Using `startAt()`, `startAfter()`, `endBefore()`, `endAt()` and `equalTo()`
13471 * allows you to choose arbitrary starting and ending points for your queries.
13472 *
13473 * The optional key argument can be used to further limit the range of the
13474 * query. If it is specified, then children that have exactly the specified
13475 * value must also have exactly the specified key as their key name. This can be
13476 * used to filter result sets with many matches for the same value.
13477 *
13478 * You can read more about `equalTo()` in
13479 * {@link https://firebase.google.com/docs/database/web/lists-of-data#filtering_data | Filtering data}.
13480 *
13481 * @param value - The value to match for. The argument type depends on which
13482 * `orderBy*()` function was used in this query. Specify a value that matches
13483 * the `orderBy*()` type. When used in combination with `orderByKey()`, the
13484 * value must be a string.
13485 * @param key - The child key to start at, among the children with the
13486 * previously specified priority. This argument is only allowed if ordering by
13487 * child, value, or priority.
13488 */
13489function equalTo(value, key) {
13490 validateKey('equalTo', 'key', key, true);
13491 return new QueryEqualToValueConstraint(value, key);
13492}
13493/**
13494 * Creates a new immutable instance of `Query` that is extended to also include
13495 * additional query constraints.
13496 *
13497 * @param query - The Query instance to use as a base for the new constraints.
13498 * @param queryConstraints - The list of `QueryConstraint`s to apply.
13499 * @throws if any of the provided query constraints cannot be combined with the
13500 * existing or new constraints.
13501 */
13502function query(query, ...queryConstraints) {
13503 let queryImpl = getModularInstance(query);
13504 for (const constraint of queryConstraints) {
13505 queryImpl = constraint._apply(queryImpl);
13506 }
13507 return queryImpl;
13508}
13509/**
13510 * Define reference constructor in various modules
13511 *
13512 * We are doing this here to avoid several circular
13513 * dependency issues
13514 */
13515syncPointSetReferenceConstructor(ReferenceImpl);
13516syncTreeSetReferenceConstructor(ReferenceImpl);
13517
13518/**
13519 * @license
13520 * Copyright 2020 Google LLC
13521 *
13522 * Licensed under the Apache License, Version 2.0 (the "License");
13523 * you may not use this file except in compliance with the License.
13524 * You may obtain a copy of the License at
13525 *
13526 * http://www.apache.org/licenses/LICENSE-2.0
13527 *
13528 * Unless required by applicable law or agreed to in writing, software
13529 * distributed under the License is distributed on an "AS IS" BASIS,
13530 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13531 * See the License for the specific language governing permissions and
13532 * limitations under the License.
13533 */
13534/**
13535 * This variable is also defined in the firebase Node.js Admin SDK. Before
13536 * modifying this definition, consult the definition in:
13537 *
13538 * https://github.com/firebase/firebase-admin-node
13539 *
13540 * and make sure the two are consistent.
13541 */
13542const FIREBASE_DATABASE_EMULATOR_HOST_VAR = 'FIREBASE_DATABASE_EMULATOR_HOST';
13543/**
13544 * Creates and caches `Repo` instances.
13545 */
13546const repos = {};
13547/**
13548 * If true, any new `Repo` will be created to use `ReadonlyRestClient` (for testing purposes).
13549 */
13550let useRestClient = false;
13551/**
13552 * Update an existing `Repo` in place to point to a new host/port.
13553 */
13554function repoManagerApplyEmulatorSettings(repo, host, port, tokenProvider) {
13555 repo.repoInfo_ = new RepoInfo(`${host}:${port}`,
13556 /* secure= */ false, repo.repoInfo_.namespace, repo.repoInfo_.webSocketOnly, repo.repoInfo_.nodeAdmin, repo.repoInfo_.persistenceKey, repo.repoInfo_.includeNamespaceInQueryParams);
13557 if (tokenProvider) {
13558 repo.authTokenProvider_ = tokenProvider;
13559 }
13560}
13561/**
13562 * This function should only ever be called to CREATE a new database instance.
13563 * @internal
13564 */
13565function repoManagerDatabaseFromApp(app, authProvider, appCheckProvider, url, nodeAdmin) {
13566 let dbUrl = url || app.options.databaseURL;
13567 if (dbUrl === undefined) {
13568 if (!app.options.projectId) {
13569 fatal("Can't determine Firebase Database URL. Be sure to include " +
13570 ' a Project ID when calling firebase.initializeApp().');
13571 }
13572 log('Using default host for project ', app.options.projectId);
13573 dbUrl = `${app.options.projectId}-default-rtdb.firebaseio.com`;
13574 }
13575 let parsedUrl = parseRepoInfo(dbUrl, nodeAdmin);
13576 let repoInfo = parsedUrl.repoInfo;
13577 let isEmulator;
13578 let dbEmulatorHost = undefined;
13579 if (typeof process !== 'undefined' && process.env) {
13580 dbEmulatorHost = process.env[FIREBASE_DATABASE_EMULATOR_HOST_VAR];
13581 }
13582 if (dbEmulatorHost) {
13583 isEmulator = true;
13584 dbUrl = `http://${dbEmulatorHost}?ns=${repoInfo.namespace}`;
13585 parsedUrl = parseRepoInfo(dbUrl, nodeAdmin);
13586 repoInfo = parsedUrl.repoInfo;
13587 }
13588 else {
13589 isEmulator = !parsedUrl.repoInfo.secure;
13590 }
13591 const authTokenProvider = nodeAdmin && isEmulator
13592 ? new EmulatorTokenProvider(EmulatorTokenProvider.OWNER)
13593 : new FirebaseAuthTokenProvider(app.name, app.options, authProvider);
13594 validateUrl('Invalid Firebase Database URL', parsedUrl);
13595 if (!pathIsEmpty(parsedUrl.path)) {
13596 fatal('Database URL must point to the root of a Firebase Database ' +
13597 '(not including a child path).');
13598 }
13599 const repo = repoManagerCreateRepo(repoInfo, app, authTokenProvider, new AppCheckTokenProvider(app.name, appCheckProvider));
13600 return new Database(repo, app);
13601}
13602/**
13603 * Remove the repo and make sure it is disconnected.
13604 *
13605 */
13606function repoManagerDeleteRepo(repo, appName) {
13607 const appRepos = repos[appName];
13608 // This should never happen...
13609 if (!appRepos || appRepos[repo.key] !== repo) {
13610 fatal(`Database ${appName}(${repo.repoInfo_}) has already been deleted.`);
13611 }
13612 repoInterrupt(repo);
13613 delete appRepos[repo.key];
13614}
13615/**
13616 * Ensures a repo doesn't already exist and then creates one using the
13617 * provided app.
13618 *
13619 * @param repoInfo - The metadata about the Repo
13620 * @returns The Repo object for the specified server / repoName.
13621 */
13622function repoManagerCreateRepo(repoInfo, app, authTokenProvider, appCheckProvider) {
13623 let appRepos = repos[app.name];
13624 if (!appRepos) {
13625 appRepos = {};
13626 repos[app.name] = appRepos;
13627 }
13628 let repo = appRepos[repoInfo.toURLString()];
13629 if (repo) {
13630 fatal('Database initialized multiple times. Please make sure the format of the database URL matches with each database() call.');
13631 }
13632 repo = new Repo(repoInfo, useRestClient, authTokenProvider, appCheckProvider);
13633 appRepos[repoInfo.toURLString()] = repo;
13634 return repo;
13635}
13636/**
13637 * Forces us to use ReadonlyRestClient instead of PersistentConnection for new Repos.
13638 */
13639function repoManagerForceRestClient(forceRestClient) {
13640 useRestClient = forceRestClient;
13641}
13642/**
13643 * Class representing a Firebase Realtime Database.
13644 */
13645class Database {
13646 /** @hideconstructor */
13647 constructor(_repoInternal,
13648 /** The {@link @firebase/app#FirebaseApp} associated with this Realtime Database instance. */
13649 app) {
13650 this._repoInternal = _repoInternal;
13651 this.app = app;
13652 /** Represents a `Database` instance. */
13653 this['type'] = 'database';
13654 /** Track if the instance has been used (root or repo accessed) */
13655 this._instanceStarted = false;
13656 }
13657 get _repo() {
13658 if (!this._instanceStarted) {
13659 repoStart(this._repoInternal, this.app.options.appId, this.app.options['databaseAuthVariableOverride']);
13660 this._instanceStarted = true;
13661 }
13662 return this._repoInternal;
13663 }
13664 get _root() {
13665 if (!this._rootInternal) {
13666 this._rootInternal = new ReferenceImpl(this._repo, newEmptyPath());
13667 }
13668 return this._rootInternal;
13669 }
13670 _delete() {
13671 if (this._rootInternal !== null) {
13672 repoManagerDeleteRepo(this._repo, this.app.name);
13673 this._repoInternal = null;
13674 this._rootInternal = null;
13675 }
13676 return Promise.resolve();
13677 }
13678 _checkNotDeleted(apiName) {
13679 if (this._rootInternal === null) {
13680 fatal('Cannot call ' + apiName + ' on a deleted database.');
13681 }
13682 }
13683}
13684function checkTransportInit() {
13685 if (TransportManager.IS_TRANSPORT_INITIALIZED) {
13686 warn('Transport has already been initialized. Please call this function before calling ref or setting up a listener');
13687 }
13688}
13689/**
13690 * Force the use of websockets instead of longPolling.
13691 */
13692function forceWebSockets() {
13693 checkTransportInit();
13694 BrowserPollConnection.forceDisallow();
13695}
13696/**
13697 * Force the use of longPolling instead of websockets. This will be ignored if websocket protocol is used in databaseURL.
13698 */
13699function forceLongPolling() {
13700 checkTransportInit();
13701 WebSocketConnection.forceDisallow();
13702 BrowserPollConnection.forceAllow();
13703}
13704/**
13705 * Returns the instance of the Realtime Database SDK that is associated
13706 * with the provided {@link @firebase/app#FirebaseApp}. Initializes a new instance with
13707 * with default settings if no instance exists or if the existing instance uses
13708 * a custom database URL.
13709 *
13710 * @param app - The {@link @firebase/app#FirebaseApp} instance that the returned Realtime
13711 * Database instance is associated with.
13712 * @param url - The URL of the Realtime Database instance to connect to. If not
13713 * provided, the SDK connects to the default instance of the Firebase App.
13714 * @returns The `Database` instance of the provided app.
13715 */
13716function getDatabase(app = getApp(), url) {
13717 return _getProvider(app, 'database').getImmediate({
13718 identifier: url
13719 });
13720}
13721/**
13722 * Modify the provided instance to communicate with the Realtime Database
13723 * emulator.
13724 *
13725 * <p>Note: This method must be called before performing any other operation.
13726 *
13727 * @param db - The instance to modify.
13728 * @param host - The emulator host (ex: localhost)
13729 * @param port - The emulator port (ex: 8080)
13730 * @param options.mockUserToken - the mock auth token to use for unit testing Security Rules
13731 */
13732function connectDatabaseEmulator(db, host, port, options = {}) {
13733 db = getModularInstance(db);
13734 db._checkNotDeleted('useEmulator');
13735 if (db._instanceStarted) {
13736 fatal('Cannot call useEmulator() after instance has already been initialized.');
13737 }
13738 const repo = db._repoInternal;
13739 let tokenProvider = undefined;
13740 if (repo.repoInfo_.nodeAdmin) {
13741 if (options.mockUserToken) {
13742 fatal('mockUserToken is not supported by the Admin SDK. For client access with mock users, please use the "firebase" package instead of "firebase-admin".');
13743 }
13744 tokenProvider = new EmulatorTokenProvider(EmulatorTokenProvider.OWNER);
13745 }
13746 else if (options.mockUserToken) {
13747 const token = typeof options.mockUserToken === 'string'
13748 ? options.mockUserToken
13749 : createMockUserToken(options.mockUserToken, db.app.options.projectId);
13750 tokenProvider = new EmulatorTokenProvider(token);
13751 }
13752 // Modify the repo to apply emulator settings
13753 repoManagerApplyEmulatorSettings(repo, host, port, tokenProvider);
13754}
13755/**
13756 * Disconnects from the server (all Database operations will be completed
13757 * offline).
13758 *
13759 * The client automatically maintains a persistent connection to the Database
13760 * server, which will remain active indefinitely and reconnect when
13761 * disconnected. However, the `goOffline()` and `goOnline()` methods may be used
13762 * to control the client connection in cases where a persistent connection is
13763 * undesirable.
13764 *
13765 * While offline, the client will no longer receive data updates from the
13766 * Database. However, all Database operations performed locally will continue to
13767 * immediately fire events, allowing your application to continue behaving
13768 * normally. Additionally, each operation performed locally will automatically
13769 * be queued and retried upon reconnection to the Database server.
13770 *
13771 * To reconnect to the Database and begin receiving remote events, see
13772 * `goOnline()`.
13773 *
13774 * @param db - The instance to disconnect.
13775 */
13776function goOffline(db) {
13777 db = getModularInstance(db);
13778 db._checkNotDeleted('goOffline');
13779 repoInterrupt(db._repo);
13780}
13781/**
13782 * Reconnects to the server and synchronizes the offline Database state
13783 * with the server state.
13784 *
13785 * This method should be used after disabling the active connection with
13786 * `goOffline()`. Once reconnected, the client will transmit the proper data
13787 * and fire the appropriate events so that your client "catches up"
13788 * automatically.
13789 *
13790 * @param db - The instance to reconnect.
13791 */
13792function goOnline(db) {
13793 db = getModularInstance(db);
13794 db._checkNotDeleted('goOnline');
13795 repoResume(db._repo);
13796}
13797function enableLogging(logger, persistent) {
13798 enableLogging$1(logger, persistent);
13799}
13800
13801/**
13802 * @license
13803 * Copyright 2021 Google LLC
13804 *
13805 * Licensed under the Apache License, Version 2.0 (the "License");
13806 * you may not use this file except in compliance with the License.
13807 * You may obtain a copy of the License at
13808 *
13809 * http://www.apache.org/licenses/LICENSE-2.0
13810 *
13811 * Unless required by applicable law or agreed to in writing, software
13812 * distributed under the License is distributed on an "AS IS" BASIS,
13813 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13814 * See the License for the specific language governing permissions and
13815 * limitations under the License.
13816 */
13817function registerDatabase(variant) {
13818 setSDKVersion(SDK_VERSION$1);
13819 _registerComponent(new Component('database', (container, { instanceIdentifier: url }) => {
13820 const app = container.getProvider('app').getImmediate();
13821 const authProvider = container.getProvider('auth-internal');
13822 const appCheckProvider = container.getProvider('app-check-internal');
13823 return repoManagerDatabaseFromApp(app, authProvider, appCheckProvider, url);
13824 }, "PUBLIC" /* PUBLIC */).setMultipleInstances(true));
13825 registerVersion(name, version, variant);
13826 // BUILD_TARGET will be replaced by values like esm5, esm2017, cjs5, etc during the compilation
13827 registerVersion(name, version, 'esm2017');
13828}
13829
13830/**
13831 * @license
13832 * Copyright 2020 Google LLC
13833 *
13834 * Licensed under the Apache License, Version 2.0 (the "License");
13835 * you may not use this file except in compliance with the License.
13836 * You may obtain a copy of the License at
13837 *
13838 * http://www.apache.org/licenses/LICENSE-2.0
13839 *
13840 * Unless required by applicable law or agreed to in writing, software
13841 * distributed under the License is distributed on an "AS IS" BASIS,
13842 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13843 * See the License for the specific language governing permissions and
13844 * limitations under the License.
13845 */
13846const SERVER_TIMESTAMP = {
13847 '.sv': 'timestamp'
13848};
13849/**
13850 * Returns a placeholder value for auto-populating the current timestamp (time
13851 * since the Unix epoch, in milliseconds) as determined by the Firebase
13852 * servers.
13853 */
13854function serverTimestamp() {
13855 return SERVER_TIMESTAMP;
13856}
13857/**
13858 * Returns a placeholder value that can be used to atomically increment the
13859 * current database value by the provided delta.
13860 *
13861 * @param delta - the amount to modify the current value atomically.
13862 * @returns A placeholder value for modifying data atomically server-side.
13863 */
13864function increment(delta) {
13865 return {
13866 '.sv': {
13867 'increment': delta
13868 }
13869 };
13870}
13871
13872/**
13873 * @license
13874 * Copyright 2020 Google LLC
13875 *
13876 * Licensed under the Apache License, Version 2.0 (the "License");
13877 * you may not use this file except in compliance with the License.
13878 * You may obtain a copy of the License at
13879 *
13880 * http://www.apache.org/licenses/LICENSE-2.0
13881 *
13882 * Unless required by applicable law or agreed to in writing, software
13883 * distributed under the License is distributed on an "AS IS" BASIS,
13884 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13885 * See the License for the specific language governing permissions and
13886 * limitations under the License.
13887 */
13888/**
13889 * A type for the resolve value of {@link runTransaction}.
13890 */
13891class TransactionResult {
13892 /** @hideconstructor */
13893 constructor(
13894 /** Whether the transaction was successfully committed. */
13895 committed,
13896 /** The resulting data snapshot. */
13897 snapshot) {
13898 this.committed = committed;
13899 this.snapshot = snapshot;
13900 }
13901 /** Returns a JSON-serializable representation of this object. */
13902 toJSON() {
13903 return { committed: this.committed, snapshot: this.snapshot.toJSON() };
13904 }
13905}
13906/**
13907 * Atomically modifies the data at this location.
13908 *
13909 * Atomically modify the data at this location. Unlike a normal `set()`, which
13910 * just overwrites the data regardless of its previous value, `runTransaction()` is
13911 * used to modify the existing value to a new value, ensuring there are no
13912 * conflicts with other clients writing to the same location at the same time.
13913 *
13914 * To accomplish this, you pass `runTransaction()` an update function which is
13915 * used to transform the current value into a new value. If another client
13916 * writes to the location before your new value is successfully written, your
13917 * update function will be called again with the new current value, and the
13918 * write will be retried. This will happen repeatedly until your write succeeds
13919 * without conflict or you abort the transaction by not returning a value from
13920 * your update function.
13921 *
13922 * Note: Modifying data with `set()` will cancel any pending transactions at
13923 * that location, so extreme care should be taken if mixing `set()` and
13924 * `runTransaction()` to update the same data.
13925 *
13926 * Note: When using transactions with Security and Firebase Rules in place, be
13927 * aware that a client needs `.read` access in addition to `.write` access in
13928 * order to perform a transaction. This is because the client-side nature of
13929 * transactions requires the client to read the data in order to transactionally
13930 * update it.
13931 *
13932 * @param ref - The location to atomically modify.
13933 * @param transactionUpdate - A developer-supplied function which will be passed
13934 * the current data stored at this location (as a JavaScript object). The
13935 * function should return the new value it would like written (as a JavaScript
13936 * object). If `undefined` is returned (i.e. you return with no arguments) the
13937 * transaction will be aborted and the data at this location will not be
13938 * modified.
13939 * @param options - An options object to configure transactions.
13940 * @returns A `Promise` that can optionally be used instead of the `onComplete`
13941 * callback to handle success and failure.
13942 */
13943function runTransaction(ref,
13944// eslint-disable-next-line @typescript-eslint/no-explicit-any
13945transactionUpdate, options) {
13946 var _a;
13947 ref = getModularInstance(ref);
13948 validateWritablePath('Reference.transaction', ref._path);
13949 if (ref.key === '.length' || ref.key === '.keys') {
13950 throw ('Reference.transaction failed: ' + ref.key + ' is a read-only object.');
13951 }
13952 const applyLocally = (_a = options === null || options === void 0 ? void 0 : options.applyLocally) !== null && _a !== void 0 ? _a : true;
13953 const deferred = new Deferred();
13954 const promiseComplete = (error, committed, node) => {
13955 let dataSnapshot = null;
13956 if (error) {
13957 deferred.reject(error);
13958 }
13959 else {
13960 dataSnapshot = new DataSnapshot(node, new ReferenceImpl(ref._repo, ref._path), PRIORITY_INDEX);
13961 deferred.resolve(new TransactionResult(committed, dataSnapshot));
13962 }
13963 };
13964 // Add a watch to make sure we get server updates.
13965 const unwatcher = onValue(ref, () => { });
13966 repoStartTransaction(ref._repo, ref._path, transactionUpdate, promiseComplete, unwatcher, applyLocally);
13967 return deferred.promise;
13968}
13969
13970/**
13971 * @license
13972 * Copyright 2017 Google LLC
13973 *
13974 * Licensed under the Apache License, Version 2.0 (the "License");
13975 * you may not use this file except in compliance with the License.
13976 * You may obtain a copy of the License at
13977 *
13978 * http://www.apache.org/licenses/LICENSE-2.0
13979 *
13980 * Unless required by applicable law or agreed to in writing, software
13981 * distributed under the License is distributed on an "AS IS" BASIS,
13982 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13983 * See the License for the specific language governing permissions and
13984 * limitations under the License.
13985 */
13986PersistentConnection;
13987// eslint-disable-next-line @typescript-eslint/no-explicit-any
13988PersistentConnection.prototype.simpleListen = function (pathString, onComplete) {
13989 this.sendRequest('q', { p: pathString }, onComplete);
13990};
13991// eslint-disable-next-line @typescript-eslint/no-explicit-any
13992PersistentConnection.prototype.echo = function (data, onEcho) {
13993 this.sendRequest('echo', { d: data }, onEcho);
13994};
13995// RealTimeConnection properties that we use in tests.
13996Connection;
13997/**
13998 * @internal
13999 */
14000const hijackHash = function (newHash) {
14001 const oldPut = PersistentConnection.prototype.put;
14002 PersistentConnection.prototype.put = function (pathString, data, onComplete, hash) {
14003 if (hash !== undefined) {
14004 hash = newHash();
14005 }
14006 oldPut.call(this, pathString, data, onComplete, hash);
14007 };
14008 return function () {
14009 PersistentConnection.prototype.put = oldPut;
14010 };
14011};
14012RepoInfo;
14013/**
14014 * Forces the RepoManager to create Repos that use ReadonlyRestClient instead of PersistentConnection.
14015 * @internal
14016 */
14017const forceRestClient = function (forceRestClient) {
14018 repoManagerForceRestClient(forceRestClient);
14019};
14020
14021/**
14022 * Firebase Realtime Database
14023 *
14024 * @packageDocumentation
14025 */
14026registerDatabase();
14027
14028export { 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 };
14029//# sourceMappingURL=index.esm2017.js.map