UNPKG

559 kBJavaScriptView Raw
1import { getApp, _getProvider, _registerComponent, registerVersion, SDK_VERSION as SDK_VERSION$1 } from '@firebase/app';
2import { Component } from '@firebase/component';
3import { stringify, jsonEval, contains, assert, base64, stringToByteArray, Sha1, isNodeSdk, 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.12.0";
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);
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) {
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 return repoInfoConnectionURL(repoInfo, WEBSOCKET, urlParams);
1835 }
1836 /**
1837 * @param onMessage - Callback when messages arrive
1838 * @param onDisconnect - Callback with connection lost.
1839 */
1840 open(onMessage, onDisconnect) {
1841 this.onDisconnect = onDisconnect;
1842 this.onMessage = onMessage;
1843 this.log_('Websocket connecting to ' + this.connURL);
1844 this.everConnected_ = false;
1845 // Assume failure until proven otherwise.
1846 PersistentStorage.set('previous_websocket_failure', true);
1847 try {
1848 if (isNodeSdk()) {
1849 const device = this.nodeAdmin ? 'AdminNode' : 'Node';
1850 // UA Format: Firebase/<wire_protocol>/<sdk_version>/<platform>/<device>
1851 const options = {
1852 headers: {
1853 'User-Agent': `Firebase/${PROTOCOL_VERSION}/${SDK_VERSION}/${process.platform}/${device}`,
1854 'X-Firebase-GMPID': this.applicationId || ''
1855 }
1856 };
1857 // If using Node with admin creds, AppCheck-related checks are unnecessary.
1858 // Note that we send the credentials here even if they aren't admin credentials, which is
1859 // not a problem.
1860 // Note that this header is just used to bypass appcheck, and the token should still be sent
1861 // through the websocket connection once it is established.
1862 if (this.authToken) {
1863 options.headers['Authorization'] = `Bearer ${this.authToken}`;
1864 }
1865 if (this.appCheckToken) {
1866 options.headers['X-Firebase-AppCheck'] = this.appCheckToken;
1867 }
1868 // Plumb appropriate http_proxy environment variable into faye-websocket if it exists.
1869 const env = process['env'];
1870 const proxy = this.connURL.indexOf('wss://') === 0
1871 ? env['HTTPS_PROXY'] || env['https_proxy']
1872 : env['HTTP_PROXY'] || env['http_proxy'];
1873 if (proxy) {
1874 options['proxy'] = { origin: proxy };
1875 }
1876 this.mySock = new WebSocketImpl(this.connURL, [], options);
1877 }
1878 else {
1879 const options = {
1880 headers: {
1881 'X-Firebase-GMPID': this.applicationId || '',
1882 'X-Firebase-AppCheck': this.appCheckToken || ''
1883 }
1884 };
1885 this.mySock = new WebSocketImpl(this.connURL, [], options);
1886 }
1887 }
1888 catch (e) {
1889 this.log_('Error instantiating WebSocket.');
1890 const error = e.message || e.data;
1891 if (error) {
1892 this.log_(error);
1893 }
1894 this.onClosed_();
1895 return;
1896 }
1897 this.mySock.onopen = () => {
1898 this.log_('Websocket connected.');
1899 this.everConnected_ = true;
1900 };
1901 this.mySock.onclose = () => {
1902 this.log_('Websocket connection was disconnected.');
1903 this.mySock = null;
1904 this.onClosed_();
1905 };
1906 this.mySock.onmessage = m => {
1907 this.handleIncomingFrame(m);
1908 };
1909 this.mySock.onerror = e => {
1910 this.log_('WebSocket error. Closing connection.');
1911 // eslint-disable-next-line @typescript-eslint/no-explicit-any
1912 const error = e.message || e.data;
1913 if (error) {
1914 this.log_(error);
1915 }
1916 this.onClosed_();
1917 };
1918 }
1919 /**
1920 * No-op for websockets, we don't need to do anything once the connection is confirmed as open
1921 */
1922 start() { }
1923 static forceDisallow() {
1924 WebSocketConnection.forceDisallow_ = true;
1925 }
1926 static isAvailable() {
1927 let isOldAndroid = false;
1928 if (typeof navigator !== 'undefined' && navigator.userAgent) {
1929 const oldAndroidRegex = /Android ([0-9]{0,}\.[0-9]{0,})/;
1930 const oldAndroidMatch = navigator.userAgent.match(oldAndroidRegex);
1931 if (oldAndroidMatch && oldAndroidMatch.length > 1) {
1932 if (parseFloat(oldAndroidMatch[1]) < 4.4) {
1933 isOldAndroid = true;
1934 }
1935 }
1936 }
1937 return (!isOldAndroid &&
1938 WebSocketImpl !== null &&
1939 !WebSocketConnection.forceDisallow_);
1940 }
1941 /**
1942 * Returns true if we previously failed to connect with this transport.
1943 */
1944 static previouslyFailed() {
1945 // If our persistent storage is actually only in-memory storage,
1946 // we default to assuming that it previously failed to be safe.
1947 return (PersistentStorage.isInMemoryStorage ||
1948 PersistentStorage.get('previous_websocket_failure') === true);
1949 }
1950 markConnectionHealthy() {
1951 PersistentStorage.remove('previous_websocket_failure');
1952 }
1953 appendFrame_(data) {
1954 this.frames.push(data);
1955 if (this.frames.length === this.totalFrames) {
1956 const fullMess = this.frames.join('');
1957 this.frames = null;
1958 const jsonMess = jsonEval(fullMess);
1959 //handle the message
1960 this.onMessage(jsonMess);
1961 }
1962 }
1963 /**
1964 * @param frameCount - The number of frames we are expecting from the server
1965 */
1966 handleNewFrameCount_(frameCount) {
1967 this.totalFrames = frameCount;
1968 this.frames = [];
1969 }
1970 /**
1971 * Attempts to parse a frame count out of some text. If it can't, assumes a value of 1
1972 * @returns Any remaining data to be process, or null if there is none
1973 */
1974 extractFrameCount_(data) {
1975 assert(this.frames === null, 'We already have a frame buffer');
1976 // TODO: The server is only supposed to send up to 9999 frames (i.e. length <= 4), but that isn't being enforced
1977 // currently. So allowing larger frame counts (length <= 6). See https://app.asana.com/0/search/8688598998380/8237608042508
1978 if (data.length <= 6) {
1979 const frameCount = Number(data);
1980 if (!isNaN(frameCount)) {
1981 this.handleNewFrameCount_(frameCount);
1982 return null;
1983 }
1984 }
1985 this.handleNewFrameCount_(1);
1986 return data;
1987 }
1988 /**
1989 * Process a websocket frame that has arrived from the server.
1990 * @param mess - The frame data
1991 */
1992 handleIncomingFrame(mess) {
1993 if (this.mySock === null) {
1994 return; // Chrome apparently delivers incoming packets even after we .close() the connection sometimes.
1995 }
1996 const data = mess['data'];
1997 this.bytesReceived += data.length;
1998 this.stats_.incrementCounter('bytes_received', data.length);
1999 this.resetKeepAlive();
2000 if (this.frames !== null) {
2001 // we're buffering
2002 this.appendFrame_(data);
2003 }
2004 else {
2005 // try to parse out a frame count, otherwise, assume 1 and process it
2006 const remainingData = this.extractFrameCount_(data);
2007 if (remainingData !== null) {
2008 this.appendFrame_(remainingData);
2009 }
2010 }
2011 }
2012 /**
2013 * Send a message to the server
2014 * @param data - The JSON object to transmit
2015 */
2016 send(data) {
2017 this.resetKeepAlive();
2018 const dataStr = stringify(data);
2019 this.bytesSent += dataStr.length;
2020 this.stats_.incrementCounter('bytes_sent', dataStr.length);
2021 //We can only fit a certain amount in each websocket frame, so we need to split this request
2022 //up into multiple pieces if it doesn't fit in one request.
2023 const dataSegs = splitStringBySize(dataStr, WEBSOCKET_MAX_FRAME_SIZE);
2024 //Send the length header
2025 if (dataSegs.length > 1) {
2026 this.sendString_(String(dataSegs.length));
2027 }
2028 //Send the actual data in segments.
2029 for (let i = 0; i < dataSegs.length; i++) {
2030 this.sendString_(dataSegs[i]);
2031 }
2032 }
2033 shutdown_() {
2034 this.isClosed_ = true;
2035 if (this.keepaliveTimer) {
2036 clearInterval(this.keepaliveTimer);
2037 this.keepaliveTimer = null;
2038 }
2039 if (this.mySock) {
2040 this.mySock.close();
2041 this.mySock = null;
2042 }
2043 }
2044 onClosed_() {
2045 if (!this.isClosed_) {
2046 this.log_('WebSocket is closing itself');
2047 this.shutdown_();
2048 // since this is an internal close, trigger the close listener
2049 if (this.onDisconnect) {
2050 this.onDisconnect(this.everConnected_);
2051 this.onDisconnect = null;
2052 }
2053 }
2054 }
2055 /**
2056 * External-facing close handler.
2057 * Close the websocket and kill the connection.
2058 */
2059 close() {
2060 if (!this.isClosed_) {
2061 this.log_('WebSocket is being closed');
2062 this.shutdown_();
2063 }
2064 }
2065 /**
2066 * Kill the current keepalive timer and start a new one, to ensure that it always fires N seconds after
2067 * the last activity.
2068 */
2069 resetKeepAlive() {
2070 clearInterval(this.keepaliveTimer);
2071 this.keepaliveTimer = setInterval(() => {
2072 //If there has been no websocket activity for a while, send a no-op
2073 if (this.mySock) {
2074 this.sendString_('0');
2075 }
2076 this.resetKeepAlive();
2077 // eslint-disable-next-line @typescript-eslint/no-explicit-any
2078 }, Math.floor(WEBSOCKET_KEEPALIVE_INTERVAL));
2079 }
2080 /**
2081 * Send a string over the websocket.
2082 *
2083 * @param str - String to send.
2084 */
2085 sendString_(str) {
2086 // Firefox seems to sometimes throw exceptions (NS_ERROR_UNEXPECTED) from websocket .send()
2087 // calls for some unknown reason. We treat these as an error and disconnect.
2088 // See https://app.asana.com/0/58926111402292/68021340250410
2089 try {
2090 this.mySock.send(str);
2091 }
2092 catch (e) {
2093 this.log_('Exception thrown from WebSocket.send():', e.message || e.data, 'Closing connection.');
2094 setTimeout(this.onClosed_.bind(this), 0);
2095 }
2096 }
2097}
2098/**
2099 * Number of response before we consider the connection "healthy."
2100 */
2101WebSocketConnection.responsesRequiredToBeHealthy = 2;
2102/**
2103 * Time to wait for the connection te become healthy before giving up.
2104 */
2105WebSocketConnection.healthyTimeout = 30000;
2106
2107/**
2108 * @license
2109 * Copyright 2017 Google LLC
2110 *
2111 * Licensed under the Apache License, Version 2.0 (the "License");
2112 * you may not use this file except in compliance with the License.
2113 * You may obtain a copy of the License at
2114 *
2115 * http://www.apache.org/licenses/LICENSE-2.0
2116 *
2117 * Unless required by applicable law or agreed to in writing, software
2118 * distributed under the License is distributed on an "AS IS" BASIS,
2119 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2120 * See the License for the specific language governing permissions and
2121 * limitations under the License.
2122 */
2123/**
2124 * Currently simplistic, this class manages what transport a Connection should use at various stages of its
2125 * lifecycle.
2126 *
2127 * It starts with longpolling in a browser, and httppolling on node. It then upgrades to websockets if
2128 * they are available.
2129 */
2130class TransportManager {
2131 /**
2132 * @param repoInfo - Metadata around the namespace we're connecting to
2133 */
2134 constructor(repoInfo) {
2135 this.initTransports_(repoInfo);
2136 }
2137 static get ALL_TRANSPORTS() {
2138 return [BrowserPollConnection, WebSocketConnection];
2139 }
2140 initTransports_(repoInfo) {
2141 const isWebSocketsAvailable = WebSocketConnection && WebSocketConnection['isAvailable']();
2142 let isSkipPollConnection = isWebSocketsAvailable && !WebSocketConnection.previouslyFailed();
2143 if (repoInfo.webSocketOnly) {
2144 if (!isWebSocketsAvailable) {
2145 warn("wss:// URL used, but browser isn't known to support websockets. Trying anyway.");
2146 }
2147 isSkipPollConnection = true;
2148 }
2149 if (isSkipPollConnection) {
2150 this.transports_ = [WebSocketConnection];
2151 }
2152 else {
2153 const transports = (this.transports_ = []);
2154 for (const transport of TransportManager.ALL_TRANSPORTS) {
2155 if (transport && transport['isAvailable']()) {
2156 transports.push(transport);
2157 }
2158 }
2159 }
2160 }
2161 /**
2162 * @returns The constructor for the initial transport to use
2163 */
2164 initialTransport() {
2165 if (this.transports_.length > 0) {
2166 return this.transports_[0];
2167 }
2168 else {
2169 throw new Error('No transports available');
2170 }
2171 }
2172 /**
2173 * @returns The constructor for the next transport, or null
2174 */
2175 upgradeTransport() {
2176 if (this.transports_.length > 1) {
2177 return this.transports_[1];
2178 }
2179 else {
2180 return null;
2181 }
2182 }
2183}
2184
2185/**
2186 * @license
2187 * Copyright 2017 Google LLC
2188 *
2189 * Licensed under the Apache License, Version 2.0 (the "License");
2190 * you may not use this file except in compliance with the License.
2191 * You may obtain a copy of the License at
2192 *
2193 * http://www.apache.org/licenses/LICENSE-2.0
2194 *
2195 * Unless required by applicable law or agreed to in writing, software
2196 * distributed under the License is distributed on an "AS IS" BASIS,
2197 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2198 * See the License for the specific language governing permissions and
2199 * limitations under the License.
2200 */
2201// Abort upgrade attempt if it takes longer than 60s.
2202const UPGRADE_TIMEOUT = 60000;
2203// For some transports (WebSockets), we need to "validate" the transport by exchanging a few requests and responses.
2204// If we haven't sent enough requests within 5s, we'll start sending noop ping requests.
2205const DELAY_BEFORE_SENDING_EXTRA_REQUESTS = 5000;
2206// 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)
2207// then we may not be able to exchange our ping/pong requests within the healthy timeout. So if we reach the timeout
2208// but we've sent/received enough bytes, we don't cancel the connection.
2209const BYTES_SENT_HEALTHY_OVERRIDE = 10 * 1024;
2210const BYTES_RECEIVED_HEALTHY_OVERRIDE = 100 * 1024;
2211const MESSAGE_TYPE = 't';
2212const MESSAGE_DATA = 'd';
2213const CONTROL_SHUTDOWN = 's';
2214const CONTROL_RESET = 'r';
2215const CONTROL_ERROR = 'e';
2216const CONTROL_PONG = 'o';
2217const SWITCH_ACK = 'a';
2218const END_TRANSMISSION = 'n';
2219const PING = 'p';
2220const SERVER_HELLO = 'h';
2221/**
2222 * Creates a new real-time connection to the server using whichever method works
2223 * best in the current browser.
2224 */
2225class Connection {
2226 /**
2227 * @param id - an id for this connection
2228 * @param repoInfo_ - the info for the endpoint to connect to
2229 * @param applicationId_ - the Firebase App ID for this project
2230 * @param appCheckToken_ - The App Check Token for this device.
2231 * @param authToken_ - The auth token for this session.
2232 * @param onMessage_ - the callback to be triggered when a server-push message arrives
2233 * @param onReady_ - the callback to be triggered when this connection is ready to send messages.
2234 * @param onDisconnect_ - the callback to be triggered when a connection was lost
2235 * @param onKill_ - the callback to be triggered when this connection has permanently shut down.
2236 * @param lastSessionId - last session id in persistent connection. is used to clean up old session in real-time server
2237 */
2238 constructor(id, repoInfo_, applicationId_, appCheckToken_, authToken_, onMessage_, onReady_, onDisconnect_, onKill_, lastSessionId) {
2239 this.id = id;
2240 this.repoInfo_ = repoInfo_;
2241 this.applicationId_ = applicationId_;
2242 this.appCheckToken_ = appCheckToken_;
2243 this.authToken_ = authToken_;
2244 this.onMessage_ = onMessage_;
2245 this.onReady_ = onReady_;
2246 this.onDisconnect_ = onDisconnect_;
2247 this.onKill_ = onKill_;
2248 this.lastSessionId = lastSessionId;
2249 this.connectionCount = 0;
2250 this.pendingDataMessages = [];
2251 this.state_ = 0 /* CONNECTING */;
2252 this.log_ = logWrapper('c:' + this.id + ':');
2253 this.transportManager_ = new TransportManager(repoInfo_);
2254 this.log_('Connection created');
2255 this.start_();
2256 }
2257 /**
2258 * Starts a connection attempt
2259 */
2260 start_() {
2261 const conn = this.transportManager_.initialTransport();
2262 this.conn_ = new conn(this.nextTransportId_(), this.repoInfo_, this.applicationId_, this.appCheckToken_, this.authToken_, null, this.lastSessionId);
2263 // For certain transports (WebSockets), we need to send and receive several messages back and forth before we
2264 // can consider the transport healthy.
2265 this.primaryResponsesRequired_ = conn['responsesRequiredToBeHealthy'] || 0;
2266 const onMessageReceived = this.connReceiver_(this.conn_);
2267 const onConnectionLost = this.disconnReceiver_(this.conn_);
2268 this.tx_ = this.conn_;
2269 this.rx_ = this.conn_;
2270 this.secondaryConn_ = null;
2271 this.isHealthy_ = false;
2272 /*
2273 * Firefox doesn't like when code from one iframe tries to create another iframe by way of the parent frame.
2274 * This can occur in the case of a redirect, i.e. we guessed wrong on what server to connect to and received a reset.
2275 * Somehow, setTimeout seems to make this ok. That doesn't make sense from a security perspective, since you should
2276 * still have the context of your originating frame.
2277 */
2278 setTimeout(() => {
2279 // this.conn_ gets set to null in some of the tests. Check to make sure it still exists before using it
2280 this.conn_ && this.conn_.open(onMessageReceived, onConnectionLost);
2281 }, Math.floor(0));
2282 const healthyTimeoutMS = conn['healthyTimeout'] || 0;
2283 if (healthyTimeoutMS > 0) {
2284 this.healthyTimeout_ = setTimeoutNonBlocking(() => {
2285 this.healthyTimeout_ = null;
2286 if (!this.isHealthy_) {
2287 if (this.conn_ &&
2288 this.conn_.bytesReceived > BYTES_RECEIVED_HEALTHY_OVERRIDE) {
2289 this.log_('Connection exceeded healthy timeout but has received ' +
2290 this.conn_.bytesReceived +
2291 ' bytes. Marking connection healthy.');
2292 this.isHealthy_ = true;
2293 this.conn_.markConnectionHealthy();
2294 }
2295 else if (this.conn_ &&
2296 this.conn_.bytesSent > BYTES_SENT_HEALTHY_OVERRIDE) {
2297 this.log_('Connection exceeded healthy timeout but has sent ' +
2298 this.conn_.bytesSent +
2299 ' bytes. Leaving connection alive.');
2300 // NOTE: We don't want to mark it healthy, since we have no guarantee that the bytes have made it to
2301 // the server.
2302 }
2303 else {
2304 this.log_('Closing unhealthy connection after timeout.');
2305 this.close();
2306 }
2307 }
2308 // eslint-disable-next-line @typescript-eslint/no-explicit-any
2309 }, Math.floor(healthyTimeoutMS));
2310 }
2311 }
2312 nextTransportId_() {
2313 return 'c:' + this.id + ':' + this.connectionCount++;
2314 }
2315 disconnReceiver_(conn) {
2316 return everConnected => {
2317 if (conn === this.conn_) {
2318 this.onConnectionLost_(everConnected);
2319 }
2320 else if (conn === this.secondaryConn_) {
2321 this.log_('Secondary connection lost.');
2322 this.onSecondaryConnectionLost_();
2323 }
2324 else {
2325 this.log_('closing an old connection');
2326 }
2327 };
2328 }
2329 connReceiver_(conn) {
2330 return (message) => {
2331 if (this.state_ !== 2 /* DISCONNECTED */) {
2332 if (conn === this.rx_) {
2333 this.onPrimaryMessageReceived_(message);
2334 }
2335 else if (conn === this.secondaryConn_) {
2336 this.onSecondaryMessageReceived_(message);
2337 }
2338 else {
2339 this.log_('message on old connection');
2340 }
2341 }
2342 };
2343 }
2344 /**
2345 * @param dataMsg - An arbitrary data message to be sent to the server
2346 */
2347 sendRequest(dataMsg) {
2348 // wrap in a data message envelope and send it on
2349 const msg = { t: 'd', d: dataMsg };
2350 this.sendData_(msg);
2351 }
2352 tryCleanupConnection() {
2353 if (this.tx_ === this.secondaryConn_ && this.rx_ === this.secondaryConn_) {
2354 this.log_('cleaning up and promoting a connection: ' + this.secondaryConn_.connId);
2355 this.conn_ = this.secondaryConn_;
2356 this.secondaryConn_ = null;
2357 // the server will shutdown the old connection
2358 }
2359 }
2360 onSecondaryControl_(controlData) {
2361 if (MESSAGE_TYPE in controlData) {
2362 const cmd = controlData[MESSAGE_TYPE];
2363 if (cmd === SWITCH_ACK) {
2364 this.upgradeIfSecondaryHealthy_();
2365 }
2366 else if (cmd === CONTROL_RESET) {
2367 // Most likely the session wasn't valid. Abandon the switch attempt
2368 this.log_('Got a reset on secondary, closing it');
2369 this.secondaryConn_.close();
2370 // If we were already using this connection for something, than we need to fully close
2371 if (this.tx_ === this.secondaryConn_ ||
2372 this.rx_ === this.secondaryConn_) {
2373 this.close();
2374 }
2375 }
2376 else if (cmd === CONTROL_PONG) {
2377 this.log_('got pong on secondary.');
2378 this.secondaryResponsesRequired_--;
2379 this.upgradeIfSecondaryHealthy_();
2380 }
2381 }
2382 }
2383 onSecondaryMessageReceived_(parsedData) {
2384 const layer = requireKey('t', parsedData);
2385 const data = requireKey('d', parsedData);
2386 if (layer === 'c') {
2387 this.onSecondaryControl_(data);
2388 }
2389 else if (layer === 'd') {
2390 // got a data message, but we're still second connection. Need to buffer it up
2391 this.pendingDataMessages.push(data);
2392 }
2393 else {
2394 throw new Error('Unknown protocol layer: ' + layer);
2395 }
2396 }
2397 upgradeIfSecondaryHealthy_() {
2398 if (this.secondaryResponsesRequired_ <= 0) {
2399 this.log_('Secondary connection is healthy.');
2400 this.isHealthy_ = true;
2401 this.secondaryConn_.markConnectionHealthy();
2402 this.proceedWithUpgrade_();
2403 }
2404 else {
2405 // Send a ping to make sure the connection is healthy.
2406 this.log_('sending ping on secondary.');
2407 this.secondaryConn_.send({ t: 'c', d: { t: PING, d: {} } });
2408 }
2409 }
2410 proceedWithUpgrade_() {
2411 // tell this connection to consider itself open
2412 this.secondaryConn_.start();
2413 // send ack
2414 this.log_('sending client ack on secondary');
2415 this.secondaryConn_.send({ t: 'c', d: { t: SWITCH_ACK, d: {} } });
2416 // send end packet on primary transport, switch to sending on this one
2417 // can receive on this one, buffer responses until end received on primary transport
2418 this.log_('Ending transmission on primary');
2419 this.conn_.send({ t: 'c', d: { t: END_TRANSMISSION, d: {} } });
2420 this.tx_ = this.secondaryConn_;
2421 this.tryCleanupConnection();
2422 }
2423 onPrimaryMessageReceived_(parsedData) {
2424 // Must refer to parsedData properties in quotes, so closure doesn't touch them.
2425 const layer = requireKey('t', parsedData);
2426 const data = requireKey('d', parsedData);
2427 if (layer === 'c') {
2428 this.onControl_(data);
2429 }
2430 else if (layer === 'd') {
2431 this.onDataMessage_(data);
2432 }
2433 }
2434 onDataMessage_(message) {
2435 this.onPrimaryResponse_();
2436 // We don't do anything with data messages, just kick them up a level
2437 this.onMessage_(message);
2438 }
2439 onPrimaryResponse_() {
2440 if (!this.isHealthy_) {
2441 this.primaryResponsesRequired_--;
2442 if (this.primaryResponsesRequired_ <= 0) {
2443 this.log_('Primary connection is healthy.');
2444 this.isHealthy_ = true;
2445 this.conn_.markConnectionHealthy();
2446 }
2447 }
2448 }
2449 onControl_(controlData) {
2450 const cmd = requireKey(MESSAGE_TYPE, controlData);
2451 if (MESSAGE_DATA in controlData) {
2452 const payload = controlData[MESSAGE_DATA];
2453 if (cmd === SERVER_HELLO) {
2454 this.onHandshake_(payload);
2455 }
2456 else if (cmd === END_TRANSMISSION) {
2457 this.log_('recvd end transmission on primary');
2458 this.rx_ = this.secondaryConn_;
2459 for (let i = 0; i < this.pendingDataMessages.length; ++i) {
2460 this.onDataMessage_(this.pendingDataMessages[i]);
2461 }
2462 this.pendingDataMessages = [];
2463 this.tryCleanupConnection();
2464 }
2465 else if (cmd === CONTROL_SHUTDOWN) {
2466 // This was previously the 'onKill' callback passed to the lower-level connection
2467 // payload in this case is the reason for the shutdown. Generally a human-readable error
2468 this.onConnectionShutdown_(payload);
2469 }
2470 else if (cmd === CONTROL_RESET) {
2471 // payload in this case is the host we should contact
2472 this.onReset_(payload);
2473 }
2474 else if (cmd === CONTROL_ERROR) {
2475 error('Server Error: ' + payload);
2476 }
2477 else if (cmd === CONTROL_PONG) {
2478 this.log_('got pong on primary.');
2479 this.onPrimaryResponse_();
2480 this.sendPingOnPrimaryIfNecessary_();
2481 }
2482 else {
2483 error('Unknown control packet command: ' + cmd);
2484 }
2485 }
2486 }
2487 /**
2488 * @param handshake - The handshake data returned from the server
2489 */
2490 onHandshake_(handshake) {
2491 const timestamp = handshake.ts;
2492 const version = handshake.v;
2493 const host = handshake.h;
2494 this.sessionId = handshake.s;
2495 this.repoInfo_.host = host;
2496 // if we've already closed the connection, then don't bother trying to progress further
2497 if (this.state_ === 0 /* CONNECTING */) {
2498 this.conn_.start();
2499 this.onConnectionEstablished_(this.conn_, timestamp);
2500 if (PROTOCOL_VERSION !== version) {
2501 warn('Protocol version mismatch detected');
2502 }
2503 // TODO: do we want to upgrade? when? maybe a delay?
2504 this.tryStartUpgrade_();
2505 }
2506 }
2507 tryStartUpgrade_() {
2508 const conn = this.transportManager_.upgradeTransport();
2509 if (conn) {
2510 this.startUpgrade_(conn);
2511 }
2512 }
2513 startUpgrade_(conn) {
2514 this.secondaryConn_ = new conn(this.nextTransportId_(), this.repoInfo_, this.applicationId_, this.appCheckToken_, this.authToken_, this.sessionId);
2515 // For certain transports (WebSockets), we need to send and receive several messages back and forth before we
2516 // can consider the transport healthy.
2517 this.secondaryResponsesRequired_ =
2518 conn['responsesRequiredToBeHealthy'] || 0;
2519 const onMessage = this.connReceiver_(this.secondaryConn_);
2520 const onDisconnect = this.disconnReceiver_(this.secondaryConn_);
2521 this.secondaryConn_.open(onMessage, onDisconnect);
2522 // If we haven't successfully upgraded after UPGRADE_TIMEOUT, give up and kill the secondary.
2523 setTimeoutNonBlocking(() => {
2524 if (this.secondaryConn_) {
2525 this.log_('Timed out trying to upgrade.');
2526 this.secondaryConn_.close();
2527 }
2528 }, Math.floor(UPGRADE_TIMEOUT));
2529 }
2530 onReset_(host) {
2531 this.log_('Reset packet received. New host: ' + host);
2532 this.repoInfo_.host = host;
2533 // TODO: if we're already "connected", we need to trigger a disconnect at the next layer up.
2534 // We don't currently support resets after the connection has already been established
2535 if (this.state_ === 1 /* CONNECTED */) {
2536 this.close();
2537 }
2538 else {
2539 // Close whatever connections we have open and start again.
2540 this.closeConnections_();
2541 this.start_();
2542 }
2543 }
2544 onConnectionEstablished_(conn, timestamp) {
2545 this.log_('Realtime connection established.');
2546 this.conn_ = conn;
2547 this.state_ = 1 /* CONNECTED */;
2548 if (this.onReady_) {
2549 this.onReady_(timestamp, this.sessionId);
2550 this.onReady_ = null;
2551 }
2552 // If after 5 seconds we haven't sent enough requests to the server to get the connection healthy,
2553 // send some pings.
2554 if (this.primaryResponsesRequired_ === 0) {
2555 this.log_('Primary connection is healthy.');
2556 this.isHealthy_ = true;
2557 }
2558 else {
2559 setTimeoutNonBlocking(() => {
2560 this.sendPingOnPrimaryIfNecessary_();
2561 }, Math.floor(DELAY_BEFORE_SENDING_EXTRA_REQUESTS));
2562 }
2563 }
2564 sendPingOnPrimaryIfNecessary_() {
2565 // If the connection isn't considered healthy yet, we'll send a noop ping packet request.
2566 if (!this.isHealthy_ && this.state_ === 1 /* CONNECTED */) {
2567 this.log_('sending ping on primary.');
2568 this.sendData_({ t: 'c', d: { t: PING, d: {} } });
2569 }
2570 }
2571 onSecondaryConnectionLost_() {
2572 const conn = this.secondaryConn_;
2573 this.secondaryConn_ = null;
2574 if (this.tx_ === conn || this.rx_ === conn) {
2575 // we are relying on this connection already in some capacity. Therefore, a failure is real
2576 this.close();
2577 }
2578 }
2579 /**
2580 * @param everConnected - Whether or not the connection ever reached a server. Used to determine if
2581 * we should flush the host cache
2582 */
2583 onConnectionLost_(everConnected) {
2584 this.conn_ = null;
2585 // NOTE: IF you're seeing a Firefox error for this line, I think it might be because it's getting
2586 // called on window close and RealtimeState.CONNECTING is no longer defined. Just a guess.
2587 if (!everConnected && this.state_ === 0 /* CONNECTING */) {
2588 this.log_('Realtime connection failed.');
2589 // Since we failed to connect at all, clear any cached entry for this namespace in case the machine went away
2590 if (this.repoInfo_.isCacheableHost()) {
2591 PersistentStorage.remove('host:' + this.repoInfo_.host);
2592 // reset the internal host to what we would show the user, i.e. <ns>.firebaseio.com
2593 this.repoInfo_.internalHost = this.repoInfo_.host;
2594 }
2595 }
2596 else if (this.state_ === 1 /* CONNECTED */) {
2597 this.log_('Realtime connection lost.');
2598 }
2599 this.close();
2600 }
2601 onConnectionShutdown_(reason) {
2602 this.log_('Connection shutdown command received. Shutting down...');
2603 if (this.onKill_) {
2604 this.onKill_(reason);
2605 this.onKill_ = null;
2606 }
2607 // We intentionally don't want to fire onDisconnect (kill is a different case),
2608 // so clear the callback.
2609 this.onDisconnect_ = null;
2610 this.close();
2611 }
2612 sendData_(data) {
2613 if (this.state_ !== 1 /* CONNECTED */) {
2614 throw 'Connection is not connected';
2615 }
2616 else {
2617 this.tx_.send(data);
2618 }
2619 }
2620 /**
2621 * Cleans up this connection, calling the appropriate callbacks
2622 */
2623 close() {
2624 if (this.state_ !== 2 /* DISCONNECTED */) {
2625 this.log_('Closing realtime connection.');
2626 this.state_ = 2 /* DISCONNECTED */;
2627 this.closeConnections_();
2628 if (this.onDisconnect_) {
2629 this.onDisconnect_();
2630 this.onDisconnect_ = null;
2631 }
2632 }
2633 }
2634 closeConnections_() {
2635 this.log_('Shutting down all connections');
2636 if (this.conn_) {
2637 this.conn_.close();
2638 this.conn_ = null;
2639 }
2640 if (this.secondaryConn_) {
2641 this.secondaryConn_.close();
2642 this.secondaryConn_ = null;
2643 }
2644 if (this.healthyTimeout_) {
2645 clearTimeout(this.healthyTimeout_);
2646 this.healthyTimeout_ = null;
2647 }
2648 }
2649}
2650
2651/**
2652 * @license
2653 * Copyright 2017 Google LLC
2654 *
2655 * Licensed under the Apache License, Version 2.0 (the "License");
2656 * you may not use this file except in compliance with the License.
2657 * You may obtain a copy of the License at
2658 *
2659 * http://www.apache.org/licenses/LICENSE-2.0
2660 *
2661 * Unless required by applicable law or agreed to in writing, software
2662 * distributed under the License is distributed on an "AS IS" BASIS,
2663 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2664 * See the License for the specific language governing permissions and
2665 * limitations under the License.
2666 */
2667/**
2668 * Interface defining the set of actions that can be performed against the Firebase server
2669 * (basically corresponds to our wire protocol).
2670 *
2671 * @interface
2672 */
2673class ServerActions {
2674 put(pathString, data, onComplete, hash) { }
2675 merge(pathString, data, onComplete, hash) { }
2676 /**
2677 * Refreshes the auth token for the current connection.
2678 * @param token - The authentication token
2679 */
2680 refreshAuthToken(token) { }
2681 /**
2682 * Refreshes the app check token for the current connection.
2683 * @param token The app check token
2684 */
2685 refreshAppCheckToken(token) { }
2686 onDisconnectPut(pathString, data, onComplete) { }
2687 onDisconnectMerge(pathString, data, onComplete) { }
2688 onDisconnectCancel(pathString, onComplete) { }
2689 reportStats(stats) { }
2690}
2691
2692/**
2693 * @license
2694 * Copyright 2017 Google LLC
2695 *
2696 * Licensed under the Apache License, Version 2.0 (the "License");
2697 * you may not use this file except in compliance with the License.
2698 * You may obtain a copy of the License at
2699 *
2700 * http://www.apache.org/licenses/LICENSE-2.0
2701 *
2702 * Unless required by applicable law or agreed to in writing, software
2703 * distributed under the License is distributed on an "AS IS" BASIS,
2704 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2705 * See the License for the specific language governing permissions and
2706 * limitations under the License.
2707 */
2708/**
2709 * Base class to be used if you want to emit events. Call the constructor with
2710 * the set of allowed event names.
2711 */
2712class EventEmitter {
2713 constructor(allowedEvents_) {
2714 this.allowedEvents_ = allowedEvents_;
2715 this.listeners_ = {};
2716 assert(Array.isArray(allowedEvents_) && allowedEvents_.length > 0, 'Requires a non-empty array');
2717 }
2718 /**
2719 * To be called by derived classes to trigger events.
2720 */
2721 trigger(eventType, ...varArgs) {
2722 if (Array.isArray(this.listeners_[eventType])) {
2723 // Clone the list, since callbacks could add/remove listeners.
2724 const listeners = [...this.listeners_[eventType]];
2725 for (let i = 0; i < listeners.length; i++) {
2726 listeners[i].callback.apply(listeners[i].context, varArgs);
2727 }
2728 }
2729 }
2730 on(eventType, callback, context) {
2731 this.validateEventType_(eventType);
2732 this.listeners_[eventType] = this.listeners_[eventType] || [];
2733 this.listeners_[eventType].push({ callback, context });
2734 const eventData = this.getInitialEvent(eventType);
2735 if (eventData) {
2736 callback.apply(context, eventData);
2737 }
2738 }
2739 off(eventType, callback, context) {
2740 this.validateEventType_(eventType);
2741 const listeners = this.listeners_[eventType] || [];
2742 for (let i = 0; i < listeners.length; i++) {
2743 if (listeners[i].callback === callback &&
2744 (!context || context === listeners[i].context)) {
2745 listeners.splice(i, 1);
2746 return;
2747 }
2748 }
2749 }
2750 validateEventType_(eventType) {
2751 assert(this.allowedEvents_.find(et => {
2752 return et === eventType;
2753 }), 'Unknown event: ' + eventType);
2754 }
2755}
2756
2757/**
2758 * @license
2759 * Copyright 2017 Google LLC
2760 *
2761 * Licensed under the Apache License, Version 2.0 (the "License");
2762 * you may not use this file except in compliance with the License.
2763 * You may obtain a copy of the License at
2764 *
2765 * http://www.apache.org/licenses/LICENSE-2.0
2766 *
2767 * Unless required by applicable law or agreed to in writing, software
2768 * distributed under the License is distributed on an "AS IS" BASIS,
2769 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2770 * See the License for the specific language governing permissions and
2771 * limitations under the License.
2772 */
2773/**
2774 * Monitors online state (as reported by window.online/offline events).
2775 *
2776 * The expectation is that this could have many false positives (thinks we are online
2777 * when we're not), but no false negatives. So we can safely use it to determine when
2778 * we definitely cannot reach the internet.
2779 */
2780class OnlineMonitor extends EventEmitter {
2781 constructor() {
2782 super(['online']);
2783 this.online_ = true;
2784 // We've had repeated complaints that Cordova apps can get stuck "offline", e.g.
2785 // https://forum.ionicframework.com/t/firebase-connection-is-lost-and-never-come-back/43810
2786 // It would seem that the 'online' event does not always fire consistently. So we disable it
2787 // for Cordova.
2788 if (typeof window !== 'undefined' &&
2789 typeof window.addEventListener !== 'undefined' &&
2790 !isMobileCordova()) {
2791 window.addEventListener('online', () => {
2792 if (!this.online_) {
2793 this.online_ = true;
2794 this.trigger('online', true);
2795 }
2796 }, false);
2797 window.addEventListener('offline', () => {
2798 if (this.online_) {
2799 this.online_ = false;
2800 this.trigger('online', false);
2801 }
2802 }, false);
2803 }
2804 }
2805 static getInstance() {
2806 return new OnlineMonitor();
2807 }
2808 getInitialEvent(eventType) {
2809 assert(eventType === 'online', 'Unknown event type: ' + eventType);
2810 return [this.online_];
2811 }
2812 currentlyOnline() {
2813 return this.online_;
2814 }
2815}
2816
2817/**
2818 * @license
2819 * Copyright 2017 Google LLC
2820 *
2821 * Licensed under the Apache License, Version 2.0 (the "License");
2822 * you may not use this file except in compliance with the License.
2823 * You may obtain a copy of the License at
2824 *
2825 * http://www.apache.org/licenses/LICENSE-2.0
2826 *
2827 * Unless required by applicable law or agreed to in writing, software
2828 * distributed under the License is distributed on an "AS IS" BASIS,
2829 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2830 * See the License for the specific language governing permissions and
2831 * limitations under the License.
2832 */
2833/** Maximum key depth. */
2834const MAX_PATH_DEPTH = 32;
2835/** Maximum number of (UTF8) bytes in a Firebase path. */
2836const MAX_PATH_LENGTH_BYTES = 768;
2837/**
2838 * An immutable object representing a parsed path. It's immutable so that you
2839 * can pass them around to other functions without worrying about them changing
2840 * it.
2841 */
2842class Path {
2843 /**
2844 * @param pathOrString - Path string to parse, or another path, or the raw
2845 * tokens array
2846 */
2847 constructor(pathOrString, pieceNum) {
2848 if (pieceNum === void 0) {
2849 this.pieces_ = pathOrString.split('/');
2850 // Remove empty pieces.
2851 let copyTo = 0;
2852 for (let i = 0; i < this.pieces_.length; i++) {
2853 if (this.pieces_[i].length > 0) {
2854 this.pieces_[copyTo] = this.pieces_[i];
2855 copyTo++;
2856 }
2857 }
2858 this.pieces_.length = copyTo;
2859 this.pieceNum_ = 0;
2860 }
2861 else {
2862 this.pieces_ = pathOrString;
2863 this.pieceNum_ = pieceNum;
2864 }
2865 }
2866 toString() {
2867 let pathString = '';
2868 for (let i = this.pieceNum_; i < this.pieces_.length; i++) {
2869 if (this.pieces_[i] !== '') {
2870 pathString += '/' + this.pieces_[i];
2871 }
2872 }
2873 return pathString || '/';
2874 }
2875}
2876function newEmptyPath() {
2877 return new Path('');
2878}
2879function pathGetFront(path) {
2880 if (path.pieceNum_ >= path.pieces_.length) {
2881 return null;
2882 }
2883 return path.pieces_[path.pieceNum_];
2884}
2885/**
2886 * @returns The number of segments in this path
2887 */
2888function pathGetLength(path) {
2889 return path.pieces_.length - path.pieceNum_;
2890}
2891function pathPopFront(path) {
2892 let pieceNum = path.pieceNum_;
2893 if (pieceNum < path.pieces_.length) {
2894 pieceNum++;
2895 }
2896 return new Path(path.pieces_, pieceNum);
2897}
2898function pathGetBack(path) {
2899 if (path.pieceNum_ < path.pieces_.length) {
2900 return path.pieces_[path.pieces_.length - 1];
2901 }
2902 return null;
2903}
2904function pathToUrlEncodedString(path) {
2905 let pathString = '';
2906 for (let i = path.pieceNum_; i < path.pieces_.length; i++) {
2907 if (path.pieces_[i] !== '') {
2908 pathString += '/' + encodeURIComponent(String(path.pieces_[i]));
2909 }
2910 }
2911 return pathString || '/';
2912}
2913/**
2914 * Shallow copy of the parts of the path.
2915 *
2916 */
2917function pathSlice(path, begin = 0) {
2918 return path.pieces_.slice(path.pieceNum_ + begin);
2919}
2920function pathParent(path) {
2921 if (path.pieceNum_ >= path.pieces_.length) {
2922 return null;
2923 }
2924 const pieces = [];
2925 for (let i = path.pieceNum_; i < path.pieces_.length - 1; i++) {
2926 pieces.push(path.pieces_[i]);
2927 }
2928 return new Path(pieces, 0);
2929}
2930function pathChild(path, childPathObj) {
2931 const pieces = [];
2932 for (let i = path.pieceNum_; i < path.pieces_.length; i++) {
2933 pieces.push(path.pieces_[i]);
2934 }
2935 if (childPathObj instanceof Path) {
2936 for (let i = childPathObj.pieceNum_; i < childPathObj.pieces_.length; i++) {
2937 pieces.push(childPathObj.pieces_[i]);
2938 }
2939 }
2940 else {
2941 const childPieces = childPathObj.split('/');
2942 for (let i = 0; i < childPieces.length; i++) {
2943 if (childPieces[i].length > 0) {
2944 pieces.push(childPieces[i]);
2945 }
2946 }
2947 }
2948 return new Path(pieces, 0);
2949}
2950/**
2951 * @returns True if there are no segments in this path
2952 */
2953function pathIsEmpty(path) {
2954 return path.pieceNum_ >= path.pieces_.length;
2955}
2956/**
2957 * @returns The path from outerPath to innerPath
2958 */
2959function newRelativePath(outerPath, innerPath) {
2960 const outer = pathGetFront(outerPath), inner = pathGetFront(innerPath);
2961 if (outer === null) {
2962 return innerPath;
2963 }
2964 else if (outer === inner) {
2965 return newRelativePath(pathPopFront(outerPath), pathPopFront(innerPath));
2966 }
2967 else {
2968 throw new Error('INTERNAL ERROR: innerPath (' +
2969 innerPath +
2970 ') is not within ' +
2971 'outerPath (' +
2972 outerPath +
2973 ')');
2974 }
2975}
2976/**
2977 * @returns -1, 0, 1 if left is less, equal, or greater than the right.
2978 */
2979function pathCompare(left, right) {
2980 const leftKeys = pathSlice(left, 0);
2981 const rightKeys = pathSlice(right, 0);
2982 for (let i = 0; i < leftKeys.length && i < rightKeys.length; i++) {
2983 const cmp = nameCompare(leftKeys[i], rightKeys[i]);
2984 if (cmp !== 0) {
2985 return cmp;
2986 }
2987 }
2988 if (leftKeys.length === rightKeys.length) {
2989 return 0;
2990 }
2991 return leftKeys.length < rightKeys.length ? -1 : 1;
2992}
2993/**
2994 * @returns true if paths are the same.
2995 */
2996function pathEquals(path, other) {
2997 if (pathGetLength(path) !== pathGetLength(other)) {
2998 return false;
2999 }
3000 for (let i = path.pieceNum_, j = other.pieceNum_; i <= path.pieces_.length; i++, j++) {
3001 if (path.pieces_[i] !== other.pieces_[j]) {
3002 return false;
3003 }
3004 }
3005 return true;
3006}
3007/**
3008 * @returns True if this path is a parent (or the same as) other
3009 */
3010function pathContains(path, other) {
3011 let i = path.pieceNum_;
3012 let j = other.pieceNum_;
3013 if (pathGetLength(path) > pathGetLength(other)) {
3014 return false;
3015 }
3016 while (i < path.pieces_.length) {
3017 if (path.pieces_[i] !== other.pieces_[j]) {
3018 return false;
3019 }
3020 ++i;
3021 ++j;
3022 }
3023 return true;
3024}
3025/**
3026 * Dynamic (mutable) path used to count path lengths.
3027 *
3028 * This class is used to efficiently check paths for valid
3029 * length (in UTF8 bytes) and depth (used in path validation).
3030 *
3031 * Throws Error exception if path is ever invalid.
3032 *
3033 * The definition of a path always begins with '/'.
3034 */
3035class ValidationPath {
3036 /**
3037 * @param path - Initial Path.
3038 * @param errorPrefix_ - Prefix for any error messages.
3039 */
3040 constructor(path, errorPrefix_) {
3041 this.errorPrefix_ = errorPrefix_;
3042 this.parts_ = pathSlice(path, 0);
3043 /** Initialize to number of '/' chars needed in path. */
3044 this.byteLength_ = Math.max(1, this.parts_.length);
3045 for (let i = 0; i < this.parts_.length; i++) {
3046 this.byteLength_ += stringLength(this.parts_[i]);
3047 }
3048 validationPathCheckValid(this);
3049 }
3050}
3051function validationPathPush(validationPath, child) {
3052 // Count the needed '/'
3053 if (validationPath.parts_.length > 0) {
3054 validationPath.byteLength_ += 1;
3055 }
3056 validationPath.parts_.push(child);
3057 validationPath.byteLength_ += stringLength(child);
3058 validationPathCheckValid(validationPath);
3059}
3060function validationPathPop(validationPath) {
3061 const last = validationPath.parts_.pop();
3062 validationPath.byteLength_ -= stringLength(last);
3063 // Un-count the previous '/'
3064 if (validationPath.parts_.length > 0) {
3065 validationPath.byteLength_ -= 1;
3066 }
3067}
3068function validationPathCheckValid(validationPath) {
3069 if (validationPath.byteLength_ > MAX_PATH_LENGTH_BYTES) {
3070 throw new Error(validationPath.errorPrefix_ +
3071 'has a key path longer than ' +
3072 MAX_PATH_LENGTH_BYTES +
3073 ' bytes (' +
3074 validationPath.byteLength_ +
3075 ').');
3076 }
3077 if (validationPath.parts_.length > MAX_PATH_DEPTH) {
3078 throw new Error(validationPath.errorPrefix_ +
3079 'path specified exceeds the maximum depth that can be written (' +
3080 MAX_PATH_DEPTH +
3081 ') or object contains a cycle ' +
3082 validationPathToErrorString(validationPath));
3083 }
3084}
3085/**
3086 * String for use in error messages - uses '.' notation for path.
3087 */
3088function validationPathToErrorString(validationPath) {
3089 if (validationPath.parts_.length === 0) {
3090 return '';
3091 }
3092 return "in property '" + validationPath.parts_.join('.') + "'";
3093}
3094
3095/**
3096 * @license
3097 * Copyright 2017 Google LLC
3098 *
3099 * Licensed under the Apache License, Version 2.0 (the "License");
3100 * you may not use this file except in compliance with the License.
3101 * You may obtain a copy of the License at
3102 *
3103 * http://www.apache.org/licenses/LICENSE-2.0
3104 *
3105 * Unless required by applicable law or agreed to in writing, software
3106 * distributed under the License is distributed on an "AS IS" BASIS,
3107 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3108 * See the License for the specific language governing permissions and
3109 * limitations under the License.
3110 */
3111class VisibilityMonitor extends EventEmitter {
3112 constructor() {
3113 super(['visible']);
3114 let hidden;
3115 let visibilityChange;
3116 if (typeof document !== 'undefined' &&
3117 typeof document.addEventListener !== 'undefined') {
3118 if (typeof document['hidden'] !== 'undefined') {
3119 // Opera 12.10 and Firefox 18 and later support
3120 visibilityChange = 'visibilitychange';
3121 hidden = 'hidden';
3122 }
3123 else if (typeof document['mozHidden'] !== 'undefined') {
3124 visibilityChange = 'mozvisibilitychange';
3125 hidden = 'mozHidden';
3126 }
3127 else if (typeof document['msHidden'] !== 'undefined') {
3128 visibilityChange = 'msvisibilitychange';
3129 hidden = 'msHidden';
3130 }
3131 else if (typeof document['webkitHidden'] !== 'undefined') {
3132 visibilityChange = 'webkitvisibilitychange';
3133 hidden = 'webkitHidden';
3134 }
3135 }
3136 // Initially, we always assume we are visible. This ensures that in browsers
3137 // without page visibility support or in cases where we are never visible
3138 // (e.g. chrome extension), we act as if we are visible, i.e. don't delay
3139 // reconnects
3140 this.visible_ = true;
3141 if (visibilityChange) {
3142 document.addEventListener(visibilityChange, () => {
3143 const visible = !document[hidden];
3144 if (visible !== this.visible_) {
3145 this.visible_ = visible;
3146 this.trigger('visible', visible);
3147 }
3148 }, false);
3149 }
3150 }
3151 static getInstance() {
3152 return new VisibilityMonitor();
3153 }
3154 getInitialEvent(eventType) {
3155 assert(eventType === 'visible', 'Unknown event type: ' + eventType);
3156 return [this.visible_];
3157 }
3158}
3159
3160/**
3161 * @license
3162 * Copyright 2017 Google LLC
3163 *
3164 * Licensed under the Apache License, Version 2.0 (the "License");
3165 * you may not use this file except in compliance with the License.
3166 * You may obtain a copy of the License at
3167 *
3168 * http://www.apache.org/licenses/LICENSE-2.0
3169 *
3170 * Unless required by applicable law or agreed to in writing, software
3171 * distributed under the License is distributed on an "AS IS" BASIS,
3172 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3173 * See the License for the specific language governing permissions and
3174 * limitations under the License.
3175 */
3176const RECONNECT_MIN_DELAY = 1000;
3177const RECONNECT_MAX_DELAY_DEFAULT = 60 * 5 * 1000; // 5 minutes in milliseconds (Case: 1858)
3178const GET_CONNECT_TIMEOUT = 3 * 1000;
3179const RECONNECT_MAX_DELAY_FOR_ADMINS = 30 * 1000; // 30 seconds for admin clients (likely to be a backend server)
3180const RECONNECT_DELAY_MULTIPLIER = 1.3;
3181const RECONNECT_DELAY_RESET_TIMEOUT = 30000; // Reset delay back to MIN_DELAY after being connected for 30sec.
3182const SERVER_KILL_INTERRUPT_REASON = 'server_kill';
3183// If auth fails repeatedly, we'll assume something is wrong and log a warning / back off.
3184const INVALID_TOKEN_THRESHOLD = 3;
3185/**
3186 * Firebase connection. Abstracts wire protocol and handles reconnecting.
3187 *
3188 * NOTE: All JSON objects sent to the realtime connection must have property names enclosed
3189 * in quotes to make sure the closure compiler does not minify them.
3190 */
3191class PersistentConnection extends ServerActions {
3192 /**
3193 * @param repoInfo_ - Data about the namespace we are connecting to
3194 * @param applicationId_ - The Firebase App ID for this project
3195 * @param onDataUpdate_ - A callback for new data from the server
3196 */
3197 constructor(repoInfo_, applicationId_, onDataUpdate_, onConnectStatus_, onServerInfoUpdate_, authTokenProvider_, appCheckTokenProvider_, authOverride_) {
3198 super();
3199 this.repoInfo_ = repoInfo_;
3200 this.applicationId_ = applicationId_;
3201 this.onDataUpdate_ = onDataUpdate_;
3202 this.onConnectStatus_ = onConnectStatus_;
3203 this.onServerInfoUpdate_ = onServerInfoUpdate_;
3204 this.authTokenProvider_ = authTokenProvider_;
3205 this.appCheckTokenProvider_ = appCheckTokenProvider_;
3206 this.authOverride_ = authOverride_;
3207 // Used for diagnostic logging.
3208 this.id = PersistentConnection.nextPersistentConnectionId_++;
3209 this.log_ = logWrapper('p:' + this.id + ':');
3210 this.interruptReasons_ = {};
3211 this.listens = new Map();
3212 this.outstandingPuts_ = [];
3213 this.outstandingGets_ = [];
3214 this.outstandingPutCount_ = 0;
3215 this.outstandingGetCount_ = 0;
3216 this.onDisconnectRequestQueue_ = [];
3217 this.connected_ = false;
3218 this.reconnectDelay_ = RECONNECT_MIN_DELAY;
3219 this.maxReconnectDelay_ = RECONNECT_MAX_DELAY_DEFAULT;
3220 this.securityDebugCallback_ = null;
3221 this.lastSessionId = null;
3222 this.establishConnectionTimer_ = null;
3223 this.visible_ = false;
3224 // Before we get connected, we keep a queue of pending messages to send.
3225 this.requestCBHash_ = {};
3226 this.requestNumber_ = 0;
3227 this.realtime_ = null;
3228 this.authToken_ = null;
3229 this.appCheckToken_ = null;
3230 this.forceTokenRefresh_ = false;
3231 this.invalidAuthTokenCount_ = 0;
3232 this.invalidAppCheckTokenCount_ = 0;
3233 this.firstConnection_ = true;
3234 this.lastConnectionAttemptTime_ = null;
3235 this.lastConnectionEstablishedTime_ = null;
3236 if (authOverride_ && !isNodeSdk()) {
3237 throw new Error('Auth override specified in options, but not supported on non Node.js platforms');
3238 }
3239 VisibilityMonitor.getInstance().on('visible', this.onVisible_, this);
3240 if (repoInfo_.host.indexOf('fblocal') === -1) {
3241 OnlineMonitor.getInstance().on('online', this.onOnline_, this);
3242 }
3243 }
3244 sendRequest(action, body, onResponse) {
3245 const curReqNum = ++this.requestNumber_;
3246 const msg = { r: curReqNum, a: action, b: body };
3247 this.log_(stringify(msg));
3248 assert(this.connected_, "sendRequest call when we're not connected not allowed.");
3249 this.realtime_.sendRequest(msg);
3250 if (onResponse) {
3251 this.requestCBHash_[curReqNum] = onResponse;
3252 }
3253 }
3254 get(query) {
3255 this.initConnection_();
3256 const deferred = new Deferred();
3257 const request = {
3258 p: query._path.toString(),
3259 q: query._queryObject
3260 };
3261 const outstandingGet = {
3262 action: 'g',
3263 request,
3264 onComplete: (message) => {
3265 const payload = message['d'];
3266 if (message['s'] === 'ok') {
3267 this.onDataUpdate_(request['p'], payload,
3268 /*isMerge*/ false,
3269 /*tag*/ null);
3270 deferred.resolve(payload);
3271 }
3272 else {
3273 deferred.reject(payload);
3274 }
3275 }
3276 };
3277 this.outstandingGets_.push(outstandingGet);
3278 this.outstandingGetCount_++;
3279 const index = this.outstandingGets_.length - 1;
3280 if (!this.connected_) {
3281 setTimeout(() => {
3282 const get = this.outstandingGets_[index];
3283 if (get === undefined || outstandingGet !== get) {
3284 return;
3285 }
3286 delete this.outstandingGets_[index];
3287 this.outstandingGetCount_--;
3288 if (this.outstandingGetCount_ === 0) {
3289 this.outstandingGets_ = [];
3290 }
3291 this.log_('get ' + index + ' timed out on connection');
3292 deferred.reject(new Error('Client is offline.'));
3293 }, GET_CONNECT_TIMEOUT);
3294 }
3295 if (this.connected_) {
3296 this.sendGet_(index);
3297 }
3298 return deferred.promise;
3299 }
3300 listen(query, currentHashFn, tag, onComplete) {
3301 this.initConnection_();
3302 const queryId = query._queryIdentifier;
3303 const pathString = query._path.toString();
3304 this.log_('Listen called for ' + pathString + ' ' + queryId);
3305 if (!this.listens.has(pathString)) {
3306 this.listens.set(pathString, new Map());
3307 }
3308 assert(query._queryParams.isDefault() || !query._queryParams.loadsAllData(), 'listen() called for non-default but complete query');
3309 assert(!this.listens.get(pathString).has(queryId), 'listen() called twice for same path/queryId.');
3310 const listenSpec = {
3311 onComplete,
3312 hashFn: currentHashFn,
3313 query,
3314 tag
3315 };
3316 this.listens.get(pathString).set(queryId, listenSpec);
3317 if (this.connected_) {
3318 this.sendListen_(listenSpec);
3319 }
3320 }
3321 sendGet_(index) {
3322 const get = this.outstandingGets_[index];
3323 this.sendRequest('g', get.request, (message) => {
3324 delete this.outstandingGets_[index];
3325 this.outstandingGetCount_--;
3326 if (this.outstandingGetCount_ === 0) {
3327 this.outstandingGets_ = [];
3328 }
3329 if (get.onComplete) {
3330 get.onComplete(message);
3331 }
3332 });
3333 }
3334 sendListen_(listenSpec) {
3335 const query = listenSpec.query;
3336 const pathString = query._path.toString();
3337 const queryId = query._queryIdentifier;
3338 this.log_('Listen on ' + pathString + ' for ' + queryId);
3339 const req = { /*path*/ p: pathString };
3340 const action = 'q';
3341 // Only bother to send query if it's non-default.
3342 if (listenSpec.tag) {
3343 req['q'] = query._queryObject;
3344 req['t'] = listenSpec.tag;
3345 }
3346 req[ /*hash*/'h'] = listenSpec.hashFn();
3347 this.sendRequest(action, req, (message) => {
3348 const payload = message[ /*data*/'d'];
3349 const status = message[ /*status*/'s'];
3350 // print warnings in any case...
3351 PersistentConnection.warnOnListenWarnings_(payload, query);
3352 const currentListenSpec = this.listens.get(pathString) &&
3353 this.listens.get(pathString).get(queryId);
3354 // only trigger actions if the listen hasn't been removed and readded
3355 if (currentListenSpec === listenSpec) {
3356 this.log_('listen response', message);
3357 if (status !== 'ok') {
3358 this.removeListen_(pathString, queryId);
3359 }
3360 if (listenSpec.onComplete) {
3361 listenSpec.onComplete(status, payload);
3362 }
3363 }
3364 });
3365 }
3366 static warnOnListenWarnings_(payload, query) {
3367 if (payload && typeof payload === 'object' && contains(payload, 'w')) {
3368 // eslint-disable-next-line @typescript-eslint/no-explicit-any
3369 const warnings = safeGet(payload, 'w');
3370 if (Array.isArray(warnings) && ~warnings.indexOf('no_index')) {
3371 const indexSpec = '".indexOn": "' + query._queryParams.getIndex().toString() + '"';
3372 const indexPath = query._path.toString();
3373 warn(`Using an unspecified index. Your data will be downloaded and ` +
3374 `filtered on the client. Consider adding ${indexSpec} at ` +
3375 `${indexPath} to your security rules for better performance.`);
3376 }
3377 }
3378 }
3379 refreshAuthToken(token) {
3380 this.authToken_ = token;
3381 this.log_('Auth token refreshed');
3382 if (this.authToken_) {
3383 this.tryAuth();
3384 }
3385 else {
3386 //If we're connected we want to let the server know to unauthenticate us. If we're not connected, simply delete
3387 //the credential so we dont become authenticated next time we connect.
3388 if (this.connected_) {
3389 this.sendRequest('unauth', {}, () => { });
3390 }
3391 }
3392 this.reduceReconnectDelayIfAdminCredential_(token);
3393 }
3394 reduceReconnectDelayIfAdminCredential_(credential) {
3395 // NOTE: This isn't intended to be bulletproof (a malicious developer can always just modify the client).
3396 // Additionally, we don't bother resetting the max delay back to the default if auth fails / expires.
3397 const isFirebaseSecret = credential && credential.length === 40;
3398 if (isFirebaseSecret || isAdmin(credential)) {
3399 this.log_('Admin auth credential detected. Reducing max reconnect time.');
3400 this.maxReconnectDelay_ = RECONNECT_MAX_DELAY_FOR_ADMINS;
3401 }
3402 }
3403 refreshAppCheckToken(token) {
3404 this.appCheckToken_ = token;
3405 this.log_('App check token refreshed');
3406 if (this.appCheckToken_) {
3407 this.tryAppCheck();
3408 }
3409 else {
3410 //If we're connected we want to let the server know to unauthenticate us.
3411 //If we're not connected, simply delete the credential so we dont become
3412 // authenticated next time we connect.
3413 if (this.connected_) {
3414 this.sendRequest('unappeck', {}, () => { });
3415 }
3416 }
3417 }
3418 /**
3419 * Attempts to authenticate with the given credentials. If the authentication attempt fails, it's triggered like
3420 * a auth revoked (the connection is closed).
3421 */
3422 tryAuth() {
3423 if (this.connected_ && this.authToken_) {
3424 const token = this.authToken_;
3425 const authMethod = isValidFormat(token) ? 'auth' : 'gauth';
3426 const requestData = { cred: token };
3427 if (this.authOverride_ === null) {
3428 requestData['noauth'] = true;
3429 }
3430 else if (typeof this.authOverride_ === 'object') {
3431 requestData['authvar'] = this.authOverride_;
3432 }
3433 this.sendRequest(authMethod, requestData, (res) => {
3434 const status = res[ /*status*/'s'];
3435 const data = res[ /*data*/'d'] || 'error';
3436 if (this.authToken_ === token) {
3437 if (status === 'ok') {
3438 this.invalidAuthTokenCount_ = 0;
3439 }
3440 else {
3441 // Triggers reconnect and force refresh for auth token
3442 this.onAuthRevoked_(status, data);
3443 }
3444 }
3445 });
3446 }
3447 }
3448 /**
3449 * Attempts to authenticate with the given token. If the authentication
3450 * attempt fails, it's triggered like the token was revoked (the connection is
3451 * closed).
3452 */
3453 tryAppCheck() {
3454 if (this.connected_ && this.appCheckToken_) {
3455 this.sendRequest('appcheck', { 'token': this.appCheckToken_ }, (res) => {
3456 const status = res[ /*status*/'s'];
3457 const data = res[ /*data*/'d'] || 'error';
3458 if (status === 'ok') {
3459 this.invalidAppCheckTokenCount_ = 0;
3460 }
3461 else {
3462 this.onAppCheckRevoked_(status, data);
3463 }
3464 });
3465 }
3466 }
3467 /**
3468 * @inheritDoc
3469 */
3470 unlisten(query, tag) {
3471 const pathString = query._path.toString();
3472 const queryId = query._queryIdentifier;
3473 this.log_('Unlisten called for ' + pathString + ' ' + queryId);
3474 assert(query._queryParams.isDefault() || !query._queryParams.loadsAllData(), 'unlisten() called for non-default but complete query');
3475 const listen = this.removeListen_(pathString, queryId);
3476 if (listen && this.connected_) {
3477 this.sendUnlisten_(pathString, queryId, query._queryObject, tag);
3478 }
3479 }
3480 sendUnlisten_(pathString, queryId, queryObj, tag) {
3481 this.log_('Unlisten on ' + pathString + ' for ' + queryId);
3482 const req = { /*path*/ p: pathString };
3483 const action = 'n';
3484 // Only bother sending queryId if it's non-default.
3485 if (tag) {
3486 req['q'] = queryObj;
3487 req['t'] = tag;
3488 }
3489 this.sendRequest(action, req);
3490 }
3491 onDisconnectPut(pathString, data, onComplete) {
3492 this.initConnection_();
3493 if (this.connected_) {
3494 this.sendOnDisconnect_('o', pathString, data, onComplete);
3495 }
3496 else {
3497 this.onDisconnectRequestQueue_.push({
3498 pathString,
3499 action: 'o',
3500 data,
3501 onComplete
3502 });
3503 }
3504 }
3505 onDisconnectMerge(pathString, data, onComplete) {
3506 this.initConnection_();
3507 if (this.connected_) {
3508 this.sendOnDisconnect_('om', pathString, data, onComplete);
3509 }
3510 else {
3511 this.onDisconnectRequestQueue_.push({
3512 pathString,
3513 action: 'om',
3514 data,
3515 onComplete
3516 });
3517 }
3518 }
3519 onDisconnectCancel(pathString, onComplete) {
3520 this.initConnection_();
3521 if (this.connected_) {
3522 this.sendOnDisconnect_('oc', pathString, null, onComplete);
3523 }
3524 else {
3525 this.onDisconnectRequestQueue_.push({
3526 pathString,
3527 action: 'oc',
3528 data: null,
3529 onComplete
3530 });
3531 }
3532 }
3533 sendOnDisconnect_(action, pathString, data, onComplete) {
3534 const request = { /*path*/ p: pathString, /*data*/ d: data };
3535 this.log_('onDisconnect ' + action, request);
3536 this.sendRequest(action, request, (response) => {
3537 if (onComplete) {
3538 setTimeout(() => {
3539 onComplete(response[ /*status*/'s'], response[ /* data */'d']);
3540 }, Math.floor(0));
3541 }
3542 });
3543 }
3544 put(pathString, data, onComplete, hash) {
3545 this.putInternal('p', pathString, data, onComplete, hash);
3546 }
3547 merge(pathString, data, onComplete, hash) {
3548 this.putInternal('m', pathString, data, onComplete, hash);
3549 }
3550 putInternal(action, pathString, data, onComplete, hash) {
3551 this.initConnection_();
3552 const request = {
3553 /*path*/ p: pathString,
3554 /*data*/ d: data
3555 };
3556 if (hash !== undefined) {
3557 request[ /*hash*/'h'] = hash;
3558 }
3559 // TODO: Only keep track of the most recent put for a given path?
3560 this.outstandingPuts_.push({
3561 action,
3562 request,
3563 onComplete
3564 });
3565 this.outstandingPutCount_++;
3566 const index = this.outstandingPuts_.length - 1;
3567 if (this.connected_) {
3568 this.sendPut_(index);
3569 }
3570 else {
3571 this.log_('Buffering put: ' + pathString);
3572 }
3573 }
3574 sendPut_(index) {
3575 const action = this.outstandingPuts_[index].action;
3576 const request = this.outstandingPuts_[index].request;
3577 const onComplete = this.outstandingPuts_[index].onComplete;
3578 this.outstandingPuts_[index].queued = this.connected_;
3579 this.sendRequest(action, request, (message) => {
3580 this.log_(action + ' response', message);
3581 delete this.outstandingPuts_[index];
3582 this.outstandingPutCount_--;
3583 // Clean up array occasionally.
3584 if (this.outstandingPutCount_ === 0) {
3585 this.outstandingPuts_ = [];
3586 }
3587 if (onComplete) {
3588 onComplete(message[ /*status*/'s'], message[ /* data */'d']);
3589 }
3590 });
3591 }
3592 reportStats(stats) {
3593 // If we're not connected, we just drop the stats.
3594 if (this.connected_) {
3595 const request = { /*counters*/ c: stats };
3596 this.log_('reportStats', request);
3597 this.sendRequest(/*stats*/ 's', request, result => {
3598 const status = result[ /*status*/'s'];
3599 if (status !== 'ok') {
3600 const errorReason = result[ /* data */'d'];
3601 this.log_('reportStats', 'Error sending stats: ' + errorReason);
3602 }
3603 });
3604 }
3605 }
3606 onDataMessage_(message) {
3607 if ('r' in message) {
3608 // this is a response
3609 this.log_('from server: ' + stringify(message));
3610 const reqNum = message['r'];
3611 const onResponse = this.requestCBHash_[reqNum];
3612 if (onResponse) {
3613 delete this.requestCBHash_[reqNum];
3614 onResponse(message[ /*body*/'b']);
3615 }
3616 }
3617 else if ('error' in message) {
3618 throw 'A server-side error has occurred: ' + message['error'];
3619 }
3620 else if ('a' in message) {
3621 // a and b are action and body, respectively
3622 this.onDataPush_(message['a'], message['b']);
3623 }
3624 }
3625 onDataPush_(action, body) {
3626 this.log_('handleServerMessage', action, body);
3627 if (action === 'd') {
3628 this.onDataUpdate_(body[ /*path*/'p'], body[ /*data*/'d'],
3629 /*isMerge*/ false, body['t']);
3630 }
3631 else if (action === 'm') {
3632 this.onDataUpdate_(body[ /*path*/'p'], body[ /*data*/'d'],
3633 /*isMerge=*/ true, body['t']);
3634 }
3635 else if (action === 'c') {
3636 this.onListenRevoked_(body[ /*path*/'p'], body[ /*query*/'q']);
3637 }
3638 else if (action === 'ac') {
3639 this.onAuthRevoked_(body[ /*status code*/'s'], body[ /* explanation */'d']);
3640 }
3641 else if (action === 'apc') {
3642 this.onAppCheckRevoked_(body[ /*status code*/'s'], body[ /* explanation */'d']);
3643 }
3644 else if (action === 'sd') {
3645 this.onSecurityDebugPacket_(body);
3646 }
3647 else {
3648 error('Unrecognized action received from server: ' +
3649 stringify(action) +
3650 '\nAre you using the latest client?');
3651 }
3652 }
3653 onReady_(timestamp, sessionId) {
3654 this.log_('connection ready');
3655 this.connected_ = true;
3656 this.lastConnectionEstablishedTime_ = new Date().getTime();
3657 this.handleTimestamp_(timestamp);
3658 this.lastSessionId = sessionId;
3659 if (this.firstConnection_) {
3660 this.sendConnectStats_();
3661 }
3662 this.restoreState_();
3663 this.firstConnection_ = false;
3664 this.onConnectStatus_(true);
3665 }
3666 scheduleConnect_(timeout) {
3667 assert(!this.realtime_, "Scheduling a connect when we're already connected/ing?");
3668 if (this.establishConnectionTimer_) {
3669 clearTimeout(this.establishConnectionTimer_);
3670 }
3671 // NOTE: Even when timeout is 0, it's important to do a setTimeout to work around an infuriating "Security Error" in
3672 // Firefox when trying to write to our long-polling iframe in some scenarios (e.g. Forge or our unit tests).
3673 this.establishConnectionTimer_ = setTimeout(() => {
3674 this.establishConnectionTimer_ = null;
3675 this.establishConnection_();
3676 // eslint-disable-next-line @typescript-eslint/no-explicit-any
3677 }, Math.floor(timeout));
3678 }
3679 initConnection_() {
3680 if (!this.realtime_ && this.firstConnection_) {
3681 this.scheduleConnect_(0);
3682 }
3683 }
3684 onVisible_(visible) {
3685 // NOTE: Tabbing away and back to a window will defeat our reconnect backoff, but I think that's fine.
3686 if (visible &&
3687 !this.visible_ &&
3688 this.reconnectDelay_ === this.maxReconnectDelay_) {
3689 this.log_('Window became visible. Reducing delay.');
3690 this.reconnectDelay_ = RECONNECT_MIN_DELAY;
3691 if (!this.realtime_) {
3692 this.scheduleConnect_(0);
3693 }
3694 }
3695 this.visible_ = visible;
3696 }
3697 onOnline_(online) {
3698 if (online) {
3699 this.log_('Browser went online.');
3700 this.reconnectDelay_ = RECONNECT_MIN_DELAY;
3701 if (!this.realtime_) {
3702 this.scheduleConnect_(0);
3703 }
3704 }
3705 else {
3706 this.log_('Browser went offline. Killing connection.');
3707 if (this.realtime_) {
3708 this.realtime_.close();
3709 }
3710 }
3711 }
3712 onRealtimeDisconnect_() {
3713 this.log_('data client disconnected');
3714 this.connected_ = false;
3715 this.realtime_ = null;
3716 // Since we don't know if our sent transactions succeeded or not, we need to cancel them.
3717 this.cancelSentTransactions_();
3718 // Clear out the pending requests.
3719 this.requestCBHash_ = {};
3720 if (this.shouldReconnect_()) {
3721 if (!this.visible_) {
3722 this.log_("Window isn't visible. Delaying reconnect.");
3723 this.reconnectDelay_ = this.maxReconnectDelay_;
3724 this.lastConnectionAttemptTime_ = new Date().getTime();
3725 }
3726 else if (this.lastConnectionEstablishedTime_) {
3727 // If we've been connected long enough, reset reconnect delay to minimum.
3728 const timeSinceLastConnectSucceeded = new Date().getTime() - this.lastConnectionEstablishedTime_;
3729 if (timeSinceLastConnectSucceeded > RECONNECT_DELAY_RESET_TIMEOUT) {
3730 this.reconnectDelay_ = RECONNECT_MIN_DELAY;
3731 }
3732 this.lastConnectionEstablishedTime_ = null;
3733 }
3734 const timeSinceLastConnectAttempt = new Date().getTime() - this.lastConnectionAttemptTime_;
3735 let reconnectDelay = Math.max(0, this.reconnectDelay_ - timeSinceLastConnectAttempt);
3736 reconnectDelay = Math.random() * reconnectDelay;
3737 this.log_('Trying to reconnect in ' + reconnectDelay + 'ms');
3738 this.scheduleConnect_(reconnectDelay);
3739 // Adjust reconnect delay for next time.
3740 this.reconnectDelay_ = Math.min(this.maxReconnectDelay_, this.reconnectDelay_ * RECONNECT_DELAY_MULTIPLIER);
3741 }
3742 this.onConnectStatus_(false);
3743 }
3744 async establishConnection_() {
3745 if (this.shouldReconnect_()) {
3746 this.log_('Making a connection attempt');
3747 this.lastConnectionAttemptTime_ = new Date().getTime();
3748 this.lastConnectionEstablishedTime_ = null;
3749 const onDataMessage = this.onDataMessage_.bind(this);
3750 const onReady = this.onReady_.bind(this);
3751 const onDisconnect = this.onRealtimeDisconnect_.bind(this);
3752 const connId = this.id + ':' + PersistentConnection.nextConnectionId_++;
3753 const lastSessionId = this.lastSessionId;
3754 let canceled = false;
3755 let connection = null;
3756 const closeFn = function () {
3757 if (connection) {
3758 connection.close();
3759 }
3760 else {
3761 canceled = true;
3762 onDisconnect();
3763 }
3764 };
3765 const sendRequestFn = function (msg) {
3766 assert(connection, "sendRequest call when we're not connected not allowed.");
3767 connection.sendRequest(msg);
3768 };
3769 this.realtime_ = {
3770 close: closeFn,
3771 sendRequest: sendRequestFn
3772 };
3773 const forceRefresh = this.forceTokenRefresh_;
3774 this.forceTokenRefresh_ = false;
3775 try {
3776 // First fetch auth and app check token, and establish connection after
3777 // fetching the token was successful
3778 const [authToken, appCheckToken] = await Promise.all([
3779 this.authTokenProvider_.getToken(forceRefresh),
3780 this.appCheckTokenProvider_.getToken(forceRefresh)
3781 ]);
3782 if (!canceled) {
3783 log('getToken() completed. Creating connection.');
3784 this.authToken_ = authToken && authToken.accessToken;
3785 this.appCheckToken_ = appCheckToken && appCheckToken.token;
3786 connection = new Connection(connId, this.repoInfo_, this.applicationId_, this.appCheckToken_, this.authToken_, onDataMessage, onReady, onDisconnect,
3787 /* onKill= */ reason => {
3788 warn(reason + ' (' + this.repoInfo_.toString() + ')');
3789 this.interrupt(SERVER_KILL_INTERRUPT_REASON);
3790 }, lastSessionId);
3791 }
3792 else {
3793 log('getToken() completed but was canceled');
3794 }
3795 }
3796 catch (error) {
3797 this.log_('Failed to get token: ' + error);
3798 if (!canceled) {
3799 if (this.repoInfo_.nodeAdmin) {
3800 // This may be a critical error for the Admin Node.js SDK, so log a warning.
3801 // But getToken() may also just have temporarily failed, so we still want to
3802 // continue retrying.
3803 warn(error);
3804 }
3805 closeFn();
3806 }
3807 }
3808 }
3809 }
3810 interrupt(reason) {
3811 log('Interrupting connection for reason: ' + reason);
3812 this.interruptReasons_[reason] = true;
3813 if (this.realtime_) {
3814 this.realtime_.close();
3815 }
3816 else {
3817 if (this.establishConnectionTimer_) {
3818 clearTimeout(this.establishConnectionTimer_);
3819 this.establishConnectionTimer_ = null;
3820 }
3821 if (this.connected_) {
3822 this.onRealtimeDisconnect_();
3823 }
3824 }
3825 }
3826 resume(reason) {
3827 log('Resuming connection for reason: ' + reason);
3828 delete this.interruptReasons_[reason];
3829 if (isEmpty(this.interruptReasons_)) {
3830 this.reconnectDelay_ = RECONNECT_MIN_DELAY;
3831 if (!this.realtime_) {
3832 this.scheduleConnect_(0);
3833 }
3834 }
3835 }
3836 handleTimestamp_(timestamp) {
3837 const delta = timestamp - new Date().getTime();
3838 this.onServerInfoUpdate_({ serverTimeOffset: delta });
3839 }
3840 cancelSentTransactions_() {
3841 for (let i = 0; i < this.outstandingPuts_.length; i++) {
3842 const put = this.outstandingPuts_[i];
3843 if (put && /*hash*/ 'h' in put.request && put.queued) {
3844 if (put.onComplete) {
3845 put.onComplete('disconnect');
3846 }
3847 delete this.outstandingPuts_[i];
3848 this.outstandingPutCount_--;
3849 }
3850 }
3851 // Clean up array occasionally.
3852 if (this.outstandingPutCount_ === 0) {
3853 this.outstandingPuts_ = [];
3854 }
3855 }
3856 onListenRevoked_(pathString, query) {
3857 // Remove the listen and manufacture a "permission_denied" error for the failed listen.
3858 let queryId;
3859 if (!query) {
3860 queryId = 'default';
3861 }
3862 else {
3863 queryId = query.map(q => ObjectToUniqueKey(q)).join('$');
3864 }
3865 const listen = this.removeListen_(pathString, queryId);
3866 if (listen && listen.onComplete) {
3867 listen.onComplete('permission_denied');
3868 }
3869 }
3870 removeListen_(pathString, queryId) {
3871 const normalizedPathString = new Path(pathString).toString(); // normalize path.
3872 let listen;
3873 if (this.listens.has(normalizedPathString)) {
3874 const map = this.listens.get(normalizedPathString);
3875 listen = map.get(queryId);
3876 map.delete(queryId);
3877 if (map.size === 0) {
3878 this.listens.delete(normalizedPathString);
3879 }
3880 }
3881 else {
3882 // all listens for this path has already been removed
3883 listen = undefined;
3884 }
3885 return listen;
3886 }
3887 onAuthRevoked_(statusCode, explanation) {
3888 log('Auth token revoked: ' + statusCode + '/' + explanation);
3889 this.authToken_ = null;
3890 this.forceTokenRefresh_ = true;
3891 this.realtime_.close();
3892 if (statusCode === 'invalid_token' || statusCode === 'permission_denied') {
3893 // We'll wait a couple times before logging the warning / increasing the
3894 // retry period since oauth tokens will report as "invalid" if they're
3895 // just expired. Plus there may be transient issues that resolve themselves.
3896 this.invalidAuthTokenCount_++;
3897 if (this.invalidAuthTokenCount_ >= INVALID_TOKEN_THRESHOLD) {
3898 // Set a long reconnect delay because recovery is unlikely
3899 this.reconnectDelay_ = RECONNECT_MAX_DELAY_FOR_ADMINS;
3900 // Notify the auth token provider that the token is invalid, which will log
3901 // a warning
3902 this.authTokenProvider_.notifyForInvalidToken();
3903 }
3904 }
3905 }
3906 onAppCheckRevoked_(statusCode, explanation) {
3907 log('App check token revoked: ' + statusCode + '/' + explanation);
3908 this.appCheckToken_ = null;
3909 this.forceTokenRefresh_ = true;
3910 // Note: We don't close the connection as the developer may not have
3911 // enforcement enabled. The backend closes connections with enforcements.
3912 if (statusCode === 'invalid_token' || statusCode === 'permission_denied') {
3913 // We'll wait a couple times before logging the warning / increasing the
3914 // retry period since oauth tokens will report as "invalid" if they're
3915 // just expired. Plus there may be transient issues that resolve themselves.
3916 this.invalidAppCheckTokenCount_++;
3917 if (this.invalidAppCheckTokenCount_ >= INVALID_TOKEN_THRESHOLD) {
3918 this.appCheckTokenProvider_.notifyForInvalidToken();
3919 }
3920 }
3921 }
3922 onSecurityDebugPacket_(body) {
3923 if (this.securityDebugCallback_) {
3924 this.securityDebugCallback_(body);
3925 }
3926 else {
3927 if ('msg' in body) {
3928 console.log('FIREBASE: ' + body['msg'].replace('\n', '\nFIREBASE: '));
3929 }
3930 }
3931 }
3932 restoreState_() {
3933 //Re-authenticate ourselves if we have a credential stored.
3934 this.tryAuth();
3935 this.tryAppCheck();
3936 // Puts depend on having received the corresponding data update from the server before they complete, so we must
3937 // make sure to send listens before puts.
3938 for (const queries of this.listens.values()) {
3939 for (const listenSpec of queries.values()) {
3940 this.sendListen_(listenSpec);
3941 }
3942 }
3943 for (let i = 0; i < this.outstandingPuts_.length; i++) {
3944 if (this.outstandingPuts_[i]) {
3945 this.sendPut_(i);
3946 }
3947 }
3948 while (this.onDisconnectRequestQueue_.length) {
3949 const request = this.onDisconnectRequestQueue_.shift();
3950 this.sendOnDisconnect_(request.action, request.pathString, request.data, request.onComplete);
3951 }
3952 for (let i = 0; i < this.outstandingGets_.length; i++) {
3953 if (this.outstandingGets_[i]) {
3954 this.sendGet_(i);
3955 }
3956 }
3957 }
3958 /**
3959 * Sends client stats for first connection
3960 */
3961 sendConnectStats_() {
3962 const stats = {};
3963 let clientName = 'js';
3964 if (isNodeSdk()) {
3965 if (this.repoInfo_.nodeAdmin) {
3966 clientName = 'admin_node';
3967 }
3968 else {
3969 clientName = 'node';
3970 }
3971 }
3972 stats['sdk.' + clientName + '.' + SDK_VERSION.replace(/\./g, '-')] = 1;
3973 if (isMobileCordova()) {
3974 stats['framework.cordova'] = 1;
3975 }
3976 else if (isReactNative()) {
3977 stats['framework.reactnative'] = 1;
3978 }
3979 this.reportStats(stats);
3980 }
3981 shouldReconnect_() {
3982 const online = OnlineMonitor.getInstance().currentlyOnline();
3983 return isEmpty(this.interruptReasons_) && online;
3984 }
3985}
3986PersistentConnection.nextPersistentConnectionId_ = 0;
3987/**
3988 * Counter for number of connections created. Mainly used for tagging in the logs
3989 */
3990PersistentConnection.nextConnectionId_ = 0;
3991
3992/**
3993 * @license
3994 * Copyright 2017 Google LLC
3995 *
3996 * Licensed under the Apache License, Version 2.0 (the "License");
3997 * you may not use this file except in compliance with the License.
3998 * You may obtain a copy of the License at
3999 *
4000 * http://www.apache.org/licenses/LICENSE-2.0
4001 *
4002 * Unless required by applicable law or agreed to in writing, software
4003 * distributed under the License is distributed on an "AS IS" BASIS,
4004 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4005 * See the License for the specific language governing permissions and
4006 * limitations under the License.
4007 */
4008class NamedNode {
4009 constructor(name, node) {
4010 this.name = name;
4011 this.node = node;
4012 }
4013 static Wrap(name, node) {
4014 return new NamedNode(name, node);
4015 }
4016}
4017
4018/**
4019 * @license
4020 * Copyright 2017 Google LLC
4021 *
4022 * Licensed under the Apache License, Version 2.0 (the "License");
4023 * you may not use this file except in compliance with the License.
4024 * You may obtain a copy of the License at
4025 *
4026 * http://www.apache.org/licenses/LICENSE-2.0
4027 *
4028 * Unless required by applicable law or agreed to in writing, software
4029 * distributed under the License is distributed on an "AS IS" BASIS,
4030 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4031 * See the License for the specific language governing permissions and
4032 * limitations under the License.
4033 */
4034class Index {
4035 /**
4036 * @returns A standalone comparison function for
4037 * this index
4038 */
4039 getCompare() {
4040 return this.compare.bind(this);
4041 }
4042 /**
4043 * Given a before and after value for a node, determine if the indexed value has changed. Even if they are different,
4044 * it's possible that the changes are isolated to parts of the snapshot that are not indexed.
4045 *
4046 *
4047 * @returns True if the portion of the snapshot being indexed changed between oldNode and newNode
4048 */
4049 indexedValueChanged(oldNode, newNode) {
4050 const oldWrapped = new NamedNode(MIN_NAME, oldNode);
4051 const newWrapped = new NamedNode(MIN_NAME, newNode);
4052 return this.compare(oldWrapped, newWrapped) !== 0;
4053 }
4054 /**
4055 * @returns a node wrapper that will sort equal to or less than
4056 * any other node wrapper, using this index
4057 */
4058 minPost() {
4059 // eslint-disable-next-line @typescript-eslint/no-explicit-any
4060 return NamedNode.MIN;
4061 }
4062}
4063
4064/**
4065 * @license
4066 * Copyright 2017 Google LLC
4067 *
4068 * Licensed under the Apache License, Version 2.0 (the "License");
4069 * you may not use this file except in compliance with the License.
4070 * You may obtain a copy of the License at
4071 *
4072 * http://www.apache.org/licenses/LICENSE-2.0
4073 *
4074 * Unless required by applicable law or agreed to in writing, software
4075 * distributed under the License is distributed on an "AS IS" BASIS,
4076 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4077 * See the License for the specific language governing permissions and
4078 * limitations under the License.
4079 */
4080let __EMPTY_NODE;
4081class KeyIndex extends Index {
4082 static get __EMPTY_NODE() {
4083 return __EMPTY_NODE;
4084 }
4085 static set __EMPTY_NODE(val) {
4086 __EMPTY_NODE = val;
4087 }
4088 compare(a, b) {
4089 return nameCompare(a.name, b.name);
4090 }
4091 isDefinedOn(node) {
4092 // We could probably return true here (since every node has a key), but it's never called
4093 // so just leaving unimplemented for now.
4094 throw assertionError('KeyIndex.isDefinedOn not expected to be called.');
4095 }
4096 indexedValueChanged(oldNode, newNode) {
4097 return false; // The key for a node never changes.
4098 }
4099 minPost() {
4100 // eslint-disable-next-line @typescript-eslint/no-explicit-any
4101 return NamedNode.MIN;
4102 }
4103 maxPost() {
4104 // TODO: This should really be created once and cached in a static property, but
4105 // NamedNode isn't defined yet, so I can't use it in a static. Bleh.
4106 return new NamedNode(MAX_NAME, __EMPTY_NODE);
4107 }
4108 makePost(indexValue, name) {
4109 assert(typeof indexValue === 'string', 'KeyIndex indexValue must always be a string.');
4110 // We just use empty node, but it'll never be compared, since our comparator only looks at name.
4111 return new NamedNode(indexValue, __EMPTY_NODE);
4112 }
4113 /**
4114 * @returns String representation for inclusion in a query spec
4115 */
4116 toString() {
4117 return '.key';
4118 }
4119}
4120const KEY_INDEX = new KeyIndex();
4121
4122/**
4123 * @license
4124 * Copyright 2017 Google LLC
4125 *
4126 * Licensed under the Apache License, Version 2.0 (the "License");
4127 * you may not use this file except in compliance with the License.
4128 * You may obtain a copy of the License at
4129 *
4130 * http://www.apache.org/licenses/LICENSE-2.0
4131 *
4132 * Unless required by applicable law or agreed to in writing, software
4133 * distributed under the License is distributed on an "AS IS" BASIS,
4134 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4135 * See the License for the specific language governing permissions and
4136 * limitations under the License.
4137 */
4138/**
4139 * An iterator over an LLRBNode.
4140 */
4141class SortedMapIterator {
4142 /**
4143 * @param node - Node to iterate.
4144 * @param isReverse_ - Whether or not to iterate in reverse
4145 */
4146 constructor(node, startKey, comparator, isReverse_, resultGenerator_ = null) {
4147 this.isReverse_ = isReverse_;
4148 this.resultGenerator_ = resultGenerator_;
4149 this.nodeStack_ = [];
4150 let cmp = 1;
4151 while (!node.isEmpty()) {
4152 node = node;
4153 cmp = startKey ? comparator(node.key, startKey) : 1;
4154 // flip the comparison if we're going in reverse
4155 if (isReverse_) {
4156 cmp *= -1;
4157 }
4158 if (cmp < 0) {
4159 // This node is less than our start key. ignore it
4160 if (this.isReverse_) {
4161 node = node.left;
4162 }
4163 else {
4164 node = node.right;
4165 }
4166 }
4167 else if (cmp === 0) {
4168 // This node is exactly equal to our start key. Push it on the stack, but stop iterating;
4169 this.nodeStack_.push(node);
4170 break;
4171 }
4172 else {
4173 // This node is greater than our start key, add it to the stack and move to the next one
4174 this.nodeStack_.push(node);
4175 if (this.isReverse_) {
4176 node = node.right;
4177 }
4178 else {
4179 node = node.left;
4180 }
4181 }
4182 }
4183 }
4184 getNext() {
4185 if (this.nodeStack_.length === 0) {
4186 return null;
4187 }
4188 let node = this.nodeStack_.pop();
4189 let result;
4190 if (this.resultGenerator_) {
4191 result = this.resultGenerator_(node.key, node.value);
4192 }
4193 else {
4194 result = { key: node.key, value: node.value };
4195 }
4196 if (this.isReverse_) {
4197 node = node.left;
4198 while (!node.isEmpty()) {
4199 this.nodeStack_.push(node);
4200 node = node.right;
4201 }
4202 }
4203 else {
4204 node = node.right;
4205 while (!node.isEmpty()) {
4206 this.nodeStack_.push(node);
4207 node = node.left;
4208 }
4209 }
4210 return result;
4211 }
4212 hasNext() {
4213 return this.nodeStack_.length > 0;
4214 }
4215 peek() {
4216 if (this.nodeStack_.length === 0) {
4217 return null;
4218 }
4219 const node = this.nodeStack_[this.nodeStack_.length - 1];
4220 if (this.resultGenerator_) {
4221 return this.resultGenerator_(node.key, node.value);
4222 }
4223 else {
4224 return { key: node.key, value: node.value };
4225 }
4226 }
4227}
4228/**
4229 * Represents a node in a Left-leaning Red-Black tree.
4230 */
4231class LLRBNode {
4232 /**
4233 * @param key - Key associated with this node.
4234 * @param value - Value associated with this node.
4235 * @param color - Whether this node is red.
4236 * @param left - Left child.
4237 * @param right - Right child.
4238 */
4239 constructor(key, value, color, left, right) {
4240 this.key = key;
4241 this.value = value;
4242 this.color = color != null ? color : LLRBNode.RED;
4243 this.left =
4244 left != null ? left : SortedMap.EMPTY_NODE;
4245 this.right =
4246 right != null ? right : SortedMap.EMPTY_NODE;
4247 }
4248 /**
4249 * Returns a copy of the current node, optionally replacing pieces of it.
4250 *
4251 * @param key - New key for the node, or null.
4252 * @param value - New value for the node, or null.
4253 * @param color - New color for the node, or null.
4254 * @param left - New left child for the node, or null.
4255 * @param right - New right child for the node, or null.
4256 * @returns The node copy.
4257 */
4258 copy(key, value, color, left, right) {
4259 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);
4260 }
4261 /**
4262 * @returns The total number of nodes in the tree.
4263 */
4264 count() {
4265 return this.left.count() + 1 + this.right.count();
4266 }
4267 /**
4268 * @returns True if the tree is empty.
4269 */
4270 isEmpty() {
4271 return false;
4272 }
4273 /**
4274 * Traverses the tree in 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 The first truthy value returned by action, or the last falsey
4280 * value returned by action
4281 */
4282 inorderTraversal(action) {
4283 return (this.left.inorderTraversal(action) ||
4284 !!action(this.key, this.value) ||
4285 this.right.inorderTraversal(action));
4286 }
4287 /**
4288 * Traverses the tree in reverse key order and calls the specified action function
4289 * for each node.
4290 *
4291 * @param action - Callback function to be called for each
4292 * node. If it returns true, traversal is aborted.
4293 * @returns True if traversal was aborted.
4294 */
4295 reverseTraversal(action) {
4296 return (this.right.reverseTraversal(action) ||
4297 action(this.key, this.value) ||
4298 this.left.reverseTraversal(action));
4299 }
4300 /**
4301 * @returns The minimum node in the tree.
4302 */
4303 min_() {
4304 if (this.left.isEmpty()) {
4305 return this;
4306 }
4307 else {
4308 return this.left.min_();
4309 }
4310 }
4311 /**
4312 * @returns The maximum key in the tree.
4313 */
4314 minKey() {
4315 return this.min_().key;
4316 }
4317 /**
4318 * @returns The maximum key in the tree.
4319 */
4320 maxKey() {
4321 if (this.right.isEmpty()) {
4322 return this.key;
4323 }
4324 else {
4325 return this.right.maxKey();
4326 }
4327 }
4328 /**
4329 * @param key - Key to insert.
4330 * @param value - Value to insert.
4331 * @param comparator - Comparator.
4332 * @returns New tree, with the key/value added.
4333 */
4334 insert(key, value, comparator) {
4335 let n = this;
4336 const cmp = comparator(key, n.key);
4337 if (cmp < 0) {
4338 n = n.copy(null, null, null, n.left.insert(key, value, comparator), null);
4339 }
4340 else if (cmp === 0) {
4341 n = n.copy(null, value, null, null, null);
4342 }
4343 else {
4344 n = n.copy(null, null, null, null, n.right.insert(key, value, comparator));
4345 }
4346 return n.fixUp_();
4347 }
4348 /**
4349 * @returns New tree, with the minimum key removed.
4350 */
4351 removeMin_() {
4352 if (this.left.isEmpty()) {
4353 return SortedMap.EMPTY_NODE;
4354 }
4355 let n = this;
4356 if (!n.left.isRed_() && !n.left.left.isRed_()) {
4357 n = n.moveRedLeft_();
4358 }
4359 n = n.copy(null, null, null, n.left.removeMin_(), null);
4360 return n.fixUp_();
4361 }
4362 /**
4363 * @param key - The key of the item to remove.
4364 * @param comparator - Comparator.
4365 * @returns New tree, with the specified item removed.
4366 */
4367 remove(key, comparator) {
4368 let n, smallest;
4369 n = this;
4370 if (comparator(key, n.key) < 0) {
4371 if (!n.left.isEmpty() && !n.left.isRed_() && !n.left.left.isRed_()) {
4372 n = n.moveRedLeft_();
4373 }
4374 n = n.copy(null, null, null, n.left.remove(key, comparator), null);
4375 }
4376 else {
4377 if (n.left.isRed_()) {
4378 n = n.rotateRight_();
4379 }
4380 if (!n.right.isEmpty() && !n.right.isRed_() && !n.right.left.isRed_()) {
4381 n = n.moveRedRight_();
4382 }
4383 if (comparator(key, n.key) === 0) {
4384 if (n.right.isEmpty()) {
4385 return SortedMap.EMPTY_NODE;
4386 }
4387 else {
4388 smallest = n.right.min_();
4389 n = n.copy(smallest.key, smallest.value, null, null, n.right.removeMin_());
4390 }
4391 }
4392 n = n.copy(null, null, null, null, n.right.remove(key, comparator));
4393 }
4394 return n.fixUp_();
4395 }
4396 /**
4397 * @returns Whether this is a RED node.
4398 */
4399 isRed_() {
4400 return this.color;
4401 }
4402 /**
4403 * @returns New tree after performing any needed rotations.
4404 */
4405 fixUp_() {
4406 let n = this;
4407 if (n.right.isRed_() && !n.left.isRed_()) {
4408 n = n.rotateLeft_();
4409 }
4410 if (n.left.isRed_() && n.left.left.isRed_()) {
4411 n = n.rotateRight_();
4412 }
4413 if (n.left.isRed_() && n.right.isRed_()) {
4414 n = n.colorFlip_();
4415 }
4416 return n;
4417 }
4418 /**
4419 * @returns New tree, after moveRedLeft.
4420 */
4421 moveRedLeft_() {
4422 let n = this.colorFlip_();
4423 if (n.right.left.isRed_()) {
4424 n = n.copy(null, null, null, null, n.right.rotateRight_());
4425 n = n.rotateLeft_();
4426 n = n.colorFlip_();
4427 }
4428 return n;
4429 }
4430 /**
4431 * @returns New tree, after moveRedRight.
4432 */
4433 moveRedRight_() {
4434 let n = this.colorFlip_();
4435 if (n.left.left.isRed_()) {
4436 n = n.rotateRight_();
4437 n = n.colorFlip_();
4438 }
4439 return n;
4440 }
4441 /**
4442 * @returns New tree, after rotateLeft.
4443 */
4444 rotateLeft_() {
4445 const nl = this.copy(null, null, LLRBNode.RED, null, this.right.left);
4446 return this.right.copy(null, null, this.color, nl, null);
4447 }
4448 /**
4449 * @returns New tree, after rotateRight.
4450 */
4451 rotateRight_() {
4452 const nr = this.copy(null, null, LLRBNode.RED, this.left.right, null);
4453 return this.left.copy(null, null, this.color, null, nr);
4454 }
4455 /**
4456 * @returns Newt ree, after colorFlip.
4457 */
4458 colorFlip_() {
4459 const left = this.left.copy(null, null, !this.left.color, null, null);
4460 const right = this.right.copy(null, null, !this.right.color, null, null);
4461 return this.copy(null, null, !this.color, left, right);
4462 }
4463 /**
4464 * For testing.
4465 *
4466 * @returns True if all is well.
4467 */
4468 checkMaxDepth_() {
4469 const blackDepth = this.check_();
4470 return Math.pow(2.0, blackDepth) <= this.count() + 1;
4471 }
4472 check_() {
4473 if (this.isRed_() && this.left.isRed_()) {
4474 throw new Error('Red node has red child(' + this.key + ',' + this.value + ')');
4475 }
4476 if (this.right.isRed_()) {
4477 throw new Error('Right child of (' + this.key + ',' + this.value + ') is red');
4478 }
4479 const blackDepth = this.left.check_();
4480 if (blackDepth !== this.right.check_()) {
4481 throw new Error('Black depths differ');
4482 }
4483 else {
4484 return blackDepth + (this.isRed_() ? 0 : 1);
4485 }
4486 }
4487}
4488LLRBNode.RED = true;
4489LLRBNode.BLACK = false;
4490/**
4491 * Represents an empty node (a leaf node in the Red-Black Tree).
4492 */
4493class LLRBEmptyNode {
4494 /**
4495 * Returns a copy of the current node.
4496 *
4497 * @returns The node copy.
4498 */
4499 copy(key, value, color, left, right) {
4500 return this;
4501 }
4502 /**
4503 * Returns a copy of the tree, with the specified key/value added.
4504 *
4505 * @param key - Key to be added.
4506 * @param value - Value to be added.
4507 * @param comparator - Comparator.
4508 * @returns New tree, with item added.
4509 */
4510 insert(key, value, comparator) {
4511 return new LLRBNode(key, value, null);
4512 }
4513 /**
4514 * Returns a copy of the tree, with the specified key removed.
4515 *
4516 * @param key - The key to remove.
4517 * @param comparator - Comparator.
4518 * @returns New tree, with item removed.
4519 */
4520 remove(key, comparator) {
4521 return this;
4522 }
4523 /**
4524 * @returns The total number of nodes in the tree.
4525 */
4526 count() {
4527 return 0;
4528 }
4529 /**
4530 * @returns True if the tree is empty.
4531 */
4532 isEmpty() {
4533 return true;
4534 }
4535 /**
4536 * Traverses the tree in key order and calls the specified action function
4537 * for each node.
4538 *
4539 * @param action - Callback function to be called for each
4540 * node. If it returns true, traversal is aborted.
4541 * @returns True if traversal was aborted.
4542 */
4543 inorderTraversal(action) {
4544 return false;
4545 }
4546 /**
4547 * Traverses the tree in reverse key order and calls the specified action function
4548 * for each node.
4549 *
4550 * @param action - Callback function to be called for each
4551 * node. If it returns true, traversal is aborted.
4552 * @returns True if traversal was aborted.
4553 */
4554 reverseTraversal(action) {
4555 return false;
4556 }
4557 minKey() {
4558 return null;
4559 }
4560 maxKey() {
4561 return null;
4562 }
4563 check_() {
4564 return 0;
4565 }
4566 /**
4567 * @returns Whether this node is red.
4568 */
4569 isRed_() {
4570 return false;
4571 }
4572}
4573/**
4574 * An immutable sorted map implementation, based on a Left-leaning Red-Black
4575 * tree.
4576 */
4577class SortedMap {
4578 /**
4579 * @param comparator_ - Key comparator.
4580 * @param root_ - Optional root node for the map.
4581 */
4582 constructor(comparator_, root_ = SortedMap.EMPTY_NODE) {
4583 this.comparator_ = comparator_;
4584 this.root_ = root_;
4585 }
4586 /**
4587 * Returns a copy of the map, with the specified key/value added or replaced.
4588 * (TODO: We should perhaps rename this method to 'put')
4589 *
4590 * @param key - Key to be added.
4591 * @param value - Value to be added.
4592 * @returns New map, with item added.
4593 */
4594 insert(key, value) {
4595 return new SortedMap(this.comparator_, this.root_
4596 .insert(key, value, this.comparator_)
4597 .copy(null, null, LLRBNode.BLACK, null, null));
4598 }
4599 /**
4600 * Returns a copy of the map, with the specified key removed.
4601 *
4602 * @param key - The key to remove.
4603 * @returns New map, with item removed.
4604 */
4605 remove(key) {
4606 return new SortedMap(this.comparator_, this.root_
4607 .remove(key, this.comparator_)
4608 .copy(null, null, LLRBNode.BLACK, null, null));
4609 }
4610 /**
4611 * Returns the value of the node with the given key, or null.
4612 *
4613 * @param key - The key to look up.
4614 * @returns The value of the node with the given key, or null if the
4615 * key doesn't exist.
4616 */
4617 get(key) {
4618 let cmp;
4619 let node = this.root_;
4620 while (!node.isEmpty()) {
4621 cmp = this.comparator_(key, node.key);
4622 if (cmp === 0) {
4623 return node.value;
4624 }
4625 else if (cmp < 0) {
4626 node = node.left;
4627 }
4628 else if (cmp > 0) {
4629 node = node.right;
4630 }
4631 }
4632 return null;
4633 }
4634 /**
4635 * Returns the key of the item *before* the specified key, or null if key is the first item.
4636 * @param key - The key to find the predecessor of
4637 * @returns The predecessor key.
4638 */
4639 getPredecessorKey(key) {
4640 let cmp, node = this.root_, rightParent = null;
4641 while (!node.isEmpty()) {
4642 cmp = this.comparator_(key, node.key);
4643 if (cmp === 0) {
4644 if (!node.left.isEmpty()) {
4645 node = node.left;
4646 while (!node.right.isEmpty()) {
4647 node = node.right;
4648 }
4649 return node.key;
4650 }
4651 else if (rightParent) {
4652 return rightParent.key;
4653 }
4654 else {
4655 return null; // first item.
4656 }
4657 }
4658 else if (cmp < 0) {
4659 node = node.left;
4660 }
4661 else if (cmp > 0) {
4662 rightParent = node;
4663 node = node.right;
4664 }
4665 }
4666 throw new Error('Attempted to find predecessor key for a nonexistent key. What gives?');
4667 }
4668 /**
4669 * @returns True if the map is empty.
4670 */
4671 isEmpty() {
4672 return this.root_.isEmpty();
4673 }
4674 /**
4675 * @returns The total number of nodes in the map.
4676 */
4677 count() {
4678 return this.root_.count();
4679 }
4680 /**
4681 * @returns The minimum key in the map.
4682 */
4683 minKey() {
4684 return this.root_.minKey();
4685 }
4686 /**
4687 * @returns The maximum key in the map.
4688 */
4689 maxKey() {
4690 return this.root_.maxKey();
4691 }
4692 /**
4693 * Traverses the map in key order and calls the specified action function
4694 * for each key/value pair.
4695 *
4696 * @param action - Callback function to be called
4697 * for each key/value pair. If action returns true, traversal is aborted.
4698 * @returns The first truthy value returned by action, or the last falsey
4699 * value returned by action
4700 */
4701 inorderTraversal(action) {
4702 return this.root_.inorderTraversal(action);
4703 }
4704 /**
4705 * Traverses the map in reverse key order and calls the specified action function
4706 * for each key/value pair.
4707 *
4708 * @param action - Callback function to be called
4709 * for each key/value pair. If action returns true, traversal is aborted.
4710 * @returns True if the traversal was aborted.
4711 */
4712 reverseTraversal(action) {
4713 return this.root_.reverseTraversal(action);
4714 }
4715 /**
4716 * Returns an iterator over the SortedMap.
4717 * @returns The iterator.
4718 */
4719 getIterator(resultGenerator) {
4720 return new SortedMapIterator(this.root_, null, this.comparator_, false, resultGenerator);
4721 }
4722 getIteratorFrom(key, resultGenerator) {
4723 return new SortedMapIterator(this.root_, key, this.comparator_, false, resultGenerator);
4724 }
4725 getReverseIteratorFrom(key, resultGenerator) {
4726 return new SortedMapIterator(this.root_, key, this.comparator_, true, resultGenerator);
4727 }
4728 getReverseIterator(resultGenerator) {
4729 return new SortedMapIterator(this.root_, null, this.comparator_, true, resultGenerator);
4730 }
4731}
4732/**
4733 * Always use the same empty node, to reduce memory.
4734 */
4735SortedMap.EMPTY_NODE = new LLRBEmptyNode();
4736
4737/**
4738 * @license
4739 * Copyright 2017 Google LLC
4740 *
4741 * Licensed under the Apache License, Version 2.0 (the "License");
4742 * you may not use this file except in compliance with the License.
4743 * You may obtain a copy of the License at
4744 *
4745 * http://www.apache.org/licenses/LICENSE-2.0
4746 *
4747 * Unless required by applicable law or agreed to in writing, software
4748 * distributed under the License is distributed on an "AS IS" BASIS,
4749 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4750 * See the License for the specific language governing permissions and
4751 * limitations under the License.
4752 */
4753function NAME_ONLY_COMPARATOR(left, right) {
4754 return nameCompare(left.name, right.name);
4755}
4756function NAME_COMPARATOR(left, right) {
4757 return nameCompare(left, right);
4758}
4759
4760/**
4761 * @license
4762 * Copyright 2017 Google LLC
4763 *
4764 * Licensed under the Apache License, Version 2.0 (the "License");
4765 * you may not use this file except in compliance with the License.
4766 * You may obtain a copy of the License at
4767 *
4768 * http://www.apache.org/licenses/LICENSE-2.0
4769 *
4770 * Unless required by applicable law or agreed to in writing, software
4771 * distributed under the License is distributed on an "AS IS" BASIS,
4772 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4773 * See the License for the specific language governing permissions and
4774 * limitations under the License.
4775 */
4776let MAX_NODE$2;
4777function setMaxNode$1(val) {
4778 MAX_NODE$2 = val;
4779}
4780const priorityHashText = function (priority) {
4781 if (typeof priority === 'number') {
4782 return 'number:' + doubleToIEEE754String(priority);
4783 }
4784 else {
4785 return 'string:' + priority;
4786 }
4787};
4788/**
4789 * Validates that a priority snapshot Node is valid.
4790 */
4791const validatePriorityNode = function (priorityNode) {
4792 if (priorityNode.isLeafNode()) {
4793 const val = priorityNode.val();
4794 assert(typeof val === 'string' ||
4795 typeof val === 'number' ||
4796 (typeof val === 'object' && contains(val, '.sv')), 'Priority must be a string or number.');
4797 }
4798 else {
4799 assert(priorityNode === MAX_NODE$2 || priorityNode.isEmpty(), 'priority of unexpected type.');
4800 }
4801 // Don't call getPriority() on MAX_NODE to avoid hitting assertion.
4802 assert(priorityNode === MAX_NODE$2 || priorityNode.getPriority().isEmpty(), "Priority nodes can't have a priority of their own.");
4803};
4804
4805/**
4806 * @license
4807 * Copyright 2017 Google LLC
4808 *
4809 * Licensed under the Apache License, Version 2.0 (the "License");
4810 * you may not use this file except in compliance with the License.
4811 * You may obtain a copy of the License at
4812 *
4813 * http://www.apache.org/licenses/LICENSE-2.0
4814 *
4815 * Unless required by applicable law or agreed to in writing, software
4816 * distributed under the License is distributed on an "AS IS" BASIS,
4817 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4818 * See the License for the specific language governing permissions and
4819 * limitations under the License.
4820 */
4821let __childrenNodeConstructor;
4822/**
4823 * LeafNode is a class for storing leaf nodes in a DataSnapshot. It
4824 * implements Node and stores the value of the node (a string,
4825 * number, or boolean) accessible via getValue().
4826 */
4827class LeafNode {
4828 /**
4829 * @param value_ - The value to store in this leaf node. The object type is
4830 * possible in the event of a deferred value
4831 * @param priorityNode_ - The priority of this node.
4832 */
4833 constructor(value_, priorityNode_ = LeafNode.__childrenNodeConstructor.EMPTY_NODE) {
4834 this.value_ = value_;
4835 this.priorityNode_ = priorityNode_;
4836 this.lazyHash_ = null;
4837 assert(this.value_ !== undefined && this.value_ !== null, "LeafNode shouldn't be created with null/undefined value.");
4838 validatePriorityNode(this.priorityNode_);
4839 }
4840 static set __childrenNodeConstructor(val) {
4841 __childrenNodeConstructor = val;
4842 }
4843 static get __childrenNodeConstructor() {
4844 return __childrenNodeConstructor;
4845 }
4846 /** @inheritDoc */
4847 isLeafNode() {
4848 return true;
4849 }
4850 /** @inheritDoc */
4851 getPriority() {
4852 return this.priorityNode_;
4853 }
4854 /** @inheritDoc */
4855 updatePriority(newPriorityNode) {
4856 return new LeafNode(this.value_, newPriorityNode);
4857 }
4858 /** @inheritDoc */
4859 getImmediateChild(childName) {
4860 // Hack to treat priority as a regular child
4861 if (childName === '.priority') {
4862 return this.priorityNode_;
4863 }
4864 else {
4865 return LeafNode.__childrenNodeConstructor.EMPTY_NODE;
4866 }
4867 }
4868 /** @inheritDoc */
4869 getChild(path) {
4870 if (pathIsEmpty(path)) {
4871 return this;
4872 }
4873 else if (pathGetFront(path) === '.priority') {
4874 return this.priorityNode_;
4875 }
4876 else {
4877 return LeafNode.__childrenNodeConstructor.EMPTY_NODE;
4878 }
4879 }
4880 hasChild() {
4881 return false;
4882 }
4883 /** @inheritDoc */
4884 getPredecessorChildName(childName, childNode) {
4885 return null;
4886 }
4887 /** @inheritDoc */
4888 updateImmediateChild(childName, newChildNode) {
4889 if (childName === '.priority') {
4890 return this.updatePriority(newChildNode);
4891 }
4892 else if (newChildNode.isEmpty() && childName !== '.priority') {
4893 return this;
4894 }
4895 else {
4896 return LeafNode.__childrenNodeConstructor.EMPTY_NODE.updateImmediateChild(childName, newChildNode).updatePriority(this.priorityNode_);
4897 }
4898 }
4899 /** @inheritDoc */
4900 updateChild(path, newChildNode) {
4901 const front = pathGetFront(path);
4902 if (front === null) {
4903 return newChildNode;
4904 }
4905 else if (newChildNode.isEmpty() && front !== '.priority') {
4906 return this;
4907 }
4908 else {
4909 assert(front !== '.priority' || pathGetLength(path) === 1, '.priority must be the last token in a path');
4910 return this.updateImmediateChild(front, LeafNode.__childrenNodeConstructor.EMPTY_NODE.updateChild(pathPopFront(path), newChildNode));
4911 }
4912 }
4913 /** @inheritDoc */
4914 isEmpty() {
4915 return false;
4916 }
4917 /** @inheritDoc */
4918 numChildren() {
4919 return 0;
4920 }
4921 /** @inheritDoc */
4922 forEachChild(index, action) {
4923 return false;
4924 }
4925 val(exportFormat) {
4926 if (exportFormat && !this.getPriority().isEmpty()) {
4927 return {
4928 '.value': this.getValue(),
4929 '.priority': this.getPriority().val()
4930 };
4931 }
4932 else {
4933 return this.getValue();
4934 }
4935 }
4936 /** @inheritDoc */
4937 hash() {
4938 if (this.lazyHash_ === null) {
4939 let toHash = '';
4940 if (!this.priorityNode_.isEmpty()) {
4941 toHash +=
4942 'priority:' +
4943 priorityHashText(this.priorityNode_.val()) +
4944 ':';
4945 }
4946 const type = typeof this.value_;
4947 toHash += type + ':';
4948 if (type === 'number') {
4949 toHash += doubleToIEEE754String(this.value_);
4950 }
4951 else {
4952 toHash += this.value_;
4953 }
4954 this.lazyHash_ = sha1(toHash);
4955 }
4956 return this.lazyHash_;
4957 }
4958 /**
4959 * Returns the value of the leaf node.
4960 * @returns The value of the node.
4961 */
4962 getValue() {
4963 return this.value_;
4964 }
4965 compareTo(other) {
4966 if (other === LeafNode.__childrenNodeConstructor.EMPTY_NODE) {
4967 return 1;
4968 }
4969 else if (other instanceof LeafNode.__childrenNodeConstructor) {
4970 return -1;
4971 }
4972 else {
4973 assert(other.isLeafNode(), 'Unknown node type');
4974 return this.compareToLeafNode_(other);
4975 }
4976 }
4977 /**
4978 * Comparison specifically for two leaf nodes
4979 */
4980 compareToLeafNode_(otherLeaf) {
4981 const otherLeafType = typeof otherLeaf.value_;
4982 const thisLeafType = typeof this.value_;
4983 const otherIndex = LeafNode.VALUE_TYPE_ORDER.indexOf(otherLeafType);
4984 const thisIndex = LeafNode.VALUE_TYPE_ORDER.indexOf(thisLeafType);
4985 assert(otherIndex >= 0, 'Unknown leaf type: ' + otherLeafType);
4986 assert(thisIndex >= 0, 'Unknown leaf type: ' + thisLeafType);
4987 if (otherIndex === thisIndex) {
4988 // Same type, compare values
4989 if (thisLeafType === 'object') {
4990 // Deferred value nodes are all equal, but we should also never get to this point...
4991 return 0;
4992 }
4993 else {
4994 // Note that this works because true > false, all others are number or string comparisons
4995 if (this.value_ < otherLeaf.value_) {
4996 return -1;
4997 }
4998 else if (this.value_ === otherLeaf.value_) {
4999 return 0;
5000 }
5001 else {
5002 return 1;
5003 }
5004 }
5005 }
5006 else {
5007 return thisIndex - otherIndex;
5008 }
5009 }
5010 withIndex() {
5011 return this;
5012 }
5013 isIndexed() {
5014 return true;
5015 }
5016 equals(other) {
5017 if (other === this) {
5018 return true;
5019 }
5020 else if (other.isLeafNode()) {
5021 const otherLeaf = other;
5022 return (this.value_ === otherLeaf.value_ &&
5023 this.priorityNode_.equals(otherLeaf.priorityNode_));
5024 }
5025 else {
5026 return false;
5027 }
5028 }
5029}
5030/**
5031 * The sort order for comparing leaf nodes of different types. If two leaf nodes have
5032 * the same type, the comparison falls back to their value
5033 */
5034LeafNode.VALUE_TYPE_ORDER = ['object', 'boolean', 'number', 'string'];
5035
5036/**
5037 * @license
5038 * Copyright 2017 Google LLC
5039 *
5040 * Licensed under the Apache License, Version 2.0 (the "License");
5041 * you may not use this file except in compliance with the License.
5042 * You may obtain a copy of the License at
5043 *
5044 * http://www.apache.org/licenses/LICENSE-2.0
5045 *
5046 * Unless required by applicable law or agreed to in writing, software
5047 * distributed under the License is distributed on an "AS IS" BASIS,
5048 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
5049 * See the License for the specific language governing permissions and
5050 * limitations under the License.
5051 */
5052let nodeFromJSON$1;
5053let MAX_NODE$1;
5054function setNodeFromJSON(val) {
5055 nodeFromJSON$1 = val;
5056}
5057function setMaxNode(val) {
5058 MAX_NODE$1 = val;
5059}
5060class PriorityIndex extends Index {
5061 compare(a, b) {
5062 const aPriority = a.node.getPriority();
5063 const bPriority = b.node.getPriority();
5064 const indexCmp = aPriority.compareTo(bPriority);
5065 if (indexCmp === 0) {
5066 return nameCompare(a.name, b.name);
5067 }
5068 else {
5069 return indexCmp;
5070 }
5071 }
5072 isDefinedOn(node) {
5073 return !node.getPriority().isEmpty();
5074 }
5075 indexedValueChanged(oldNode, newNode) {
5076 return !oldNode.getPriority().equals(newNode.getPriority());
5077 }
5078 minPost() {
5079 // eslint-disable-next-line @typescript-eslint/no-explicit-any
5080 return NamedNode.MIN;
5081 }
5082 maxPost() {
5083 return new NamedNode(MAX_NAME, new LeafNode('[PRIORITY-POST]', MAX_NODE$1));
5084 }
5085 makePost(indexValue, name) {
5086 const priorityNode = nodeFromJSON$1(indexValue);
5087 return new NamedNode(name, new LeafNode('[PRIORITY-POST]', priorityNode));
5088 }
5089 /**
5090 * @returns String representation for inclusion in a query spec
5091 */
5092 toString() {
5093 return '.priority';
5094 }
5095}
5096const PRIORITY_INDEX = new PriorityIndex();
5097
5098/**
5099 * @license
5100 * Copyright 2017 Google LLC
5101 *
5102 * Licensed under the Apache License, Version 2.0 (the "License");
5103 * you may not use this file except in compliance with the License.
5104 * You may obtain a copy of the License at
5105 *
5106 * http://www.apache.org/licenses/LICENSE-2.0
5107 *
5108 * Unless required by applicable law or agreed to in writing, software
5109 * distributed under the License is distributed on an "AS IS" BASIS,
5110 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
5111 * See the License for the specific language governing permissions and
5112 * limitations under the License.
5113 */
5114const LOG_2 = Math.log(2);
5115class Base12Num {
5116 constructor(length) {
5117 const logBase2 = (num) =>
5118 // eslint-disable-next-line @typescript-eslint/no-explicit-any
5119 parseInt((Math.log(num) / LOG_2), 10);
5120 const bitMask = (bits) => parseInt(Array(bits + 1).join('1'), 2);
5121 this.count = logBase2(length + 1);
5122 this.current_ = this.count - 1;
5123 const mask = bitMask(this.count);
5124 this.bits_ = (length + 1) & mask;
5125 }
5126 nextBitIsOne() {
5127 //noinspection JSBitwiseOperatorUsage
5128 const result = !(this.bits_ & (0x1 << this.current_));
5129 this.current_--;
5130 return result;
5131 }
5132}
5133/**
5134 * Takes a list of child nodes and constructs a SortedSet using the given comparison
5135 * function
5136 *
5137 * Uses the algorithm described in the paper linked here:
5138 * http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.46.1458
5139 *
5140 * @param childList - Unsorted list of children
5141 * @param cmp - The comparison method to be used
5142 * @param keyFn - An optional function to extract K from a node wrapper, if K's
5143 * type is not NamedNode
5144 * @param mapSortFn - An optional override for comparator used by the generated sorted map
5145 */
5146const buildChildSet = function (childList, cmp, keyFn, mapSortFn) {
5147 childList.sort(cmp);
5148 const buildBalancedTree = function (low, high) {
5149 const length = high - low;
5150 let namedNode;
5151 let key;
5152 if (length === 0) {
5153 return null;
5154 }
5155 else if (length === 1) {
5156 namedNode = childList[low];
5157 key = keyFn ? keyFn(namedNode) : namedNode;
5158 return new LLRBNode(key, namedNode.node, LLRBNode.BLACK, null, null);
5159 }
5160 else {
5161 // eslint-disable-next-line @typescript-eslint/no-explicit-any
5162 const middle = parseInt((length / 2), 10) + low;
5163 const left = buildBalancedTree(low, middle);
5164 const right = buildBalancedTree(middle + 1, high);
5165 namedNode = childList[middle];
5166 key = keyFn ? keyFn(namedNode) : namedNode;
5167 return new LLRBNode(key, namedNode.node, LLRBNode.BLACK, left, right);
5168 }
5169 };
5170 const buildFrom12Array = function (base12) {
5171 let node = null;
5172 let root = null;
5173 let index = childList.length;
5174 const buildPennant = function (chunkSize, color) {
5175 const low = index - chunkSize;
5176 const high = index;
5177 index -= chunkSize;
5178 const childTree = buildBalancedTree(low + 1, high);
5179 const namedNode = childList[low];
5180 const key = keyFn ? keyFn(namedNode) : namedNode;
5181 attachPennant(new LLRBNode(key, namedNode.node, color, null, childTree));
5182 };
5183 const attachPennant = function (pennant) {
5184 if (node) {
5185 node.left = pennant;
5186 node = pennant;
5187 }
5188 else {
5189 root = pennant;
5190 node = pennant;
5191 }
5192 };
5193 for (let i = 0; i < base12.count; ++i) {
5194 const isOne = base12.nextBitIsOne();
5195 // The number of nodes taken in each slice is 2^(arr.length - (i + 1))
5196 const chunkSize = Math.pow(2, base12.count - (i + 1));
5197 if (isOne) {
5198 buildPennant(chunkSize, LLRBNode.BLACK);
5199 }
5200 else {
5201 // current == 2
5202 buildPennant(chunkSize, LLRBNode.BLACK);
5203 buildPennant(chunkSize, LLRBNode.RED);
5204 }
5205 }
5206 return root;
5207 };
5208 const base12 = new Base12Num(childList.length);
5209 const root = buildFrom12Array(base12);
5210 // eslint-disable-next-line @typescript-eslint/no-explicit-any
5211 return new SortedMap(mapSortFn || cmp, root);
5212};
5213
5214/**
5215 * @license
5216 * Copyright 2017 Google LLC
5217 *
5218 * Licensed under the Apache License, Version 2.0 (the "License");
5219 * you may not use this file except in compliance with the License.
5220 * You may obtain a copy of the License at
5221 *
5222 * http://www.apache.org/licenses/LICENSE-2.0
5223 *
5224 * Unless required by applicable law or agreed to in writing, software
5225 * distributed under the License is distributed on an "AS IS" BASIS,
5226 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
5227 * See the License for the specific language governing permissions and
5228 * limitations under the License.
5229 */
5230let _defaultIndexMap;
5231const fallbackObject = {};
5232class IndexMap {
5233 constructor(indexes_, indexSet_) {
5234 this.indexes_ = indexes_;
5235 this.indexSet_ = indexSet_;
5236 }
5237 /**
5238 * The default IndexMap for nodes without a priority
5239 */
5240 static get Default() {
5241 assert(fallbackObject && PRIORITY_INDEX, 'ChildrenNode.ts has not been loaded');
5242 _defaultIndexMap =
5243 _defaultIndexMap ||
5244 new IndexMap({ '.priority': fallbackObject }, { '.priority': PRIORITY_INDEX });
5245 return _defaultIndexMap;
5246 }
5247 get(indexKey) {
5248 const sortedMap = safeGet(this.indexes_, indexKey);
5249 if (!sortedMap) {
5250 throw new Error('No index defined for ' + indexKey);
5251 }
5252 if (sortedMap instanceof SortedMap) {
5253 return sortedMap;
5254 }
5255 else {
5256 // The index exists, but it falls back to just name comparison. Return null so that the calling code uses the
5257 // regular child map
5258 return null;
5259 }
5260 }
5261 hasIndex(indexDefinition) {
5262 return contains(this.indexSet_, indexDefinition.toString());
5263 }
5264 addIndex(indexDefinition, existingChildren) {
5265 assert(indexDefinition !== KEY_INDEX, "KeyIndex always exists and isn't meant to be added to the IndexMap.");
5266 const childList = [];
5267 let sawIndexedValue = false;
5268 const iter = existingChildren.getIterator(NamedNode.Wrap);
5269 let next = iter.getNext();
5270 while (next) {
5271 sawIndexedValue =
5272 sawIndexedValue || indexDefinition.isDefinedOn(next.node);
5273 childList.push(next);
5274 next = iter.getNext();
5275 }
5276 let newIndex;
5277 if (sawIndexedValue) {
5278 newIndex = buildChildSet(childList, indexDefinition.getCompare());
5279 }
5280 else {
5281 newIndex = fallbackObject;
5282 }
5283 const indexName = indexDefinition.toString();
5284 const newIndexSet = Object.assign({}, this.indexSet_);
5285 newIndexSet[indexName] = indexDefinition;
5286 const newIndexes = Object.assign({}, this.indexes_);
5287 newIndexes[indexName] = newIndex;
5288 return new IndexMap(newIndexes, newIndexSet);
5289 }
5290 /**
5291 * Ensure that this node is properly tracked in any indexes that we're maintaining
5292 */
5293 addToIndexes(namedNode, existingChildren) {
5294 const newIndexes = map(this.indexes_, (indexedChildren, indexName) => {
5295 const index = safeGet(this.indexSet_, indexName);
5296 assert(index, 'Missing index implementation for ' + indexName);
5297 if (indexedChildren === fallbackObject) {
5298 // Check to see if we need to index everything
5299 if (index.isDefinedOn(namedNode.node)) {
5300 // We need to build this index
5301 const childList = [];
5302 const iter = existingChildren.getIterator(NamedNode.Wrap);
5303 let next = iter.getNext();
5304 while (next) {
5305 if (next.name !== namedNode.name) {
5306 childList.push(next);
5307 }
5308 next = iter.getNext();
5309 }
5310 childList.push(namedNode);
5311 return buildChildSet(childList, index.getCompare());
5312 }
5313 else {
5314 // No change, this remains a fallback
5315 return fallbackObject;
5316 }
5317 }
5318 else {
5319 const existingSnap = existingChildren.get(namedNode.name);
5320 let newChildren = indexedChildren;
5321 if (existingSnap) {
5322 newChildren = newChildren.remove(new NamedNode(namedNode.name, existingSnap));
5323 }
5324 return newChildren.insert(namedNode, namedNode.node);
5325 }
5326 });
5327 return new IndexMap(newIndexes, this.indexSet_);
5328 }
5329 /**
5330 * Create a new IndexMap instance with the given value removed
5331 */
5332 removeFromIndexes(namedNode, existingChildren) {
5333 const newIndexes = map(this.indexes_, (indexedChildren) => {
5334 if (indexedChildren === fallbackObject) {
5335 // This is the fallback. Just return it, nothing to do in this case
5336 return indexedChildren;
5337 }
5338 else {
5339 const existingSnap = existingChildren.get(namedNode.name);
5340 if (existingSnap) {
5341 return indexedChildren.remove(new NamedNode(namedNode.name, existingSnap));
5342 }
5343 else {
5344 // No record of this child
5345 return indexedChildren;
5346 }
5347 }
5348 });
5349 return new IndexMap(newIndexes, this.indexSet_);
5350 }
5351}
5352
5353/**
5354 * @license
5355 * Copyright 2017 Google LLC
5356 *
5357 * Licensed under the Apache License, Version 2.0 (the "License");
5358 * you may not use this file except in compliance with the License.
5359 * You may obtain a copy of the License at
5360 *
5361 * http://www.apache.org/licenses/LICENSE-2.0
5362 *
5363 * Unless required by applicable law or agreed to in writing, software
5364 * distributed under the License is distributed on an "AS IS" BASIS,
5365 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
5366 * See the License for the specific language governing permissions and
5367 * limitations under the License.
5368 */
5369// TODO: For memory savings, don't store priorityNode_ if it's empty.
5370let EMPTY_NODE;
5371/**
5372 * ChildrenNode is a class for storing internal nodes in a DataSnapshot
5373 * (i.e. nodes with children). It implements Node and stores the
5374 * list of children in the children property, sorted by child name.
5375 */
5376class ChildrenNode {
5377 /**
5378 * @param children_ - List of children of this node..
5379 * @param priorityNode_ - The priority of this node (as a snapshot node).
5380 */
5381 constructor(children_, priorityNode_, indexMap_) {
5382 this.children_ = children_;
5383 this.priorityNode_ = priorityNode_;
5384 this.indexMap_ = indexMap_;
5385 this.lazyHash_ = null;
5386 /**
5387 * Note: The only reason we allow null priority is for EMPTY_NODE, since we can't use
5388 * EMPTY_NODE as the priority of EMPTY_NODE. We might want to consider making EMPTY_NODE its own
5389 * class instead of an empty ChildrenNode.
5390 */
5391 if (this.priorityNode_) {
5392 validatePriorityNode(this.priorityNode_);
5393 }
5394 if (this.children_.isEmpty()) {
5395 assert(!this.priorityNode_ || this.priorityNode_.isEmpty(), 'An empty node cannot have a priority');
5396 }
5397 }
5398 static get EMPTY_NODE() {
5399 return (EMPTY_NODE ||
5400 (EMPTY_NODE = new ChildrenNode(new SortedMap(NAME_COMPARATOR), null, IndexMap.Default)));
5401 }
5402 /** @inheritDoc */
5403 isLeafNode() {
5404 return false;
5405 }
5406 /** @inheritDoc */
5407 getPriority() {
5408 return this.priorityNode_ || EMPTY_NODE;
5409 }
5410 /** @inheritDoc */
5411 updatePriority(newPriorityNode) {
5412 if (this.children_.isEmpty()) {
5413 // Don't allow priorities on empty nodes
5414 return this;
5415 }
5416 else {
5417 return new ChildrenNode(this.children_, newPriorityNode, this.indexMap_);
5418 }
5419 }
5420 /** @inheritDoc */
5421 getImmediateChild(childName) {
5422 // Hack to treat priority as a regular child
5423 if (childName === '.priority') {
5424 return this.getPriority();
5425 }
5426 else {
5427 const child = this.children_.get(childName);
5428 return child === null ? EMPTY_NODE : child;
5429 }
5430 }
5431 /** @inheritDoc */
5432 getChild(path) {
5433 const front = pathGetFront(path);
5434 if (front === null) {
5435 return this;
5436 }
5437 return this.getImmediateChild(front).getChild(pathPopFront(path));
5438 }
5439 /** @inheritDoc */
5440 hasChild(childName) {
5441 return this.children_.get(childName) !== null;
5442 }
5443 /** @inheritDoc */
5444 updateImmediateChild(childName, newChildNode) {
5445 assert(newChildNode, 'We should always be passing snapshot nodes');
5446 if (childName === '.priority') {
5447 return this.updatePriority(newChildNode);
5448 }
5449 else {
5450 const namedNode = new NamedNode(childName, newChildNode);
5451 let newChildren, newIndexMap;
5452 if (newChildNode.isEmpty()) {
5453 newChildren = this.children_.remove(childName);
5454 newIndexMap = this.indexMap_.removeFromIndexes(namedNode, this.children_);
5455 }
5456 else {
5457 newChildren = this.children_.insert(childName, newChildNode);
5458 newIndexMap = this.indexMap_.addToIndexes(namedNode, this.children_);
5459 }
5460 const newPriority = newChildren.isEmpty()
5461 ? EMPTY_NODE
5462 : this.priorityNode_;
5463 return new ChildrenNode(newChildren, newPriority, newIndexMap);
5464 }
5465 }
5466 /** @inheritDoc */
5467 updateChild(path, newChildNode) {
5468 const front = pathGetFront(path);
5469 if (front === null) {
5470 return newChildNode;
5471 }
5472 else {
5473 assert(pathGetFront(path) !== '.priority' || pathGetLength(path) === 1, '.priority must be the last token in a path');
5474 const newImmediateChild = this.getImmediateChild(front).updateChild(pathPopFront(path), newChildNode);
5475 return this.updateImmediateChild(front, newImmediateChild);
5476 }
5477 }
5478 /** @inheritDoc */
5479 isEmpty() {
5480 return this.children_.isEmpty();
5481 }
5482 /** @inheritDoc */
5483 numChildren() {
5484 return this.children_.count();
5485 }
5486 /** @inheritDoc */
5487 val(exportFormat) {
5488 if (this.isEmpty()) {
5489 return null;
5490 }
5491 const obj = {};
5492 let numKeys = 0, maxKey = 0, allIntegerKeys = true;
5493 this.forEachChild(PRIORITY_INDEX, (key, childNode) => {
5494 obj[key] = childNode.val(exportFormat);
5495 numKeys++;
5496 if (allIntegerKeys && ChildrenNode.INTEGER_REGEXP_.test(key)) {
5497 maxKey = Math.max(maxKey, Number(key));
5498 }
5499 else {
5500 allIntegerKeys = false;
5501 }
5502 });
5503 if (!exportFormat && allIntegerKeys && maxKey < 2 * numKeys) {
5504 // convert to array.
5505 const array = [];
5506 // eslint-disable-next-line guard-for-in
5507 for (const key in obj) {
5508 array[key] = obj[key];
5509 }
5510 return array;
5511 }
5512 else {
5513 if (exportFormat && !this.getPriority().isEmpty()) {
5514 obj['.priority'] = this.getPriority().val();
5515 }
5516 return obj;
5517 }
5518 }
5519 /** @inheritDoc */
5520 hash() {
5521 if (this.lazyHash_ === null) {
5522 let toHash = '';
5523 if (!this.getPriority().isEmpty()) {
5524 toHash +=
5525 'priority:' +
5526 priorityHashText(this.getPriority().val()) +
5527 ':';
5528 }
5529 this.forEachChild(PRIORITY_INDEX, (key, childNode) => {
5530 const childHash = childNode.hash();
5531 if (childHash !== '') {
5532 toHash += ':' + key + ':' + childHash;
5533 }
5534 });
5535 this.lazyHash_ = toHash === '' ? '' : sha1(toHash);
5536 }
5537 return this.lazyHash_;
5538 }
5539 /** @inheritDoc */
5540 getPredecessorChildName(childName, childNode, index) {
5541 const idx = this.resolveIndex_(index);
5542 if (idx) {
5543 const predecessor = idx.getPredecessorKey(new NamedNode(childName, childNode));
5544 return predecessor ? predecessor.name : null;
5545 }
5546 else {
5547 return this.children_.getPredecessorKey(childName);
5548 }
5549 }
5550 getFirstChildName(indexDefinition) {
5551 const idx = this.resolveIndex_(indexDefinition);
5552 if (idx) {
5553 const minKey = idx.minKey();
5554 return minKey && minKey.name;
5555 }
5556 else {
5557 return this.children_.minKey();
5558 }
5559 }
5560 getFirstChild(indexDefinition) {
5561 const minKey = this.getFirstChildName(indexDefinition);
5562 if (minKey) {
5563 return new NamedNode(minKey, this.children_.get(minKey));
5564 }
5565 else {
5566 return null;
5567 }
5568 }
5569 /**
5570 * Given an index, return the key name of the largest value we have, according to that index
5571 */
5572 getLastChildName(indexDefinition) {
5573 const idx = this.resolveIndex_(indexDefinition);
5574 if (idx) {
5575 const maxKey = idx.maxKey();
5576 return maxKey && maxKey.name;
5577 }
5578 else {
5579 return this.children_.maxKey();
5580 }
5581 }
5582 getLastChild(indexDefinition) {
5583 const maxKey = this.getLastChildName(indexDefinition);
5584 if (maxKey) {
5585 return new NamedNode(maxKey, this.children_.get(maxKey));
5586 }
5587 else {
5588 return null;
5589 }
5590 }
5591 forEachChild(index, action) {
5592 const idx = this.resolveIndex_(index);
5593 if (idx) {
5594 return idx.inorderTraversal(wrappedNode => {
5595 return action(wrappedNode.name, wrappedNode.node);
5596 });
5597 }
5598 else {
5599 return this.children_.inorderTraversal(action);
5600 }
5601 }
5602 getIterator(indexDefinition) {
5603 return this.getIteratorFrom(indexDefinition.minPost(), indexDefinition);
5604 }
5605 getIteratorFrom(startPost, indexDefinition) {
5606 const idx = this.resolveIndex_(indexDefinition);
5607 if (idx) {
5608 return idx.getIteratorFrom(startPost, key => key);
5609 }
5610 else {
5611 const iterator = this.children_.getIteratorFrom(startPost.name, NamedNode.Wrap);
5612 let next = iterator.peek();
5613 while (next != null && indexDefinition.compare(next, startPost) < 0) {
5614 iterator.getNext();
5615 next = iterator.peek();
5616 }
5617 return iterator;
5618 }
5619 }
5620 getReverseIterator(indexDefinition) {
5621 return this.getReverseIteratorFrom(indexDefinition.maxPost(), indexDefinition);
5622 }
5623 getReverseIteratorFrom(endPost, indexDefinition) {
5624 const idx = this.resolveIndex_(indexDefinition);
5625 if (idx) {
5626 return idx.getReverseIteratorFrom(endPost, key => {
5627 return key;
5628 });
5629 }
5630 else {
5631 const iterator = this.children_.getReverseIteratorFrom(endPost.name, NamedNode.Wrap);
5632 let next = iterator.peek();
5633 while (next != null && indexDefinition.compare(next, endPost) > 0) {
5634 iterator.getNext();
5635 next = iterator.peek();
5636 }
5637 return iterator;
5638 }
5639 }
5640 compareTo(other) {
5641 if (this.isEmpty()) {
5642 if (other.isEmpty()) {
5643 return 0;
5644 }
5645 else {
5646 return -1;
5647 }
5648 }
5649 else if (other.isLeafNode() || other.isEmpty()) {
5650 return 1;
5651 }
5652 else if (other === MAX_NODE) {
5653 return -1;
5654 }
5655 else {
5656 // Must be another node with children.
5657 return 0;
5658 }
5659 }
5660 withIndex(indexDefinition) {
5661 if (indexDefinition === KEY_INDEX ||
5662 this.indexMap_.hasIndex(indexDefinition)) {
5663 return this;
5664 }
5665 else {
5666 const newIndexMap = this.indexMap_.addIndex(indexDefinition, this.children_);
5667 return new ChildrenNode(this.children_, this.priorityNode_, newIndexMap);
5668 }
5669 }
5670 isIndexed(index) {
5671 return index === KEY_INDEX || this.indexMap_.hasIndex(index);
5672 }
5673 equals(other) {
5674 if (other === this) {
5675 return true;
5676 }
5677 else if (other.isLeafNode()) {
5678 return false;
5679 }
5680 else {
5681 const otherChildrenNode = other;
5682 if (!this.getPriority().equals(otherChildrenNode.getPriority())) {
5683 return false;
5684 }
5685 else if (this.children_.count() === otherChildrenNode.children_.count()) {
5686 const thisIter = this.getIterator(PRIORITY_INDEX);
5687 const otherIter = otherChildrenNode.getIterator(PRIORITY_INDEX);
5688 let thisCurrent = thisIter.getNext();
5689 let otherCurrent = otherIter.getNext();
5690 while (thisCurrent && otherCurrent) {
5691 if (thisCurrent.name !== otherCurrent.name ||
5692 !thisCurrent.node.equals(otherCurrent.node)) {
5693 return false;
5694 }
5695 thisCurrent = thisIter.getNext();
5696 otherCurrent = otherIter.getNext();
5697 }
5698 return thisCurrent === null && otherCurrent === null;
5699 }
5700 else {
5701 return false;
5702 }
5703 }
5704 }
5705 /**
5706 * Returns a SortedMap ordered by index, or null if the default (by-key) ordering can be used
5707 * instead.
5708 *
5709 */
5710 resolveIndex_(indexDefinition) {
5711 if (indexDefinition === KEY_INDEX) {
5712 return null;
5713 }
5714 else {
5715 return this.indexMap_.get(indexDefinition.toString());
5716 }
5717 }
5718}
5719ChildrenNode.INTEGER_REGEXP_ = /^(0|[1-9]\d*)$/;
5720class MaxNode extends ChildrenNode {
5721 constructor() {
5722 super(new SortedMap(NAME_COMPARATOR), ChildrenNode.EMPTY_NODE, IndexMap.Default);
5723 }
5724 compareTo(other) {
5725 if (other === this) {
5726 return 0;
5727 }
5728 else {
5729 return 1;
5730 }
5731 }
5732 equals(other) {
5733 // Not that we every compare it, but MAX_NODE is only ever equal to itself
5734 return other === this;
5735 }
5736 getPriority() {
5737 return this;
5738 }
5739 getImmediateChild(childName) {
5740 return ChildrenNode.EMPTY_NODE;
5741 }
5742 isEmpty() {
5743 return false;
5744 }
5745}
5746/**
5747 * Marker that will sort higher than any other snapshot.
5748 */
5749const MAX_NODE = new MaxNode();
5750Object.defineProperties(NamedNode, {
5751 MIN: {
5752 value: new NamedNode(MIN_NAME, ChildrenNode.EMPTY_NODE)
5753 },
5754 MAX: {
5755 value: new NamedNode(MAX_NAME, MAX_NODE)
5756 }
5757});
5758/**
5759 * Reference Extensions
5760 */
5761KeyIndex.__EMPTY_NODE = ChildrenNode.EMPTY_NODE;
5762LeafNode.__childrenNodeConstructor = ChildrenNode;
5763setMaxNode$1(MAX_NODE);
5764setMaxNode(MAX_NODE);
5765
5766/**
5767 * @license
5768 * Copyright 2017 Google LLC
5769 *
5770 * Licensed under the Apache License, Version 2.0 (the "License");
5771 * you may not use this file except in compliance with the License.
5772 * You may obtain a copy of the License at
5773 *
5774 * http://www.apache.org/licenses/LICENSE-2.0
5775 *
5776 * Unless required by applicable law or agreed to in writing, software
5777 * distributed under the License is distributed on an "AS IS" BASIS,
5778 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
5779 * See the License for the specific language governing permissions and
5780 * limitations under the License.
5781 */
5782const USE_HINZE = true;
5783/**
5784 * Constructs a snapshot node representing the passed JSON and returns it.
5785 * @param json - JSON to create a node for.
5786 * @param priority - Optional priority to use. This will be ignored if the
5787 * passed JSON contains a .priority property.
5788 */
5789function nodeFromJSON(json, priority = null) {
5790 if (json === null) {
5791 return ChildrenNode.EMPTY_NODE;
5792 }
5793 if (typeof json === 'object' && '.priority' in json) {
5794 priority = json['.priority'];
5795 }
5796 assert(priority === null ||
5797 typeof priority === 'string' ||
5798 typeof priority === 'number' ||
5799 (typeof priority === 'object' && '.sv' in priority), 'Invalid priority type found: ' + typeof priority);
5800 if (typeof json === 'object' && '.value' in json && json['.value'] !== null) {
5801 json = json['.value'];
5802 }
5803 // Valid leaf nodes include non-objects or server-value wrapper objects
5804 if (typeof json !== 'object' || '.sv' in json) {
5805 const jsonLeaf = json;
5806 return new LeafNode(jsonLeaf, nodeFromJSON(priority));
5807 }
5808 if (!(json instanceof Array) && USE_HINZE) {
5809 const children = [];
5810 let childrenHavePriority = false;
5811 const hinzeJsonObj = json;
5812 each(hinzeJsonObj, (key, child) => {
5813 if (key.substring(0, 1) !== '.') {
5814 // Ignore metadata nodes
5815 const childNode = nodeFromJSON(child);
5816 if (!childNode.isEmpty()) {
5817 childrenHavePriority =
5818 childrenHavePriority || !childNode.getPriority().isEmpty();
5819 children.push(new NamedNode(key, childNode));
5820 }
5821 }
5822 });
5823 if (children.length === 0) {
5824 return ChildrenNode.EMPTY_NODE;
5825 }
5826 const childSet = buildChildSet(children, NAME_ONLY_COMPARATOR, namedNode => namedNode.name, NAME_COMPARATOR);
5827 if (childrenHavePriority) {
5828 const sortedChildSet = buildChildSet(children, PRIORITY_INDEX.getCompare());
5829 return new ChildrenNode(childSet, nodeFromJSON(priority), new IndexMap({ '.priority': sortedChildSet }, { '.priority': PRIORITY_INDEX }));
5830 }
5831 else {
5832 return new ChildrenNode(childSet, nodeFromJSON(priority), IndexMap.Default);
5833 }
5834 }
5835 else {
5836 let node = ChildrenNode.EMPTY_NODE;
5837 each(json, (key, childData) => {
5838 if (contains(json, key)) {
5839 if (key.substring(0, 1) !== '.') {
5840 // ignore metadata nodes.
5841 const childNode = nodeFromJSON(childData);
5842 if (childNode.isLeafNode() || !childNode.isEmpty()) {
5843 node = node.updateImmediateChild(key, childNode);
5844 }
5845 }
5846 }
5847 });
5848 return node.updatePriority(nodeFromJSON(priority));
5849 }
5850}
5851setNodeFromJSON(nodeFromJSON);
5852
5853/**
5854 * @license
5855 * Copyright 2017 Google LLC
5856 *
5857 * Licensed under the Apache License, Version 2.0 (the "License");
5858 * you may not use this file except in compliance with the License.
5859 * You may obtain a copy of the License at
5860 *
5861 * http://www.apache.org/licenses/LICENSE-2.0
5862 *
5863 * Unless required by applicable law or agreed to in writing, software
5864 * distributed under the License is distributed on an "AS IS" BASIS,
5865 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
5866 * See the License for the specific language governing permissions and
5867 * limitations under the License.
5868 */
5869class PathIndex extends Index {
5870 constructor(indexPath_) {
5871 super();
5872 this.indexPath_ = indexPath_;
5873 assert(!pathIsEmpty(indexPath_) && pathGetFront(indexPath_) !== '.priority', "Can't create PathIndex with empty path or .priority key");
5874 }
5875 extractChild(snap) {
5876 return snap.getChild(this.indexPath_);
5877 }
5878 isDefinedOn(node) {
5879 return !node.getChild(this.indexPath_).isEmpty();
5880 }
5881 compare(a, b) {
5882 const aChild = this.extractChild(a.node);
5883 const bChild = this.extractChild(b.node);
5884 const indexCmp = aChild.compareTo(bChild);
5885 if (indexCmp === 0) {
5886 return nameCompare(a.name, b.name);
5887 }
5888 else {
5889 return indexCmp;
5890 }
5891 }
5892 makePost(indexValue, name) {
5893 const valueNode = nodeFromJSON(indexValue);
5894 const node = ChildrenNode.EMPTY_NODE.updateChild(this.indexPath_, valueNode);
5895 return new NamedNode(name, node);
5896 }
5897 maxPost() {
5898 const node = ChildrenNode.EMPTY_NODE.updateChild(this.indexPath_, MAX_NODE);
5899 return new NamedNode(MAX_NAME, node);
5900 }
5901 toString() {
5902 return pathSlice(this.indexPath_, 0).join('/');
5903 }
5904}
5905
5906/**
5907 * @license
5908 * Copyright 2017 Google LLC
5909 *
5910 * Licensed under the Apache License, Version 2.0 (the "License");
5911 * you may not use this file except in compliance with the License.
5912 * You may obtain a copy of the License at
5913 *
5914 * http://www.apache.org/licenses/LICENSE-2.0
5915 *
5916 * Unless required by applicable law or agreed to in writing, software
5917 * distributed under the License is distributed on an "AS IS" BASIS,
5918 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
5919 * See the License for the specific language governing permissions and
5920 * limitations under the License.
5921 */
5922class ValueIndex extends Index {
5923 compare(a, b) {
5924 const indexCmp = a.node.compareTo(b.node);
5925 if (indexCmp === 0) {
5926 return nameCompare(a.name, b.name);
5927 }
5928 else {
5929 return indexCmp;
5930 }
5931 }
5932 isDefinedOn(node) {
5933 return true;
5934 }
5935 indexedValueChanged(oldNode, newNode) {
5936 return !oldNode.equals(newNode);
5937 }
5938 minPost() {
5939 // eslint-disable-next-line @typescript-eslint/no-explicit-any
5940 return NamedNode.MIN;
5941 }
5942 maxPost() {
5943 // eslint-disable-next-line @typescript-eslint/no-explicit-any
5944 return NamedNode.MAX;
5945 }
5946 makePost(indexValue, name) {
5947 const valueNode = nodeFromJSON(indexValue);
5948 return new NamedNode(name, valueNode);
5949 }
5950 /**
5951 * @returns String representation for inclusion in a query spec
5952 */
5953 toString() {
5954 return '.value';
5955 }
5956}
5957const VALUE_INDEX = new ValueIndex();
5958
5959/**
5960 * @license
5961 * Copyright 2017 Google LLC
5962 *
5963 * Licensed under the Apache License, Version 2.0 (the "License");
5964 * you may not use this file except in compliance with the License.
5965 * You may obtain a copy of the License at
5966 *
5967 * http://www.apache.org/licenses/LICENSE-2.0
5968 *
5969 * Unless required by applicable law or agreed to in writing, software
5970 * distributed under the License is distributed on an "AS IS" BASIS,
5971 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
5972 * See the License for the specific language governing permissions and
5973 * limitations under the License.
5974 */
5975// Modeled after base64 web-safe chars, but ordered by ASCII.
5976const PUSH_CHARS = '-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz';
5977const MIN_PUSH_CHAR = '-';
5978const MAX_PUSH_CHAR = 'z';
5979const MAX_KEY_LEN = 786;
5980/**
5981 * Fancy ID generator that creates 20-character string identifiers with the
5982 * following properties:
5983 *
5984 * 1. They're based on timestamp so that they sort *after* any existing ids.
5985 * 2. They contain 72-bits of random data after the timestamp so that IDs won't
5986 * collide with other clients' IDs.
5987 * 3. They sort *lexicographically* (so the timestamp is converted to characters
5988 * that will sort properly).
5989 * 4. They're monotonically increasing. Even if you generate more than one in
5990 * the same timestamp, the latter ones will sort after the former ones. We do
5991 * this by using the previous random bits but "incrementing" them by 1 (only
5992 * in the case of a timestamp collision).
5993 */
5994const nextPushId = (function () {
5995 // Timestamp of last push, used to prevent local collisions if you push twice
5996 // in one ms.
5997 let lastPushTime = 0;
5998 // We generate 72-bits of randomness which get turned into 12 characters and
5999 // appended to the timestamp to prevent collisions with other clients. We
6000 // store the last characters we generated because in the event of a collision,
6001 // we'll use those same characters except "incremented" by one.
6002 const lastRandChars = [];
6003 return function (now) {
6004 const duplicateTime = now === lastPushTime;
6005 lastPushTime = now;
6006 let i;
6007 const timeStampChars = new Array(8);
6008 for (i = 7; i >= 0; i--) {
6009 timeStampChars[i] = PUSH_CHARS.charAt(now % 64);
6010 // NOTE: Can't use << here because javascript will convert to int and lose
6011 // the upper bits.
6012 now = Math.floor(now / 64);
6013 }
6014 assert(now === 0, 'Cannot push at time == 0');
6015 let id = timeStampChars.join('');
6016 if (!duplicateTime) {
6017 for (i = 0; i < 12; i++) {
6018 lastRandChars[i] = Math.floor(Math.random() * 64);
6019 }
6020 }
6021 else {
6022 // If the timestamp hasn't changed since last push, use the same random
6023 // number, except incremented by 1.
6024 for (i = 11; i >= 0 && lastRandChars[i] === 63; i--) {
6025 lastRandChars[i] = 0;
6026 }
6027 lastRandChars[i]++;
6028 }
6029 for (i = 0; i < 12; i++) {
6030 id += PUSH_CHARS.charAt(lastRandChars[i]);
6031 }
6032 assert(id.length === 20, 'nextPushId: Length should be 20.');
6033 return id;
6034 };
6035})();
6036const successor = function (key) {
6037 if (key === '' + INTEGER_32_MAX) {
6038 // See https://firebase.google.com/docs/database/web/lists-of-data#data-order
6039 return MIN_PUSH_CHAR;
6040 }
6041 const keyAsInt = tryParseInt(key);
6042 if (keyAsInt != null) {
6043 return '' + (keyAsInt + 1);
6044 }
6045 const next = new Array(key.length);
6046 for (let i = 0; i < next.length; i++) {
6047 next[i] = key.charAt(i);
6048 }
6049 if (next.length < MAX_KEY_LEN) {
6050 next.push(MIN_PUSH_CHAR);
6051 return next.join('');
6052 }
6053 let i = next.length - 1;
6054 while (i >= 0 && next[i] === MAX_PUSH_CHAR) {
6055 i--;
6056 }
6057 // `successor` was called on the largest possible key, so return the
6058 // MAX_NAME, which sorts larger than all keys.
6059 if (i === -1) {
6060 return MAX_NAME;
6061 }
6062 const source = next[i];
6063 const sourcePlusOne = PUSH_CHARS.charAt(PUSH_CHARS.indexOf(source) + 1);
6064 next[i] = sourcePlusOne;
6065 return next.slice(0, i + 1).join('');
6066};
6067// `key` is assumed to be non-empty.
6068const predecessor = function (key) {
6069 if (key === '' + INTEGER_32_MIN) {
6070 return MIN_NAME;
6071 }
6072 const keyAsInt = tryParseInt(key);
6073 if (keyAsInt != null) {
6074 return '' + (keyAsInt - 1);
6075 }
6076 const next = new Array(key.length);
6077 for (let i = 0; i < next.length; i++) {
6078 next[i] = key.charAt(i);
6079 }
6080 // If `key` ends in `MIN_PUSH_CHAR`, the largest key lexicographically
6081 // smaller than `key`, is `key[0:key.length - 1]`. The next key smaller
6082 // than that, `predecessor(predecessor(key))`, is
6083 //
6084 // `key[0:key.length - 2] + (key[key.length - 1] - 1) + \
6085 // { MAX_PUSH_CHAR repeated MAX_KEY_LEN - (key.length - 1) times }
6086 //
6087 // analogous to increment/decrement for base-10 integers.
6088 //
6089 // This works because lexigographic comparison works character-by-character,
6090 // using length as a tie-breaker if one key is a prefix of the other.
6091 if (next[next.length - 1] === MIN_PUSH_CHAR) {
6092 if (next.length === 1) {
6093 // See https://firebase.google.com/docs/database/web/lists-of-data#orderbykey
6094 return '' + INTEGER_32_MAX;
6095 }
6096 delete next[next.length - 1];
6097 return next.join('');
6098 }
6099 // Replace the last character with it's immediate predecessor, and
6100 // fill the suffix of the key with MAX_PUSH_CHAR. This is the
6101 // lexicographically largest possible key smaller than `key`.
6102 next[next.length - 1] = PUSH_CHARS.charAt(PUSH_CHARS.indexOf(next[next.length - 1]) - 1);
6103 return next.join('') + MAX_PUSH_CHAR.repeat(MAX_KEY_LEN - next.length);
6104};
6105
6106/**
6107 * @license
6108 * Copyright 2017 Google LLC
6109 *
6110 * Licensed under the Apache License, Version 2.0 (the "License");
6111 * you may not use this file except in compliance with the License.
6112 * You may obtain a copy of the License at
6113 *
6114 * http://www.apache.org/licenses/LICENSE-2.0
6115 *
6116 * Unless required by applicable law or agreed to in writing, software
6117 * distributed under the License is distributed on an "AS IS" BASIS,
6118 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
6119 * See the License for the specific language governing permissions and
6120 * limitations under the License.
6121 */
6122function changeValue(snapshotNode) {
6123 return { type: "value" /* VALUE */, snapshotNode };
6124}
6125function changeChildAdded(childName, snapshotNode) {
6126 return { type: "child_added" /* CHILD_ADDED */, snapshotNode, childName };
6127}
6128function changeChildRemoved(childName, snapshotNode) {
6129 return { type: "child_removed" /* CHILD_REMOVED */, snapshotNode, childName };
6130}
6131function changeChildChanged(childName, snapshotNode, oldSnap) {
6132 return {
6133 type: "child_changed" /* CHILD_CHANGED */,
6134 snapshotNode,
6135 childName,
6136 oldSnap
6137 };
6138}
6139function changeChildMoved(childName, snapshotNode) {
6140 return { type: "child_moved" /* CHILD_MOVED */, snapshotNode, childName };
6141}
6142
6143/**
6144 * @license
6145 * Copyright 2017 Google LLC
6146 *
6147 * Licensed under the Apache License, Version 2.0 (the "License");
6148 * you may not use this file except in compliance with the License.
6149 * You may obtain a copy of the License at
6150 *
6151 * http://www.apache.org/licenses/LICENSE-2.0
6152 *
6153 * Unless required by applicable law or agreed to in writing, software
6154 * distributed under the License is distributed on an "AS IS" BASIS,
6155 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
6156 * See the License for the specific language governing permissions and
6157 * limitations under the License.
6158 */
6159/**
6160 * Doesn't really filter nodes but applies an index to the node and keeps track of any changes
6161 */
6162class IndexedFilter {
6163 constructor(index_) {
6164 this.index_ = index_;
6165 }
6166 updateChild(snap, key, newChild, affectedPath, source, optChangeAccumulator) {
6167 assert(snap.isIndexed(this.index_), 'A node must be indexed if only a child is updated');
6168 const oldChild = snap.getImmediateChild(key);
6169 // Check if anything actually changed.
6170 if (oldChild.getChild(affectedPath).equals(newChild.getChild(affectedPath))) {
6171 // There's an edge case where a child can enter or leave the view because affectedPath was set to null.
6172 // In this case, affectedPath will appear null in both the old and new snapshots. So we need
6173 // to avoid treating these cases as "nothing changed."
6174 if (oldChild.isEmpty() === newChild.isEmpty()) {
6175 // Nothing changed.
6176 // This assert should be valid, but it's expensive (can dominate perf testing) so don't actually do it.
6177 //assert(oldChild.equals(newChild), 'Old and new snapshots should be equal.');
6178 return snap;
6179 }
6180 }
6181 if (optChangeAccumulator != null) {
6182 if (newChild.isEmpty()) {
6183 if (snap.hasChild(key)) {
6184 optChangeAccumulator.trackChildChange(changeChildRemoved(key, oldChild));
6185 }
6186 else {
6187 assert(snap.isLeafNode(), 'A child remove without an old child only makes sense on a leaf node');
6188 }
6189 }
6190 else if (oldChild.isEmpty()) {
6191 optChangeAccumulator.trackChildChange(changeChildAdded(key, newChild));
6192 }
6193 else {
6194 optChangeAccumulator.trackChildChange(changeChildChanged(key, newChild, oldChild));
6195 }
6196 }
6197 if (snap.isLeafNode() && newChild.isEmpty()) {
6198 return snap;
6199 }
6200 else {
6201 // Make sure the node is indexed
6202 return snap.updateImmediateChild(key, newChild).withIndex(this.index_);
6203 }
6204 }
6205 updateFullNode(oldSnap, newSnap, optChangeAccumulator) {
6206 if (optChangeAccumulator != null) {
6207 if (!oldSnap.isLeafNode()) {
6208 oldSnap.forEachChild(PRIORITY_INDEX, (key, childNode) => {
6209 if (!newSnap.hasChild(key)) {
6210 optChangeAccumulator.trackChildChange(changeChildRemoved(key, childNode));
6211 }
6212 });
6213 }
6214 if (!newSnap.isLeafNode()) {
6215 newSnap.forEachChild(PRIORITY_INDEX, (key, childNode) => {
6216 if (oldSnap.hasChild(key)) {
6217 const oldChild = oldSnap.getImmediateChild(key);
6218 if (!oldChild.equals(childNode)) {
6219 optChangeAccumulator.trackChildChange(changeChildChanged(key, childNode, oldChild));
6220 }
6221 }
6222 else {
6223 optChangeAccumulator.trackChildChange(changeChildAdded(key, childNode));
6224 }
6225 });
6226 }
6227 }
6228 return newSnap.withIndex(this.index_);
6229 }
6230 updatePriority(oldSnap, newPriority) {
6231 if (oldSnap.isEmpty()) {
6232 return ChildrenNode.EMPTY_NODE;
6233 }
6234 else {
6235 return oldSnap.updatePriority(newPriority);
6236 }
6237 }
6238 filtersNodes() {
6239 return false;
6240 }
6241 getIndexedFilter() {
6242 return this;
6243 }
6244 getIndex() {
6245 return this.index_;
6246 }
6247}
6248
6249/**
6250 * @license
6251 * Copyright 2017 Google LLC
6252 *
6253 * Licensed under the Apache License, Version 2.0 (the "License");
6254 * you may not use this file except in compliance with the License.
6255 * You may obtain a copy of the License at
6256 *
6257 * http://www.apache.org/licenses/LICENSE-2.0
6258 *
6259 * Unless required by applicable law or agreed to in writing, software
6260 * distributed under the License is distributed on an "AS IS" BASIS,
6261 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
6262 * See the License for the specific language governing permissions and
6263 * limitations under the License.
6264 */
6265/**
6266 * Filters nodes by range and uses an IndexFilter to track any changes after filtering the node
6267 */
6268class RangedFilter {
6269 constructor(params) {
6270 this.indexedFilter_ = new IndexedFilter(params.getIndex());
6271 this.index_ = params.getIndex();
6272 this.startPost_ = RangedFilter.getStartPost_(params);
6273 this.endPost_ = RangedFilter.getEndPost_(params);
6274 }
6275 getStartPost() {
6276 return this.startPost_;
6277 }
6278 getEndPost() {
6279 return this.endPost_;
6280 }
6281 matches(node) {
6282 return (this.index_.compare(this.getStartPost(), node) <= 0 &&
6283 this.index_.compare(node, this.getEndPost()) <= 0);
6284 }
6285 updateChild(snap, key, newChild, affectedPath, source, optChangeAccumulator) {
6286 if (!this.matches(new NamedNode(key, newChild))) {
6287 newChild = ChildrenNode.EMPTY_NODE;
6288 }
6289 return this.indexedFilter_.updateChild(snap, key, newChild, affectedPath, source, optChangeAccumulator);
6290 }
6291 updateFullNode(oldSnap, newSnap, optChangeAccumulator) {
6292 if (newSnap.isLeafNode()) {
6293 // Make sure we have a children node with the correct index, not a leaf node;
6294 newSnap = ChildrenNode.EMPTY_NODE;
6295 }
6296 let filtered = newSnap.withIndex(this.index_);
6297 // Don't support priorities on queries
6298 filtered = filtered.updatePriority(ChildrenNode.EMPTY_NODE);
6299 const self = this;
6300 newSnap.forEachChild(PRIORITY_INDEX, (key, childNode) => {
6301 if (!self.matches(new NamedNode(key, childNode))) {
6302 filtered = filtered.updateImmediateChild(key, ChildrenNode.EMPTY_NODE);
6303 }
6304 });
6305 return this.indexedFilter_.updateFullNode(oldSnap, filtered, optChangeAccumulator);
6306 }
6307 updatePriority(oldSnap, newPriority) {
6308 // Don't support priorities on queries
6309 return oldSnap;
6310 }
6311 filtersNodes() {
6312 return true;
6313 }
6314 getIndexedFilter() {
6315 return this.indexedFilter_;
6316 }
6317 getIndex() {
6318 return this.index_;
6319 }
6320 static getStartPost_(params) {
6321 if (params.hasStart()) {
6322 const startName = params.getIndexStartName();
6323 return params.getIndex().makePost(params.getIndexStartValue(), startName);
6324 }
6325 else {
6326 return params.getIndex().minPost();
6327 }
6328 }
6329 static getEndPost_(params) {
6330 if (params.hasEnd()) {
6331 const endName = params.getIndexEndName();
6332 return params.getIndex().makePost(params.getIndexEndValue(), endName);
6333 }
6334 else {
6335 return params.getIndex().maxPost();
6336 }
6337 }
6338}
6339
6340/**
6341 * @license
6342 * Copyright 2017 Google LLC
6343 *
6344 * Licensed under the Apache License, Version 2.0 (the "License");
6345 * you may not use this file except in compliance with the License.
6346 * You may obtain a copy of the License at
6347 *
6348 * http://www.apache.org/licenses/LICENSE-2.0
6349 *
6350 * Unless required by applicable law or agreed to in writing, software
6351 * distributed under the License is distributed on an "AS IS" BASIS,
6352 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
6353 * See the License for the specific language governing permissions and
6354 * limitations under the License.
6355 */
6356/**
6357 * Applies a limit and a range to a node and uses RangedFilter to do the heavy lifting where possible
6358 */
6359class LimitedFilter {
6360 constructor(params) {
6361 this.rangedFilter_ = new RangedFilter(params);
6362 this.index_ = params.getIndex();
6363 this.limit_ = params.getLimit();
6364 this.reverse_ = !params.isViewFromLeft();
6365 }
6366 updateChild(snap, key, newChild, affectedPath, source, optChangeAccumulator) {
6367 if (!this.rangedFilter_.matches(new NamedNode(key, newChild))) {
6368 newChild = ChildrenNode.EMPTY_NODE;
6369 }
6370 if (snap.getImmediateChild(key).equals(newChild)) {
6371 // No change
6372 return snap;
6373 }
6374 else if (snap.numChildren() < this.limit_) {
6375 return this.rangedFilter_
6376 .getIndexedFilter()
6377 .updateChild(snap, key, newChild, affectedPath, source, optChangeAccumulator);
6378 }
6379 else {
6380 return this.fullLimitUpdateChild_(snap, key, newChild, source, optChangeAccumulator);
6381 }
6382 }
6383 updateFullNode(oldSnap, newSnap, optChangeAccumulator) {
6384 let filtered;
6385 if (newSnap.isLeafNode() || newSnap.isEmpty()) {
6386 // Make sure we have a children node with the correct index, not a leaf node;
6387 filtered = ChildrenNode.EMPTY_NODE.withIndex(this.index_);
6388 }
6389 else {
6390 if (this.limit_ * 2 < newSnap.numChildren() &&
6391 newSnap.isIndexed(this.index_)) {
6392 // Easier to build up a snapshot, since what we're given has more than twice the elements we want
6393 filtered = ChildrenNode.EMPTY_NODE.withIndex(this.index_);
6394 // anchor to the startPost, endPost, or last element as appropriate
6395 let iterator;
6396 if (this.reverse_) {
6397 iterator = newSnap.getReverseIteratorFrom(this.rangedFilter_.getEndPost(), this.index_);
6398 }
6399 else {
6400 iterator = newSnap.getIteratorFrom(this.rangedFilter_.getStartPost(), this.index_);
6401 }
6402 let count = 0;
6403 while (iterator.hasNext() && count < this.limit_) {
6404 const next = iterator.getNext();
6405 let inRange;
6406 if (this.reverse_) {
6407 inRange =
6408 this.index_.compare(this.rangedFilter_.getStartPost(), next) <= 0;
6409 }
6410 else {
6411 inRange =
6412 this.index_.compare(next, this.rangedFilter_.getEndPost()) <= 0;
6413 }
6414 if (inRange) {
6415 filtered = filtered.updateImmediateChild(next.name, next.node);
6416 count++;
6417 }
6418 else {
6419 // if we have reached the end post, we cannot keep adding elemments
6420 break;
6421 }
6422 }
6423 }
6424 else {
6425 // The snap contains less than twice the limit. Faster to delete from the snap than build up a new one
6426 filtered = newSnap.withIndex(this.index_);
6427 // Don't support priorities on queries
6428 filtered = filtered.updatePriority(ChildrenNode.EMPTY_NODE);
6429 let startPost;
6430 let endPost;
6431 let cmp;
6432 let iterator;
6433 if (this.reverse_) {
6434 iterator = filtered.getReverseIterator(this.index_);
6435 startPost = this.rangedFilter_.getEndPost();
6436 endPost = this.rangedFilter_.getStartPost();
6437 const indexCompare = this.index_.getCompare();
6438 cmp = (a, b) => indexCompare(b, a);
6439 }
6440 else {
6441 iterator = filtered.getIterator(this.index_);
6442 startPost = this.rangedFilter_.getStartPost();
6443 endPost = this.rangedFilter_.getEndPost();
6444 cmp = this.index_.getCompare();
6445 }
6446 let count = 0;
6447 let foundStartPost = false;
6448 while (iterator.hasNext()) {
6449 const next = iterator.getNext();
6450 if (!foundStartPost && cmp(startPost, next) <= 0) {
6451 // start adding
6452 foundStartPost = true;
6453 }
6454 const inRange = foundStartPost && count < this.limit_ && cmp(next, endPost) <= 0;
6455 if (inRange) {
6456 count++;
6457 }
6458 else {
6459 filtered = filtered.updateImmediateChild(next.name, ChildrenNode.EMPTY_NODE);
6460 }
6461 }
6462 }
6463 }
6464 return this.rangedFilter_
6465 .getIndexedFilter()
6466 .updateFullNode(oldSnap, filtered, optChangeAccumulator);
6467 }
6468 updatePriority(oldSnap, newPriority) {
6469 // Don't support priorities on queries
6470 return oldSnap;
6471 }
6472 filtersNodes() {
6473 return true;
6474 }
6475 getIndexedFilter() {
6476 return this.rangedFilter_.getIndexedFilter();
6477 }
6478 getIndex() {
6479 return this.index_;
6480 }
6481 fullLimitUpdateChild_(snap, childKey, childSnap, source, changeAccumulator) {
6482 // TODO: rename all cache stuff etc to general snap terminology
6483 let cmp;
6484 if (this.reverse_) {
6485 const indexCmp = this.index_.getCompare();
6486 cmp = (a, b) => indexCmp(b, a);
6487 }
6488 else {
6489 cmp = this.index_.getCompare();
6490 }
6491 const oldEventCache = snap;
6492 assert(oldEventCache.numChildren() === this.limit_, '');
6493 const newChildNamedNode = new NamedNode(childKey, childSnap);
6494 const windowBoundary = this.reverse_
6495 ? oldEventCache.getFirstChild(this.index_)
6496 : oldEventCache.getLastChild(this.index_);
6497 const inRange = this.rangedFilter_.matches(newChildNamedNode);
6498 if (oldEventCache.hasChild(childKey)) {
6499 const oldChildSnap = oldEventCache.getImmediateChild(childKey);
6500 let nextChild = source.getChildAfterChild(this.index_, windowBoundary, this.reverse_);
6501 while (nextChild != null &&
6502 (nextChild.name === childKey || oldEventCache.hasChild(nextChild.name))) {
6503 // There is a weird edge case where a node is updated as part of a merge in the write tree, but hasn't
6504 // been applied to the limited filter yet. Ignore this next child which will be updated later in
6505 // the limited filter...
6506 nextChild = source.getChildAfterChild(this.index_, nextChild, this.reverse_);
6507 }
6508 const compareNext = nextChild == null ? 1 : cmp(nextChild, newChildNamedNode);
6509 const remainsInWindow = inRange && !childSnap.isEmpty() && compareNext >= 0;
6510 if (remainsInWindow) {
6511 if (changeAccumulator != null) {
6512 changeAccumulator.trackChildChange(changeChildChanged(childKey, childSnap, oldChildSnap));
6513 }
6514 return oldEventCache.updateImmediateChild(childKey, childSnap);
6515 }
6516 else {
6517 if (changeAccumulator != null) {
6518 changeAccumulator.trackChildChange(changeChildRemoved(childKey, oldChildSnap));
6519 }
6520 const newEventCache = oldEventCache.updateImmediateChild(childKey, ChildrenNode.EMPTY_NODE);
6521 const nextChildInRange = nextChild != null && this.rangedFilter_.matches(nextChild);
6522 if (nextChildInRange) {
6523 if (changeAccumulator != null) {
6524 changeAccumulator.trackChildChange(changeChildAdded(nextChild.name, nextChild.node));
6525 }
6526 return newEventCache.updateImmediateChild(nextChild.name, nextChild.node);
6527 }
6528 else {
6529 return newEventCache;
6530 }
6531 }
6532 }
6533 else if (childSnap.isEmpty()) {
6534 // we're deleting a node, but it was not in the window, so ignore it
6535 return snap;
6536 }
6537 else if (inRange) {
6538 if (cmp(windowBoundary, newChildNamedNode) >= 0) {
6539 if (changeAccumulator != null) {
6540 changeAccumulator.trackChildChange(changeChildRemoved(windowBoundary.name, windowBoundary.node));
6541 changeAccumulator.trackChildChange(changeChildAdded(childKey, childSnap));
6542 }
6543 return oldEventCache
6544 .updateImmediateChild(childKey, childSnap)
6545 .updateImmediateChild(windowBoundary.name, ChildrenNode.EMPTY_NODE);
6546 }
6547 else {
6548 return snap;
6549 }
6550 }
6551 else {
6552 return snap;
6553 }
6554 }
6555}
6556
6557/**
6558 * @license
6559 * Copyright 2017 Google LLC
6560 *
6561 * Licensed under the Apache License, Version 2.0 (the "License");
6562 * you may not use this file except in compliance with the License.
6563 * You may obtain a copy of the License at
6564 *
6565 * http://www.apache.org/licenses/LICENSE-2.0
6566 *
6567 * Unless required by applicable law or agreed to in writing, software
6568 * distributed under the License is distributed on an "AS IS" BASIS,
6569 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
6570 * See the License for the specific language governing permissions and
6571 * limitations under the License.
6572 */
6573/**
6574 * This class is an immutable-from-the-public-api struct containing a set of query parameters defining a
6575 * range to be returned for a particular location. It is assumed that validation of parameters is done at the
6576 * user-facing API level, so it is not done here.
6577 *
6578 * @internal
6579 */
6580class QueryParams {
6581 constructor() {
6582 this.limitSet_ = false;
6583 this.startSet_ = false;
6584 this.startNameSet_ = false;
6585 this.startAfterSet_ = false;
6586 this.endSet_ = false;
6587 this.endNameSet_ = false;
6588 this.endBeforeSet_ = false;
6589 this.limit_ = 0;
6590 this.viewFrom_ = '';
6591 this.indexStartValue_ = null;
6592 this.indexStartName_ = '';
6593 this.indexEndValue_ = null;
6594 this.indexEndName_ = '';
6595 this.index_ = PRIORITY_INDEX;
6596 }
6597 hasStart() {
6598 return this.startSet_;
6599 }
6600 hasStartAfter() {
6601 return this.startAfterSet_;
6602 }
6603 hasEndBefore() {
6604 return this.endBeforeSet_;
6605 }
6606 /**
6607 * @returns True if it would return from left.
6608 */
6609 isViewFromLeft() {
6610 if (this.viewFrom_ === '') {
6611 // limit(), rather than limitToFirst or limitToLast was called.
6612 // This means that only one of startSet_ and endSet_ is true. Use them
6613 // to calculate which side of the view to anchor to. If neither is set,
6614 // anchor to the end.
6615 return this.startSet_;
6616 }
6617 else {
6618 return this.viewFrom_ === "l" /* VIEW_FROM_LEFT */;
6619 }
6620 }
6621 /**
6622 * Only valid to call if hasStart() returns true
6623 */
6624 getIndexStartValue() {
6625 assert(this.startSet_, 'Only valid if start has been set');
6626 return this.indexStartValue_;
6627 }
6628 /**
6629 * Only valid to call if hasStart() returns true.
6630 * Returns the starting key name for the range defined by these query parameters
6631 */
6632 getIndexStartName() {
6633 assert(this.startSet_, 'Only valid if start has been set');
6634 if (this.startNameSet_) {
6635 return this.indexStartName_;
6636 }
6637 else {
6638 return MIN_NAME;
6639 }
6640 }
6641 hasEnd() {
6642 return this.endSet_;
6643 }
6644 /**
6645 * Only valid to call if hasEnd() returns true.
6646 */
6647 getIndexEndValue() {
6648 assert(this.endSet_, 'Only valid if end has been set');
6649 return this.indexEndValue_;
6650 }
6651 /**
6652 * Only valid to call if hasEnd() returns true.
6653 * Returns the end key name for the range defined by these query parameters
6654 */
6655 getIndexEndName() {
6656 assert(this.endSet_, 'Only valid if end has been set');
6657 if (this.endNameSet_) {
6658 return this.indexEndName_;
6659 }
6660 else {
6661 return MAX_NAME;
6662 }
6663 }
6664 hasLimit() {
6665 return this.limitSet_;
6666 }
6667 /**
6668 * @returns True if a limit has been set and it has been explicitly anchored
6669 */
6670 hasAnchoredLimit() {
6671 return this.limitSet_ && this.viewFrom_ !== '';
6672 }
6673 /**
6674 * Only valid to call if hasLimit() returns true
6675 */
6676 getLimit() {
6677 assert(this.limitSet_, 'Only valid if limit has been set');
6678 return this.limit_;
6679 }
6680 getIndex() {
6681 return this.index_;
6682 }
6683 loadsAllData() {
6684 return !(this.startSet_ || this.endSet_ || this.limitSet_);
6685 }
6686 isDefault() {
6687 return this.loadsAllData() && this.index_ === PRIORITY_INDEX;
6688 }
6689 copy() {
6690 const copy = new QueryParams();
6691 copy.limitSet_ = this.limitSet_;
6692 copy.limit_ = this.limit_;
6693 copy.startSet_ = this.startSet_;
6694 copy.indexStartValue_ = this.indexStartValue_;
6695 copy.startNameSet_ = this.startNameSet_;
6696 copy.indexStartName_ = this.indexStartName_;
6697 copy.endSet_ = this.endSet_;
6698 copy.indexEndValue_ = this.indexEndValue_;
6699 copy.endNameSet_ = this.endNameSet_;
6700 copy.indexEndName_ = this.indexEndName_;
6701 copy.index_ = this.index_;
6702 copy.viewFrom_ = this.viewFrom_;
6703 return copy;
6704 }
6705}
6706function queryParamsGetNodeFilter(queryParams) {
6707 if (queryParams.loadsAllData()) {
6708 return new IndexedFilter(queryParams.getIndex());
6709 }
6710 else if (queryParams.hasLimit()) {
6711 return new LimitedFilter(queryParams);
6712 }
6713 else {
6714 return new RangedFilter(queryParams);
6715 }
6716}
6717function queryParamsLimitToFirst(queryParams, newLimit) {
6718 const newParams = queryParams.copy();
6719 newParams.limitSet_ = true;
6720 newParams.limit_ = newLimit;
6721 newParams.viewFrom_ = "l" /* VIEW_FROM_LEFT */;
6722 return newParams;
6723}
6724function queryParamsLimitToLast(queryParams, newLimit) {
6725 const newParams = queryParams.copy();
6726 newParams.limitSet_ = true;
6727 newParams.limit_ = newLimit;
6728 newParams.viewFrom_ = "r" /* VIEW_FROM_RIGHT */;
6729 return newParams;
6730}
6731function queryParamsStartAt(queryParams, indexValue, key) {
6732 const newParams = queryParams.copy();
6733 newParams.startSet_ = true;
6734 if (indexValue === undefined) {
6735 indexValue = null;
6736 }
6737 newParams.indexStartValue_ = indexValue;
6738 if (key != null) {
6739 newParams.startNameSet_ = true;
6740 newParams.indexStartName_ = key;
6741 }
6742 else {
6743 newParams.startNameSet_ = false;
6744 newParams.indexStartName_ = '';
6745 }
6746 return newParams;
6747}
6748function queryParamsStartAfter(queryParams, indexValue, key) {
6749 let params;
6750 if (queryParams.index_ === KEY_INDEX) {
6751 if (typeof indexValue === 'string') {
6752 indexValue = successor(indexValue);
6753 }
6754 params = queryParamsStartAt(queryParams, indexValue, key);
6755 }
6756 else {
6757 let childKey;
6758 if (key == null) {
6759 childKey = MAX_NAME;
6760 }
6761 else {
6762 childKey = successor(key);
6763 }
6764 params = queryParamsStartAt(queryParams, indexValue, childKey);
6765 }
6766 params.startAfterSet_ = true;
6767 return params;
6768}
6769function queryParamsEndAt(queryParams, indexValue, key) {
6770 const newParams = queryParams.copy();
6771 newParams.endSet_ = true;
6772 if (indexValue === undefined) {
6773 indexValue = null;
6774 }
6775 newParams.indexEndValue_ = indexValue;
6776 if (key !== undefined) {
6777 newParams.endNameSet_ = true;
6778 newParams.indexEndName_ = key;
6779 }
6780 else {
6781 newParams.endNameSet_ = false;
6782 newParams.indexEndName_ = '';
6783 }
6784 return newParams;
6785}
6786function queryParamsEndBefore(queryParams, indexValue, key) {
6787 let childKey;
6788 let params;
6789 if (queryParams.index_ === KEY_INDEX) {
6790 if (typeof indexValue === 'string') {
6791 indexValue = predecessor(indexValue);
6792 }
6793 params = queryParamsEndAt(queryParams, indexValue, key);
6794 }
6795 else {
6796 if (key == null) {
6797 childKey = MIN_NAME;
6798 }
6799 else {
6800 childKey = predecessor(key);
6801 }
6802 params = queryParamsEndAt(queryParams, indexValue, childKey);
6803 }
6804 params.endBeforeSet_ = true;
6805 return params;
6806}
6807function queryParamsOrderBy(queryParams, index) {
6808 const newParams = queryParams.copy();
6809 newParams.index_ = index;
6810 return newParams;
6811}
6812/**
6813 * Returns a set of REST query string parameters representing this query.
6814 *
6815 * @returns query string parameters
6816 */
6817function queryParamsToRestQueryStringParameters(queryParams) {
6818 const qs = {};
6819 if (queryParams.isDefault()) {
6820 return qs;
6821 }
6822 let orderBy;
6823 if (queryParams.index_ === PRIORITY_INDEX) {
6824 orderBy = "$priority" /* PRIORITY_INDEX */;
6825 }
6826 else if (queryParams.index_ === VALUE_INDEX) {
6827 orderBy = "$value" /* VALUE_INDEX */;
6828 }
6829 else if (queryParams.index_ === KEY_INDEX) {
6830 orderBy = "$key" /* KEY_INDEX */;
6831 }
6832 else {
6833 assert(queryParams.index_ instanceof PathIndex, 'Unrecognized index type!');
6834 orderBy = queryParams.index_.toString();
6835 }
6836 qs["orderBy" /* ORDER_BY */] = stringify(orderBy);
6837 if (queryParams.startSet_) {
6838 qs["startAt" /* START_AT */] = stringify(queryParams.indexStartValue_);
6839 if (queryParams.startNameSet_) {
6840 qs["startAt" /* START_AT */] +=
6841 ',' + stringify(queryParams.indexStartName_);
6842 }
6843 }
6844 if (queryParams.endSet_) {
6845 qs["endAt" /* END_AT */] = stringify(queryParams.indexEndValue_);
6846 if (queryParams.endNameSet_) {
6847 qs["endAt" /* END_AT */] +=
6848 ',' + stringify(queryParams.indexEndName_);
6849 }
6850 }
6851 if (queryParams.limitSet_) {
6852 if (queryParams.isViewFromLeft()) {
6853 qs["limitToFirst" /* LIMIT_TO_FIRST */] = queryParams.limit_;
6854 }
6855 else {
6856 qs["limitToLast" /* LIMIT_TO_LAST */] = queryParams.limit_;
6857 }
6858 }
6859 return qs;
6860}
6861function queryParamsGetQueryObject(queryParams) {
6862 const obj = {};
6863 if (queryParams.startSet_) {
6864 obj["sp" /* INDEX_START_VALUE */] =
6865 queryParams.indexStartValue_;
6866 if (queryParams.startNameSet_) {
6867 obj["sn" /* INDEX_START_NAME */] =
6868 queryParams.indexStartName_;
6869 }
6870 }
6871 if (queryParams.endSet_) {
6872 obj["ep" /* INDEX_END_VALUE */] = queryParams.indexEndValue_;
6873 if (queryParams.endNameSet_) {
6874 obj["en" /* INDEX_END_NAME */] = queryParams.indexEndName_;
6875 }
6876 }
6877 if (queryParams.limitSet_) {
6878 obj["l" /* LIMIT */] = queryParams.limit_;
6879 let viewFrom = queryParams.viewFrom_;
6880 if (viewFrom === '') {
6881 if (queryParams.isViewFromLeft()) {
6882 viewFrom = "l" /* VIEW_FROM_LEFT */;
6883 }
6884 else {
6885 viewFrom = "r" /* VIEW_FROM_RIGHT */;
6886 }
6887 }
6888 obj["vf" /* VIEW_FROM */] = viewFrom;
6889 }
6890 // For now, priority index is the default, so we only specify if it's some other index
6891 if (queryParams.index_ !== PRIORITY_INDEX) {
6892 obj["i" /* INDEX */] = queryParams.index_.toString();
6893 }
6894 return obj;
6895}
6896
6897/**
6898 * @license
6899 * Copyright 2017 Google LLC
6900 *
6901 * Licensed under the Apache License, Version 2.0 (the "License");
6902 * you may not use this file except in compliance with the License.
6903 * You may obtain a copy of the License at
6904 *
6905 * http://www.apache.org/licenses/LICENSE-2.0
6906 *
6907 * Unless required by applicable law or agreed to in writing, software
6908 * distributed under the License is distributed on an "AS IS" BASIS,
6909 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
6910 * See the License for the specific language governing permissions and
6911 * limitations under the License.
6912 */
6913/**
6914 * An implementation of ServerActions that communicates with the server via REST requests.
6915 * This is mostly useful for compatibility with crawlers, where we don't want to spin up a full
6916 * persistent connection (using WebSockets or long-polling)
6917 */
6918class ReadonlyRestClient extends ServerActions {
6919 /**
6920 * @param repoInfo_ - Data about the namespace we are connecting to
6921 * @param onDataUpdate_ - A callback for new data from the server
6922 */
6923 constructor(repoInfo_, onDataUpdate_, authTokenProvider_, appCheckTokenProvider_) {
6924 super();
6925 this.repoInfo_ = repoInfo_;
6926 this.onDataUpdate_ = onDataUpdate_;
6927 this.authTokenProvider_ = authTokenProvider_;
6928 this.appCheckTokenProvider_ = appCheckTokenProvider_;
6929 /** @private {function(...[*])} */
6930 this.log_ = logWrapper('p:rest:');
6931 /**
6932 * We don't actually need to track listens, except to prevent us calling an onComplete for a listen
6933 * that's been removed. :-/
6934 */
6935 this.listens_ = {};
6936 }
6937 reportStats(stats) {
6938 throw new Error('Method not implemented.');
6939 }
6940 static getListenId_(query, tag) {
6941 if (tag !== undefined) {
6942 return 'tag$' + tag;
6943 }
6944 else {
6945 assert(query._queryParams.isDefault(), "should have a tag if it's not a default query.");
6946 return query._path.toString();
6947 }
6948 }
6949 /** @inheritDoc */
6950 listen(query, currentHashFn, tag, onComplete) {
6951 const pathString = query._path.toString();
6952 this.log_('Listen called for ' + pathString + ' ' + query._queryIdentifier);
6953 // Mark this listener so we can tell if it's removed.
6954 const listenId = ReadonlyRestClient.getListenId_(query, tag);
6955 const thisListen = {};
6956 this.listens_[listenId] = thisListen;
6957 const queryStringParameters = queryParamsToRestQueryStringParameters(query._queryParams);
6958 this.restRequest_(pathString + '.json', queryStringParameters, (error, result) => {
6959 let data = result;
6960 if (error === 404) {
6961 data = null;
6962 error = null;
6963 }
6964 if (error === null) {
6965 this.onDataUpdate_(pathString, data, /*isMerge=*/ false, tag);
6966 }
6967 if (safeGet(this.listens_, listenId) === thisListen) {
6968 let status;
6969 if (!error) {
6970 status = 'ok';
6971 }
6972 else if (error === 401) {
6973 status = 'permission_denied';
6974 }
6975 else {
6976 status = 'rest_error:' + error;
6977 }
6978 onComplete(status, null);
6979 }
6980 });
6981 }
6982 /** @inheritDoc */
6983 unlisten(query, tag) {
6984 const listenId = ReadonlyRestClient.getListenId_(query, tag);
6985 delete this.listens_[listenId];
6986 }
6987 get(query) {
6988 const queryStringParameters = queryParamsToRestQueryStringParameters(query._queryParams);
6989 const pathString = query._path.toString();
6990 const deferred = new Deferred();
6991 this.restRequest_(pathString + '.json', queryStringParameters, (error, result) => {
6992 let data = result;
6993 if (error === 404) {
6994 data = null;
6995 error = null;
6996 }
6997 if (error === null) {
6998 this.onDataUpdate_(pathString, data,
6999 /*isMerge=*/ false,
7000 /*tag=*/ null);
7001 deferred.resolve(data);
7002 }
7003 else {
7004 deferred.reject(new Error(data));
7005 }
7006 });
7007 return deferred.promise;
7008 }
7009 /** @inheritDoc */
7010 refreshAuthToken(token) {
7011 // no-op since we just always call getToken.
7012 }
7013 /**
7014 * Performs a REST request to the given path, with the provided query string parameters,
7015 * and any auth credentials we have.
7016 */
7017 restRequest_(pathString, queryStringParameters = {}, callback) {
7018 queryStringParameters['format'] = 'export';
7019 return Promise.all([
7020 this.authTokenProvider_.getToken(/*forceRefresh=*/ false),
7021 this.appCheckTokenProvider_.getToken(/*forceRefresh=*/ false)
7022 ]).then(([authToken, appCheckToken]) => {
7023 if (authToken && authToken.accessToken) {
7024 queryStringParameters['auth'] = authToken.accessToken;
7025 }
7026 if (appCheckToken && appCheckToken.token) {
7027 queryStringParameters['ac'] = appCheckToken.token;
7028 }
7029 const url = (this.repoInfo_.secure ? 'https://' : 'http://') +
7030 this.repoInfo_.host +
7031 pathString +
7032 '?' +
7033 'ns=' +
7034 this.repoInfo_.namespace +
7035 querystring(queryStringParameters);
7036 this.log_('Sending REST request for ' + url);
7037 const xhr = new XMLHttpRequest();
7038 xhr.onreadystatechange = () => {
7039 if (callback && xhr.readyState === 4) {
7040 this.log_('REST Response for ' + url + ' received. status:', xhr.status, 'response:', xhr.responseText);
7041 let res = null;
7042 if (xhr.status >= 200 && xhr.status < 300) {
7043 try {
7044 res = jsonEval(xhr.responseText);
7045 }
7046 catch (e) {
7047 warn('Failed to parse JSON response for ' +
7048 url +
7049 ': ' +
7050 xhr.responseText);
7051 }
7052 callback(null, res);
7053 }
7054 else {
7055 // 401 and 404 are expected.
7056 if (xhr.status !== 401 && xhr.status !== 404) {
7057 warn('Got unsuccessful REST response for ' +
7058 url +
7059 ' Status: ' +
7060 xhr.status);
7061 }
7062 callback(xhr.status);
7063 }
7064 callback = null;
7065 }
7066 };
7067 xhr.open('GET', url, /*asynchronous=*/ true);
7068 xhr.send();
7069 });
7070 }
7071}
7072
7073/**
7074 * @license
7075 * Copyright 2017 Google LLC
7076 *
7077 * Licensed under the Apache License, Version 2.0 (the "License");
7078 * you may not use this file except in compliance with the License.
7079 * You may obtain a copy of the License at
7080 *
7081 * http://www.apache.org/licenses/LICENSE-2.0
7082 *
7083 * Unless required by applicable law or agreed to in writing, software
7084 * distributed under the License is distributed on an "AS IS" BASIS,
7085 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7086 * See the License for the specific language governing permissions and
7087 * limitations under the License.
7088 */
7089/**
7090 * Mutable object which basically just stores a reference to the "latest" immutable snapshot.
7091 */
7092class SnapshotHolder {
7093 constructor() {
7094 this.rootNode_ = ChildrenNode.EMPTY_NODE;
7095 }
7096 getNode(path) {
7097 return this.rootNode_.getChild(path);
7098 }
7099 updateSnapshot(path, newSnapshotNode) {
7100 this.rootNode_ = this.rootNode_.updateChild(path, newSnapshotNode);
7101 }
7102}
7103
7104/**
7105 * @license
7106 * Copyright 2017 Google LLC
7107 *
7108 * Licensed under the Apache License, Version 2.0 (the "License");
7109 * you may not use this file except in compliance with the License.
7110 * You may obtain a copy of the License at
7111 *
7112 * http://www.apache.org/licenses/LICENSE-2.0
7113 *
7114 * Unless required by applicable law or agreed to in writing, software
7115 * distributed under the License is distributed on an "AS IS" BASIS,
7116 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7117 * See the License for the specific language governing permissions and
7118 * limitations under the License.
7119 */
7120function newSparseSnapshotTree() {
7121 return {
7122 value: null,
7123 children: new Map()
7124 };
7125}
7126/**
7127 * Stores the given node at the specified path. If there is already a node
7128 * at a shallower path, it merges the new data into that snapshot node.
7129 *
7130 * @param path - Path to look up snapshot for.
7131 * @param data - The new data, or null.
7132 */
7133function sparseSnapshotTreeRemember(sparseSnapshotTree, path, data) {
7134 if (pathIsEmpty(path)) {
7135 sparseSnapshotTree.value = data;
7136 sparseSnapshotTree.children.clear();
7137 }
7138 else if (sparseSnapshotTree.value !== null) {
7139 sparseSnapshotTree.value = sparseSnapshotTree.value.updateChild(path, data);
7140 }
7141 else {
7142 const childKey = pathGetFront(path);
7143 if (!sparseSnapshotTree.children.has(childKey)) {
7144 sparseSnapshotTree.children.set(childKey, newSparseSnapshotTree());
7145 }
7146 const child = sparseSnapshotTree.children.get(childKey);
7147 path = pathPopFront(path);
7148 sparseSnapshotTreeRemember(child, path, data);
7149 }
7150}
7151/**
7152 * Purge the data at path from the cache.
7153 *
7154 * @param path - Path to look up snapshot for.
7155 * @returns True if this node should now be removed.
7156 */
7157function sparseSnapshotTreeForget(sparseSnapshotTree, path) {
7158 if (pathIsEmpty(path)) {
7159 sparseSnapshotTree.value = null;
7160 sparseSnapshotTree.children.clear();
7161 return true;
7162 }
7163 else {
7164 if (sparseSnapshotTree.value !== null) {
7165 if (sparseSnapshotTree.value.isLeafNode()) {
7166 // We're trying to forget a node that doesn't exist
7167 return false;
7168 }
7169 else {
7170 const value = sparseSnapshotTree.value;
7171 sparseSnapshotTree.value = null;
7172 value.forEachChild(PRIORITY_INDEX, (key, tree) => {
7173 sparseSnapshotTreeRemember(sparseSnapshotTree, new Path(key), tree);
7174 });
7175 return sparseSnapshotTreeForget(sparseSnapshotTree, path);
7176 }
7177 }
7178 else if (sparseSnapshotTree.children.size > 0) {
7179 const childKey = pathGetFront(path);
7180 path = pathPopFront(path);
7181 if (sparseSnapshotTree.children.has(childKey)) {
7182 const safeToRemove = sparseSnapshotTreeForget(sparseSnapshotTree.children.get(childKey), path);
7183 if (safeToRemove) {
7184 sparseSnapshotTree.children.delete(childKey);
7185 }
7186 }
7187 return sparseSnapshotTree.children.size === 0;
7188 }
7189 else {
7190 return true;
7191 }
7192 }
7193}
7194/**
7195 * Recursively iterates through all of the stored tree and calls the
7196 * callback on each one.
7197 *
7198 * @param prefixPath - Path to look up node for.
7199 * @param func - The function to invoke for each tree.
7200 */
7201function sparseSnapshotTreeForEachTree(sparseSnapshotTree, prefixPath, func) {
7202 if (sparseSnapshotTree.value !== null) {
7203 func(prefixPath, sparseSnapshotTree.value);
7204 }
7205 else {
7206 sparseSnapshotTreeForEachChild(sparseSnapshotTree, (key, tree) => {
7207 const path = new Path(prefixPath.toString() + '/' + key);
7208 sparseSnapshotTreeForEachTree(tree, path, func);
7209 });
7210 }
7211}
7212/**
7213 * Iterates through each immediate child and triggers the callback.
7214 * Only seems to be used in tests.
7215 *
7216 * @param func - The function to invoke for each child.
7217 */
7218function sparseSnapshotTreeForEachChild(sparseSnapshotTree, func) {
7219 sparseSnapshotTree.children.forEach((tree, key) => {
7220 func(key, tree);
7221 });
7222}
7223
7224/**
7225 * @license
7226 * Copyright 2017 Google LLC
7227 *
7228 * Licensed under the Apache License, Version 2.0 (the "License");
7229 * you may not use this file except in compliance with the License.
7230 * You may obtain a copy of the License at
7231 *
7232 * http://www.apache.org/licenses/LICENSE-2.0
7233 *
7234 * Unless required by applicable law or agreed to in writing, software
7235 * distributed under the License is distributed on an "AS IS" BASIS,
7236 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7237 * See the License for the specific language governing permissions and
7238 * limitations under the License.
7239 */
7240/**
7241 * Returns the delta from the previous call to get stats.
7242 *
7243 * @param collection_ - The collection to "listen" to.
7244 */
7245class StatsListener {
7246 constructor(collection_) {
7247 this.collection_ = collection_;
7248 this.last_ = null;
7249 }
7250 get() {
7251 const newStats = this.collection_.get();
7252 const delta = Object.assign({}, newStats);
7253 if (this.last_) {
7254 each(this.last_, (stat, value) => {
7255 delta[stat] = delta[stat] - value;
7256 });
7257 }
7258 this.last_ = newStats;
7259 return delta;
7260 }
7261}
7262
7263/**
7264 * @license
7265 * Copyright 2017 Google LLC
7266 *
7267 * Licensed under the Apache License, Version 2.0 (the "License");
7268 * you may not use this file except in compliance with the License.
7269 * You may obtain a copy of the License at
7270 *
7271 * http://www.apache.org/licenses/LICENSE-2.0
7272 *
7273 * Unless required by applicable law or agreed to in writing, software
7274 * distributed under the License is distributed on an "AS IS" BASIS,
7275 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7276 * See the License for the specific language governing permissions and
7277 * limitations under the License.
7278 */
7279// Assuming some apps may have a short amount of time on page, and a bulk of firebase operations probably
7280// happen on page load, we try to report our first set of stats pretty quickly, but we wait at least 10
7281// seconds to try to ensure the Firebase connection is established / settled.
7282const FIRST_STATS_MIN_TIME = 10 * 1000;
7283const FIRST_STATS_MAX_TIME = 30 * 1000;
7284// We'll continue to report stats on average every 5 minutes.
7285const REPORT_STATS_INTERVAL = 5 * 60 * 1000;
7286class StatsReporter {
7287 constructor(collection, server_) {
7288 this.server_ = server_;
7289 this.statsToReport_ = {};
7290 this.statsListener_ = new StatsListener(collection);
7291 const timeout = FIRST_STATS_MIN_TIME +
7292 (FIRST_STATS_MAX_TIME - FIRST_STATS_MIN_TIME) * Math.random();
7293 setTimeoutNonBlocking(this.reportStats_.bind(this), Math.floor(timeout));
7294 }
7295 reportStats_() {
7296 const stats = this.statsListener_.get();
7297 const reportedStats = {};
7298 let haveStatsToReport = false;
7299 each(stats, (stat, value) => {
7300 if (value > 0 && contains(this.statsToReport_, stat)) {
7301 reportedStats[stat] = value;
7302 haveStatsToReport = true;
7303 }
7304 });
7305 if (haveStatsToReport) {
7306 this.server_.reportStats(reportedStats);
7307 }
7308 // queue our next run.
7309 setTimeoutNonBlocking(this.reportStats_.bind(this), Math.floor(Math.random() * 2 * REPORT_STATS_INTERVAL));
7310 }
7311}
7312
7313/**
7314 * @license
7315 * Copyright 2017 Google LLC
7316 *
7317 * Licensed under the Apache License, Version 2.0 (the "License");
7318 * you may not use this file except in compliance with the License.
7319 * You may obtain a copy of the License at
7320 *
7321 * http://www.apache.org/licenses/LICENSE-2.0
7322 *
7323 * Unless required by applicable law or agreed to in writing, software
7324 * distributed under the License is distributed on an "AS IS" BASIS,
7325 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7326 * See the License for the specific language governing permissions and
7327 * limitations under the License.
7328 */
7329/**
7330 *
7331 * @enum
7332 */
7333var OperationType;
7334(function (OperationType) {
7335 OperationType[OperationType["OVERWRITE"] = 0] = "OVERWRITE";
7336 OperationType[OperationType["MERGE"] = 1] = "MERGE";
7337 OperationType[OperationType["ACK_USER_WRITE"] = 2] = "ACK_USER_WRITE";
7338 OperationType[OperationType["LISTEN_COMPLETE"] = 3] = "LISTEN_COMPLETE";
7339})(OperationType || (OperationType = {}));
7340function newOperationSourceUser() {
7341 return {
7342 fromUser: true,
7343 fromServer: false,
7344 queryId: null,
7345 tagged: false
7346 };
7347}
7348function newOperationSourceServer() {
7349 return {
7350 fromUser: false,
7351 fromServer: true,
7352 queryId: null,
7353 tagged: false
7354 };
7355}
7356function newOperationSourceServerTaggedQuery(queryId) {
7357 return {
7358 fromUser: false,
7359 fromServer: true,
7360 queryId,
7361 tagged: true
7362 };
7363}
7364
7365/**
7366 * @license
7367 * Copyright 2017 Google LLC
7368 *
7369 * Licensed under the Apache License, Version 2.0 (the "License");
7370 * you may not use this file except in compliance with the License.
7371 * You may obtain a copy of the License at
7372 *
7373 * http://www.apache.org/licenses/LICENSE-2.0
7374 *
7375 * Unless required by applicable law or agreed to in writing, software
7376 * distributed under the License is distributed on an "AS IS" BASIS,
7377 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7378 * See the License for the specific language governing permissions and
7379 * limitations under the License.
7380 */
7381class AckUserWrite {
7382 /**
7383 * @param affectedTree - A tree containing true for each affected path. Affected paths can't overlap.
7384 */
7385 constructor(
7386 /** @inheritDoc */ path,
7387 /** @inheritDoc */ affectedTree,
7388 /** @inheritDoc */ revert) {
7389 this.path = path;
7390 this.affectedTree = affectedTree;
7391 this.revert = revert;
7392 /** @inheritDoc */
7393 this.type = OperationType.ACK_USER_WRITE;
7394 /** @inheritDoc */
7395 this.source = newOperationSourceUser();
7396 }
7397 operationForChild(childName) {
7398 if (!pathIsEmpty(this.path)) {
7399 assert(pathGetFront(this.path) === childName, 'operationForChild called for unrelated child.');
7400 return new AckUserWrite(pathPopFront(this.path), this.affectedTree, this.revert);
7401 }
7402 else if (this.affectedTree.value != null) {
7403 assert(this.affectedTree.children.isEmpty(), 'affectedTree should not have overlapping affected paths.');
7404 // All child locations are affected as well; just return same operation.
7405 return this;
7406 }
7407 else {
7408 const childTree = this.affectedTree.subtree(new Path(childName));
7409 return new AckUserWrite(newEmptyPath(), childTree, this.revert);
7410 }
7411 }
7412}
7413
7414/**
7415 * @license
7416 * Copyright 2017 Google LLC
7417 *
7418 * Licensed under the Apache License, Version 2.0 (the "License");
7419 * you may not use this file except in compliance with the License.
7420 * You may obtain a copy of the License at
7421 *
7422 * http://www.apache.org/licenses/LICENSE-2.0
7423 *
7424 * Unless required by applicable law or agreed to in writing, software
7425 * distributed under the License is distributed on an "AS IS" BASIS,
7426 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7427 * See the License for the specific language governing permissions and
7428 * limitations under the License.
7429 */
7430class ListenComplete {
7431 constructor(source, path) {
7432 this.source = source;
7433 this.path = path;
7434 /** @inheritDoc */
7435 this.type = OperationType.LISTEN_COMPLETE;
7436 }
7437 operationForChild(childName) {
7438 if (pathIsEmpty(this.path)) {
7439 return new ListenComplete(this.source, newEmptyPath());
7440 }
7441 else {
7442 return new ListenComplete(this.source, pathPopFront(this.path));
7443 }
7444 }
7445}
7446
7447/**
7448 * @license
7449 * Copyright 2017 Google LLC
7450 *
7451 * Licensed under the Apache License, Version 2.0 (the "License");
7452 * you may not use this file except in compliance with the License.
7453 * You may obtain a copy of the License at
7454 *
7455 * http://www.apache.org/licenses/LICENSE-2.0
7456 *
7457 * Unless required by applicable law or agreed to in writing, software
7458 * distributed under the License is distributed on an "AS IS" BASIS,
7459 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7460 * See the License for the specific language governing permissions and
7461 * limitations under the License.
7462 */
7463class Overwrite {
7464 constructor(source, path, snap) {
7465 this.source = source;
7466 this.path = path;
7467 this.snap = snap;
7468 /** @inheritDoc */
7469 this.type = OperationType.OVERWRITE;
7470 }
7471 operationForChild(childName) {
7472 if (pathIsEmpty(this.path)) {
7473 return new Overwrite(this.source, newEmptyPath(), this.snap.getImmediateChild(childName));
7474 }
7475 else {
7476 return new Overwrite(this.source, pathPopFront(this.path), this.snap);
7477 }
7478 }
7479}
7480
7481/**
7482 * @license
7483 * Copyright 2017 Google LLC
7484 *
7485 * Licensed under the Apache License, Version 2.0 (the "License");
7486 * you may not use this file except in compliance with the License.
7487 * You may obtain a copy of the License at
7488 *
7489 * http://www.apache.org/licenses/LICENSE-2.0
7490 *
7491 * Unless required by applicable law or agreed to in writing, software
7492 * distributed under the License is distributed on an "AS IS" BASIS,
7493 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7494 * See the License for the specific language governing permissions and
7495 * limitations under the License.
7496 */
7497class Merge {
7498 constructor(
7499 /** @inheritDoc */ source,
7500 /** @inheritDoc */ path,
7501 /** @inheritDoc */ children) {
7502 this.source = source;
7503 this.path = path;
7504 this.children = children;
7505 /** @inheritDoc */
7506 this.type = OperationType.MERGE;
7507 }
7508 operationForChild(childName) {
7509 if (pathIsEmpty(this.path)) {
7510 const childTree = this.children.subtree(new Path(childName));
7511 if (childTree.isEmpty()) {
7512 // This child is unaffected
7513 return null;
7514 }
7515 else if (childTree.value) {
7516 // We have a snapshot for the child in question. This becomes an overwrite of the child.
7517 return new Overwrite(this.source, newEmptyPath(), childTree.value);
7518 }
7519 else {
7520 // This is a merge at a deeper level
7521 return new Merge(this.source, newEmptyPath(), childTree);
7522 }
7523 }
7524 else {
7525 assert(pathGetFront(this.path) === childName, "Can't get a merge for a child not on the path of the operation");
7526 return new Merge(this.source, pathPopFront(this.path), this.children);
7527 }
7528 }
7529 toString() {
7530 return ('Operation(' +
7531 this.path +
7532 ': ' +
7533 this.source.toString() +
7534 ' merge: ' +
7535 this.children.toString() +
7536 ')');
7537 }
7538}
7539
7540/**
7541 * @license
7542 * Copyright 2017 Google LLC
7543 *
7544 * Licensed under the Apache License, Version 2.0 (the "License");
7545 * you may not use this file except in compliance with the License.
7546 * You may obtain a copy of the License at
7547 *
7548 * http://www.apache.org/licenses/LICENSE-2.0
7549 *
7550 * Unless required by applicable law or agreed to in writing, software
7551 * distributed under the License is distributed on an "AS IS" BASIS,
7552 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7553 * See the License for the specific language governing permissions and
7554 * limitations under the License.
7555 */
7556/**
7557 * A cache node only stores complete children. Additionally it holds a flag whether the node can be considered fully
7558 * initialized in the sense that we know at one point in time this represented a valid state of the world, e.g.
7559 * initialized with data from the server, or a complete overwrite by the client. The filtered flag also tracks
7560 * whether a node potentially had children removed due to a filter.
7561 */
7562class CacheNode {
7563 constructor(node_, fullyInitialized_, filtered_) {
7564 this.node_ = node_;
7565 this.fullyInitialized_ = fullyInitialized_;
7566 this.filtered_ = filtered_;
7567 }
7568 /**
7569 * Returns whether this node was fully initialized with either server data or a complete overwrite by the client
7570 */
7571 isFullyInitialized() {
7572 return this.fullyInitialized_;
7573 }
7574 /**
7575 * Returns whether this node is potentially missing children due to a filter applied to the node
7576 */
7577 isFiltered() {
7578 return this.filtered_;
7579 }
7580 isCompleteForPath(path) {
7581 if (pathIsEmpty(path)) {
7582 return this.isFullyInitialized() && !this.filtered_;
7583 }
7584 const childKey = pathGetFront(path);
7585 return this.isCompleteForChild(childKey);
7586 }
7587 isCompleteForChild(key) {
7588 return ((this.isFullyInitialized() && !this.filtered_) || this.node_.hasChild(key));
7589 }
7590 getNode() {
7591 return this.node_;
7592 }
7593}
7594
7595/**
7596 * @license
7597 * Copyright 2017 Google LLC
7598 *
7599 * Licensed under the Apache License, Version 2.0 (the "License");
7600 * you may not use this file except in compliance with the License.
7601 * You may obtain a copy of the License at
7602 *
7603 * http://www.apache.org/licenses/LICENSE-2.0
7604 *
7605 * Unless required by applicable law or agreed to in writing, software
7606 * distributed under the License is distributed on an "AS IS" BASIS,
7607 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7608 * See the License for the specific language governing permissions and
7609 * limitations under the License.
7610 */
7611/**
7612 * An EventGenerator is used to convert "raw" changes (Change) as computed by the
7613 * CacheDiffer into actual events (Event) that can be raised. See generateEventsForChanges()
7614 * for details.
7615 *
7616 */
7617class EventGenerator {
7618 constructor(query_) {
7619 this.query_ = query_;
7620 this.index_ = this.query_._queryParams.getIndex();
7621 }
7622}
7623/**
7624 * Given a set of raw changes (no moved events and prevName not specified yet), and a set of
7625 * EventRegistrations that should be notified of these changes, generate the actual events to be raised.
7626 *
7627 * Notes:
7628 * - child_moved events will be synthesized at this time for any child_changed events that affect
7629 * our index.
7630 * - prevName will be calculated based on the index ordering.
7631 */
7632function eventGeneratorGenerateEventsForChanges(eventGenerator, changes, eventCache, eventRegistrations) {
7633 const events = [];
7634 const moves = [];
7635 changes.forEach(change => {
7636 if (change.type === "child_changed" /* CHILD_CHANGED */ &&
7637 eventGenerator.index_.indexedValueChanged(change.oldSnap, change.snapshotNode)) {
7638 moves.push(changeChildMoved(change.childName, change.snapshotNode));
7639 }
7640 });
7641 eventGeneratorGenerateEventsForType(eventGenerator, events, "child_removed" /* CHILD_REMOVED */, changes, eventRegistrations, eventCache);
7642 eventGeneratorGenerateEventsForType(eventGenerator, events, "child_added" /* CHILD_ADDED */, changes, eventRegistrations, eventCache);
7643 eventGeneratorGenerateEventsForType(eventGenerator, events, "child_moved" /* CHILD_MOVED */, moves, eventRegistrations, eventCache);
7644 eventGeneratorGenerateEventsForType(eventGenerator, events, "child_changed" /* CHILD_CHANGED */, changes, eventRegistrations, eventCache);
7645 eventGeneratorGenerateEventsForType(eventGenerator, events, "value" /* VALUE */, changes, eventRegistrations, eventCache);
7646 return events;
7647}
7648/**
7649 * Given changes of a single change type, generate the corresponding events.
7650 */
7651function eventGeneratorGenerateEventsForType(eventGenerator, events, eventType, changes, registrations, eventCache) {
7652 const filteredChanges = changes.filter(change => change.type === eventType);
7653 filteredChanges.sort((a, b) => eventGeneratorCompareChanges(eventGenerator, a, b));
7654 filteredChanges.forEach(change => {
7655 const materializedChange = eventGeneratorMaterializeSingleChange(eventGenerator, change, eventCache);
7656 registrations.forEach(registration => {
7657 if (registration.respondsTo(change.type)) {
7658 events.push(registration.createEvent(materializedChange, eventGenerator.query_));
7659 }
7660 });
7661 });
7662}
7663function eventGeneratorMaterializeSingleChange(eventGenerator, change, eventCache) {
7664 if (change.type === 'value' || change.type === 'child_removed') {
7665 return change;
7666 }
7667 else {
7668 change.prevName = eventCache.getPredecessorChildName(change.childName, change.snapshotNode, eventGenerator.index_);
7669 return change;
7670 }
7671}
7672function eventGeneratorCompareChanges(eventGenerator, a, b) {
7673 if (a.childName == null || b.childName == null) {
7674 throw assertionError('Should only compare child_ events.');
7675 }
7676 const aWrapped = new NamedNode(a.childName, a.snapshotNode);
7677 const bWrapped = new NamedNode(b.childName, b.snapshotNode);
7678 return eventGenerator.index_.compare(aWrapped, bWrapped);
7679}
7680
7681/**
7682 * @license
7683 * Copyright 2017 Google LLC
7684 *
7685 * Licensed under the Apache License, Version 2.0 (the "License");
7686 * you may not use this file except in compliance with the License.
7687 * You may obtain a copy of the License at
7688 *
7689 * http://www.apache.org/licenses/LICENSE-2.0
7690 *
7691 * Unless required by applicable law or agreed to in writing, software
7692 * distributed under the License is distributed on an "AS IS" BASIS,
7693 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7694 * See the License for the specific language governing permissions and
7695 * limitations under the License.
7696 */
7697function newViewCache(eventCache, serverCache) {
7698 return { eventCache, serverCache };
7699}
7700function viewCacheUpdateEventSnap(viewCache, eventSnap, complete, filtered) {
7701 return newViewCache(new CacheNode(eventSnap, complete, filtered), viewCache.serverCache);
7702}
7703function viewCacheUpdateServerSnap(viewCache, serverSnap, complete, filtered) {
7704 return newViewCache(viewCache.eventCache, new CacheNode(serverSnap, complete, filtered));
7705}
7706function viewCacheGetCompleteEventSnap(viewCache) {
7707 return viewCache.eventCache.isFullyInitialized()
7708 ? viewCache.eventCache.getNode()
7709 : null;
7710}
7711function viewCacheGetCompleteServerSnap(viewCache) {
7712 return viewCache.serverCache.isFullyInitialized()
7713 ? viewCache.serverCache.getNode()
7714 : null;
7715}
7716
7717/**
7718 * @license
7719 * Copyright 2017 Google LLC
7720 *
7721 * Licensed under the Apache License, Version 2.0 (the "License");
7722 * you may not use this file except in compliance with the License.
7723 * You may obtain a copy of the License at
7724 *
7725 * http://www.apache.org/licenses/LICENSE-2.0
7726 *
7727 * Unless required by applicable law or agreed to in writing, software
7728 * distributed under the License is distributed on an "AS IS" BASIS,
7729 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7730 * See the License for the specific language governing permissions and
7731 * limitations under the License.
7732 */
7733let emptyChildrenSingleton;
7734/**
7735 * Singleton empty children collection.
7736 *
7737 */
7738const EmptyChildren = () => {
7739 if (!emptyChildrenSingleton) {
7740 emptyChildrenSingleton = new SortedMap(stringCompare);
7741 }
7742 return emptyChildrenSingleton;
7743};
7744/**
7745 * A tree with immutable elements.
7746 */
7747class ImmutableTree {
7748 constructor(value, children = EmptyChildren()) {
7749 this.value = value;
7750 this.children = children;
7751 }
7752 static fromObject(obj) {
7753 let tree = new ImmutableTree(null);
7754 each(obj, (childPath, childSnap) => {
7755 tree = tree.set(new Path(childPath), childSnap);
7756 });
7757 return tree;
7758 }
7759 /**
7760 * True if the value is empty and there are no children
7761 */
7762 isEmpty() {
7763 return this.value === null && this.children.isEmpty();
7764 }
7765 /**
7766 * Given a path and predicate, return the first node and the path to that node
7767 * where the predicate returns true.
7768 *
7769 * TODO Do a perf test -- If we're creating a bunch of `{path: value:}`
7770 * objects on the way back out, it may be better to pass down a pathSoFar obj.
7771 *
7772 * @param relativePath - The remainder of the path
7773 * @param predicate - The predicate to satisfy to return a node
7774 */
7775 findRootMostMatchingPathAndValue(relativePath, predicate) {
7776 if (this.value != null && predicate(this.value)) {
7777 return { path: newEmptyPath(), value: this.value };
7778 }
7779 else {
7780 if (pathIsEmpty(relativePath)) {
7781 return null;
7782 }
7783 else {
7784 const front = pathGetFront(relativePath);
7785 const child = this.children.get(front);
7786 if (child !== null) {
7787 const childExistingPathAndValue = child.findRootMostMatchingPathAndValue(pathPopFront(relativePath), predicate);
7788 if (childExistingPathAndValue != null) {
7789 const fullPath = pathChild(new Path(front), childExistingPathAndValue.path);
7790 return { path: fullPath, value: childExistingPathAndValue.value };
7791 }
7792 else {
7793 return null;
7794 }
7795 }
7796 else {
7797 return null;
7798 }
7799 }
7800 }
7801 }
7802 /**
7803 * Find, if it exists, the shortest subpath of the given path that points a defined
7804 * value in the tree
7805 */
7806 findRootMostValueAndPath(relativePath) {
7807 return this.findRootMostMatchingPathAndValue(relativePath, () => true);
7808 }
7809 /**
7810 * @returns The subtree at the given path
7811 */
7812 subtree(relativePath) {
7813 if (pathIsEmpty(relativePath)) {
7814 return this;
7815 }
7816 else {
7817 const front = pathGetFront(relativePath);
7818 const childTree = this.children.get(front);
7819 if (childTree !== null) {
7820 return childTree.subtree(pathPopFront(relativePath));
7821 }
7822 else {
7823 return new ImmutableTree(null);
7824 }
7825 }
7826 }
7827 /**
7828 * Sets a value at the specified path.
7829 *
7830 * @param relativePath - Path to set value at.
7831 * @param toSet - Value to set.
7832 * @returns Resulting tree.
7833 */
7834 set(relativePath, toSet) {
7835 if (pathIsEmpty(relativePath)) {
7836 return new ImmutableTree(toSet, this.children);
7837 }
7838 else {
7839 const front = pathGetFront(relativePath);
7840 const child = this.children.get(front) || new ImmutableTree(null);
7841 const newChild = child.set(pathPopFront(relativePath), toSet);
7842 const newChildren = this.children.insert(front, newChild);
7843 return new ImmutableTree(this.value, newChildren);
7844 }
7845 }
7846 /**
7847 * Removes the value at the specified path.
7848 *
7849 * @param relativePath - Path to value to remove.
7850 * @returns Resulting tree.
7851 */
7852 remove(relativePath) {
7853 if (pathIsEmpty(relativePath)) {
7854 if (this.children.isEmpty()) {
7855 return new ImmutableTree(null);
7856 }
7857 else {
7858 return new ImmutableTree(null, this.children);
7859 }
7860 }
7861 else {
7862 const front = pathGetFront(relativePath);
7863 const child = this.children.get(front);
7864 if (child) {
7865 const newChild = child.remove(pathPopFront(relativePath));
7866 let newChildren;
7867 if (newChild.isEmpty()) {
7868 newChildren = this.children.remove(front);
7869 }
7870 else {
7871 newChildren = this.children.insert(front, newChild);
7872 }
7873 if (this.value === null && newChildren.isEmpty()) {
7874 return new ImmutableTree(null);
7875 }
7876 else {
7877 return new ImmutableTree(this.value, newChildren);
7878 }
7879 }
7880 else {
7881 return this;
7882 }
7883 }
7884 }
7885 /**
7886 * Gets a value from the tree.
7887 *
7888 * @param relativePath - Path to get value for.
7889 * @returns Value at path, or null.
7890 */
7891 get(relativePath) {
7892 if (pathIsEmpty(relativePath)) {
7893 return this.value;
7894 }
7895 else {
7896 const front = pathGetFront(relativePath);
7897 const child = this.children.get(front);
7898 if (child) {
7899 return child.get(pathPopFront(relativePath));
7900 }
7901 else {
7902 return null;
7903 }
7904 }
7905 }
7906 /**
7907 * Replace the subtree at the specified path with the given new tree.
7908 *
7909 * @param relativePath - Path to replace subtree for.
7910 * @param newTree - New tree.
7911 * @returns Resulting tree.
7912 */
7913 setTree(relativePath, newTree) {
7914 if (pathIsEmpty(relativePath)) {
7915 return newTree;
7916 }
7917 else {
7918 const front = pathGetFront(relativePath);
7919 const child = this.children.get(front) || new ImmutableTree(null);
7920 const newChild = child.setTree(pathPopFront(relativePath), newTree);
7921 let newChildren;
7922 if (newChild.isEmpty()) {
7923 newChildren = this.children.remove(front);
7924 }
7925 else {
7926 newChildren = this.children.insert(front, newChild);
7927 }
7928 return new ImmutableTree(this.value, newChildren);
7929 }
7930 }
7931 /**
7932 * Performs a depth first fold on this tree. Transforms a tree into a single
7933 * value, given a function that operates on the path to a node, an optional
7934 * current value, and a map of child names to folded subtrees
7935 */
7936 fold(fn) {
7937 return this.fold_(newEmptyPath(), fn);
7938 }
7939 /**
7940 * Recursive helper for public-facing fold() method
7941 */
7942 fold_(pathSoFar, fn) {
7943 const accum = {};
7944 this.children.inorderTraversal((childKey, childTree) => {
7945 accum[childKey] = childTree.fold_(pathChild(pathSoFar, childKey), fn);
7946 });
7947 return fn(pathSoFar, this.value, accum);
7948 }
7949 /**
7950 * Find the first matching value on the given path. Return the result of applying f to it.
7951 */
7952 findOnPath(path, f) {
7953 return this.findOnPath_(path, newEmptyPath(), f);
7954 }
7955 findOnPath_(pathToFollow, pathSoFar, f) {
7956 const result = this.value ? f(pathSoFar, this.value) : false;
7957 if (result) {
7958 return result;
7959 }
7960 else {
7961 if (pathIsEmpty(pathToFollow)) {
7962 return null;
7963 }
7964 else {
7965 const front = pathGetFront(pathToFollow);
7966 const nextChild = this.children.get(front);
7967 if (nextChild) {
7968 return nextChild.findOnPath_(pathPopFront(pathToFollow), pathChild(pathSoFar, front), f);
7969 }
7970 else {
7971 return null;
7972 }
7973 }
7974 }
7975 }
7976 foreachOnPath(path, f) {
7977 return this.foreachOnPath_(path, newEmptyPath(), f);
7978 }
7979 foreachOnPath_(pathToFollow, currentRelativePath, f) {
7980 if (pathIsEmpty(pathToFollow)) {
7981 return this;
7982 }
7983 else {
7984 if (this.value) {
7985 f(currentRelativePath, this.value);
7986 }
7987 const front = pathGetFront(pathToFollow);
7988 const nextChild = this.children.get(front);
7989 if (nextChild) {
7990 return nextChild.foreachOnPath_(pathPopFront(pathToFollow), pathChild(currentRelativePath, front), f);
7991 }
7992 else {
7993 return new ImmutableTree(null);
7994 }
7995 }
7996 }
7997 /**
7998 * Calls the given function for each node in the tree that has a value.
7999 *
8000 * @param f - A function to be called with the path from the root of the tree to
8001 * a node, and the value at that node. Called in depth-first order.
8002 */
8003 foreach(f) {
8004 this.foreach_(newEmptyPath(), f);
8005 }
8006 foreach_(currentRelativePath, f) {
8007 this.children.inorderTraversal((childName, childTree) => {
8008 childTree.foreach_(pathChild(currentRelativePath, childName), f);
8009 });
8010 if (this.value) {
8011 f(currentRelativePath, this.value);
8012 }
8013 }
8014 foreachChild(f) {
8015 this.children.inorderTraversal((childName, childTree) => {
8016 if (childTree.value) {
8017 f(childName, childTree.value);
8018 }
8019 });
8020 }
8021}
8022
8023/**
8024 * @license
8025 * Copyright 2017 Google LLC
8026 *
8027 * Licensed under the Apache License, Version 2.0 (the "License");
8028 * you may not use this file except in compliance with the License.
8029 * You may obtain a copy of the License at
8030 *
8031 * http://www.apache.org/licenses/LICENSE-2.0
8032 *
8033 * Unless required by applicable law or agreed to in writing, software
8034 * distributed under the License is distributed on an "AS IS" BASIS,
8035 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
8036 * See the License for the specific language governing permissions and
8037 * limitations under the License.
8038 */
8039/**
8040 * This class holds a collection of writes that can be applied to nodes in unison. It abstracts away the logic with
8041 * dealing with priority writes and multiple nested writes. At any given path there is only allowed to be one write
8042 * modifying that path. Any write to an existing path or shadowing an existing path will modify that existing write
8043 * to reflect the write added.
8044 */
8045class CompoundWrite {
8046 constructor(writeTree_) {
8047 this.writeTree_ = writeTree_;
8048 }
8049 static empty() {
8050 return new CompoundWrite(new ImmutableTree(null));
8051 }
8052}
8053function compoundWriteAddWrite(compoundWrite, path, node) {
8054 if (pathIsEmpty(path)) {
8055 return new CompoundWrite(new ImmutableTree(node));
8056 }
8057 else {
8058 const rootmost = compoundWrite.writeTree_.findRootMostValueAndPath(path);
8059 if (rootmost != null) {
8060 const rootMostPath = rootmost.path;
8061 let value = rootmost.value;
8062 const relativePath = newRelativePath(rootMostPath, path);
8063 value = value.updateChild(relativePath, node);
8064 return new CompoundWrite(compoundWrite.writeTree_.set(rootMostPath, value));
8065 }
8066 else {
8067 const subtree = new ImmutableTree(node);
8068 const newWriteTree = compoundWrite.writeTree_.setTree(path, subtree);
8069 return new CompoundWrite(newWriteTree);
8070 }
8071 }
8072}
8073function compoundWriteAddWrites(compoundWrite, path, updates) {
8074 let newWrite = compoundWrite;
8075 each(updates, (childKey, node) => {
8076 newWrite = compoundWriteAddWrite(newWrite, pathChild(path, childKey), node);
8077 });
8078 return newWrite;
8079}
8080/**
8081 * Will remove a write at the given path and deeper paths. This will <em>not</em> modify a write at a higher
8082 * location, which must be removed by calling this method with that path.
8083 *
8084 * @param compoundWrite - The CompoundWrite to remove.
8085 * @param path - The path at which a write and all deeper writes should be removed
8086 * @returns The new CompoundWrite with the removed path
8087 */
8088function compoundWriteRemoveWrite(compoundWrite, path) {
8089 if (pathIsEmpty(path)) {
8090 return CompoundWrite.empty();
8091 }
8092 else {
8093 const newWriteTree = compoundWrite.writeTree_.setTree(path, new ImmutableTree(null));
8094 return new CompoundWrite(newWriteTree);
8095 }
8096}
8097/**
8098 * Returns whether this CompoundWrite will fully overwrite a node at a given location and can therefore be
8099 * considered "complete".
8100 *
8101 * @param compoundWrite - The CompoundWrite to check.
8102 * @param path - The path to check for
8103 * @returns Whether there is a complete write at that path
8104 */
8105function compoundWriteHasCompleteWrite(compoundWrite, path) {
8106 return compoundWriteGetCompleteNode(compoundWrite, path) != null;
8107}
8108/**
8109 * Returns a node for a path if and only if the node is a "complete" overwrite at that path. This will not aggregate
8110 * writes from deeper paths, but will return child nodes from a more shallow path.
8111 *
8112 * @param compoundWrite - The CompoundWrite to get the node from.
8113 * @param path - The path to get a complete write
8114 * @returns The node if complete at that path, or null otherwise.
8115 */
8116function compoundWriteGetCompleteNode(compoundWrite, path) {
8117 const rootmost = compoundWrite.writeTree_.findRootMostValueAndPath(path);
8118 if (rootmost != null) {
8119 return compoundWrite.writeTree_
8120 .get(rootmost.path)
8121 .getChild(newRelativePath(rootmost.path, path));
8122 }
8123 else {
8124 return null;
8125 }
8126}
8127/**
8128 * Returns all children that are guaranteed to be a complete overwrite.
8129 *
8130 * @param compoundWrite - The CompoundWrite to get children from.
8131 * @returns A list of all complete children.
8132 */
8133function compoundWriteGetCompleteChildren(compoundWrite) {
8134 const children = [];
8135 const node = compoundWrite.writeTree_.value;
8136 if (node != null) {
8137 // If it's a leaf node, it has no children; so nothing to do.
8138 if (!node.isLeafNode()) {
8139 node.forEachChild(PRIORITY_INDEX, (childName, childNode) => {
8140 children.push(new NamedNode(childName, childNode));
8141 });
8142 }
8143 }
8144 else {
8145 compoundWrite.writeTree_.children.inorderTraversal((childName, childTree) => {
8146 if (childTree.value != null) {
8147 children.push(new NamedNode(childName, childTree.value));
8148 }
8149 });
8150 }
8151 return children;
8152}
8153function compoundWriteChildCompoundWrite(compoundWrite, path) {
8154 if (pathIsEmpty(path)) {
8155 return compoundWrite;
8156 }
8157 else {
8158 const shadowingNode = compoundWriteGetCompleteNode(compoundWrite, path);
8159 if (shadowingNode != null) {
8160 return new CompoundWrite(new ImmutableTree(shadowingNode));
8161 }
8162 else {
8163 return new CompoundWrite(compoundWrite.writeTree_.subtree(path));
8164 }
8165 }
8166}
8167/**
8168 * Returns true if this CompoundWrite is empty and therefore does not modify any nodes.
8169 * @returns Whether this CompoundWrite is empty
8170 */
8171function compoundWriteIsEmpty(compoundWrite) {
8172 return compoundWrite.writeTree_.isEmpty();
8173}
8174/**
8175 * Applies this CompoundWrite to a node. The node is returned with all writes from this CompoundWrite applied to the
8176 * node
8177 * @param node - The node to apply this CompoundWrite to
8178 * @returns The node with all writes applied
8179 */
8180function compoundWriteApply(compoundWrite, node) {
8181 return applySubtreeWrite(newEmptyPath(), compoundWrite.writeTree_, node);
8182}
8183function applySubtreeWrite(relativePath, writeTree, node) {
8184 if (writeTree.value != null) {
8185 // Since there a write is always a leaf, we're done here
8186 return node.updateChild(relativePath, writeTree.value);
8187 }
8188 else {
8189 let priorityWrite = null;
8190 writeTree.children.inorderTraversal((childKey, childTree) => {
8191 if (childKey === '.priority') {
8192 // Apply priorities at the end so we don't update priorities for either empty nodes or forget
8193 // to apply priorities to empty nodes that are later filled
8194 assert(childTree.value !== null, 'Priority writes must always be leaf nodes');
8195 priorityWrite = childTree.value;
8196 }
8197 else {
8198 node = applySubtreeWrite(pathChild(relativePath, childKey), childTree, node);
8199 }
8200 });
8201 // If there was a priority write, we only apply it if the node is not empty
8202 if (!node.getChild(relativePath).isEmpty() && priorityWrite !== null) {
8203 node = node.updateChild(pathChild(relativePath, '.priority'), priorityWrite);
8204 }
8205 return node;
8206 }
8207}
8208
8209/**
8210 * @license
8211 * Copyright 2017 Google LLC
8212 *
8213 * Licensed under the Apache License, Version 2.0 (the "License");
8214 * you may not use this file except in compliance with the License.
8215 * You may obtain a copy of the License at
8216 *
8217 * http://www.apache.org/licenses/LICENSE-2.0
8218 *
8219 * Unless required by applicable law or agreed to in writing, software
8220 * distributed under the License is distributed on an "AS IS" BASIS,
8221 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
8222 * See the License for the specific language governing permissions and
8223 * limitations under the License.
8224 */
8225/**
8226 * Create a new WriteTreeRef for the given path. For use with a new sync point at the given path.
8227 *
8228 */
8229function writeTreeChildWrites(writeTree, path) {
8230 return newWriteTreeRef(path, writeTree);
8231}
8232/**
8233 * Record a new overwrite from user code.
8234 *
8235 * @param visible - This is set to false by some transactions. It should be excluded from event caches
8236 */
8237function writeTreeAddOverwrite(writeTree, path, snap, writeId, visible) {
8238 assert(writeId > writeTree.lastWriteId, 'Stacking an older write on top of newer ones');
8239 if (visible === undefined) {
8240 visible = true;
8241 }
8242 writeTree.allWrites.push({
8243 path,
8244 snap,
8245 writeId,
8246 visible
8247 });
8248 if (visible) {
8249 writeTree.visibleWrites = compoundWriteAddWrite(writeTree.visibleWrites, path, snap);
8250 }
8251 writeTree.lastWriteId = writeId;
8252}
8253/**
8254 * Record a new merge from user code.
8255 */
8256function writeTreeAddMerge(writeTree, path, changedChildren, writeId) {
8257 assert(writeId > writeTree.lastWriteId, 'Stacking an older merge on top of newer ones');
8258 writeTree.allWrites.push({
8259 path,
8260 children: changedChildren,
8261 writeId,
8262 visible: true
8263 });
8264 writeTree.visibleWrites = compoundWriteAddWrites(writeTree.visibleWrites, path, changedChildren);
8265 writeTree.lastWriteId = writeId;
8266}
8267function writeTreeGetWrite(writeTree, writeId) {
8268 for (let i = 0; i < writeTree.allWrites.length; i++) {
8269 const record = writeTree.allWrites[i];
8270 if (record.writeId === writeId) {
8271 return record;
8272 }
8273 }
8274 return null;
8275}
8276/**
8277 * Remove a write (either an overwrite or merge) that has been successfully acknowledge by the server. Recalculates
8278 * the tree if necessary. We return true if it may have been visible, meaning views need to reevaluate.
8279 *
8280 * @returns true if the write may have been visible (meaning we'll need to reevaluate / raise
8281 * events as a result).
8282 */
8283function writeTreeRemoveWrite(writeTree, writeId) {
8284 // Note: disabling this check. It could be a transaction that preempted another transaction, and thus was applied
8285 // out of order.
8286 //const validClear = revert || this.allWrites_.length === 0 || writeId <= this.allWrites_[0].writeId;
8287 //assert(validClear, "Either we don't have this write, or it's the first one in the queue");
8288 const idx = writeTree.allWrites.findIndex(s => {
8289 return s.writeId === writeId;
8290 });
8291 assert(idx >= 0, 'removeWrite called with nonexistent writeId.');
8292 const writeToRemove = writeTree.allWrites[idx];
8293 writeTree.allWrites.splice(idx, 1);
8294 let removedWriteWasVisible = writeToRemove.visible;
8295 let removedWriteOverlapsWithOtherWrites = false;
8296 let i = writeTree.allWrites.length - 1;
8297 while (removedWriteWasVisible && i >= 0) {
8298 const currentWrite = writeTree.allWrites[i];
8299 if (currentWrite.visible) {
8300 if (i >= idx &&
8301 writeTreeRecordContainsPath_(currentWrite, writeToRemove.path)) {
8302 // The removed write was completely shadowed by a subsequent write.
8303 removedWriteWasVisible = false;
8304 }
8305 else if (pathContains(writeToRemove.path, currentWrite.path)) {
8306 // Either we're covering some writes or they're covering part of us (depending on which came first).
8307 removedWriteOverlapsWithOtherWrites = true;
8308 }
8309 }
8310 i--;
8311 }
8312 if (!removedWriteWasVisible) {
8313 return false;
8314 }
8315 else if (removedWriteOverlapsWithOtherWrites) {
8316 // There's some shadowing going on. Just rebuild the visible writes from scratch.
8317 writeTreeResetTree_(writeTree);
8318 return true;
8319 }
8320 else {
8321 // There's no shadowing. We can safely just remove the write(s) from visibleWrites.
8322 if (writeToRemove.snap) {
8323 writeTree.visibleWrites = compoundWriteRemoveWrite(writeTree.visibleWrites, writeToRemove.path);
8324 }
8325 else {
8326 const children = writeToRemove.children;
8327 each(children, (childName) => {
8328 writeTree.visibleWrites = compoundWriteRemoveWrite(writeTree.visibleWrites, pathChild(writeToRemove.path, childName));
8329 });
8330 }
8331 return true;
8332 }
8333}
8334function writeTreeRecordContainsPath_(writeRecord, path) {
8335 if (writeRecord.snap) {
8336 return pathContains(writeRecord.path, path);
8337 }
8338 else {
8339 for (const childName in writeRecord.children) {
8340 if (writeRecord.children.hasOwnProperty(childName) &&
8341 pathContains(pathChild(writeRecord.path, childName), path)) {
8342 return true;
8343 }
8344 }
8345 return false;
8346 }
8347}
8348/**
8349 * Re-layer the writes and merges into a tree so we can efficiently calculate event snapshots
8350 */
8351function writeTreeResetTree_(writeTree) {
8352 writeTree.visibleWrites = writeTreeLayerTree_(writeTree.allWrites, writeTreeDefaultFilter_, newEmptyPath());
8353 if (writeTree.allWrites.length > 0) {
8354 writeTree.lastWriteId =
8355 writeTree.allWrites[writeTree.allWrites.length - 1].writeId;
8356 }
8357 else {
8358 writeTree.lastWriteId = -1;
8359 }
8360}
8361/**
8362 * The default filter used when constructing the tree. Keep everything that's visible.
8363 */
8364function writeTreeDefaultFilter_(write) {
8365 return write.visible;
8366}
8367/**
8368 * Static method. Given an array of WriteRecords, a filter for which ones to include, and a path, construct the tree of
8369 * event data at that path.
8370 */
8371function writeTreeLayerTree_(writes, filter, treeRoot) {
8372 let compoundWrite = CompoundWrite.empty();
8373 for (let i = 0; i < writes.length; ++i) {
8374 const write = writes[i];
8375 // Theory, a later set will either:
8376 // a) abort a relevant transaction, so no need to worry about excluding it from calculating that transaction
8377 // b) not be relevant to a transaction (separate branch), so again will not affect the data for that transaction
8378 if (filter(write)) {
8379 const writePath = write.path;
8380 let relativePath;
8381 if (write.snap) {
8382 if (pathContains(treeRoot, writePath)) {
8383 relativePath = newRelativePath(treeRoot, writePath);
8384 compoundWrite = compoundWriteAddWrite(compoundWrite, relativePath, write.snap);
8385 }
8386 else if (pathContains(writePath, treeRoot)) {
8387 relativePath = newRelativePath(writePath, treeRoot);
8388 compoundWrite = compoundWriteAddWrite(compoundWrite, newEmptyPath(), write.snap.getChild(relativePath));
8389 }
8390 else ;
8391 }
8392 else if (write.children) {
8393 if (pathContains(treeRoot, writePath)) {
8394 relativePath = newRelativePath(treeRoot, writePath);
8395 compoundWrite = compoundWriteAddWrites(compoundWrite, relativePath, write.children);
8396 }
8397 else if (pathContains(writePath, treeRoot)) {
8398 relativePath = newRelativePath(writePath, treeRoot);
8399 if (pathIsEmpty(relativePath)) {
8400 compoundWrite = compoundWriteAddWrites(compoundWrite, newEmptyPath(), write.children);
8401 }
8402 else {
8403 const child = safeGet(write.children, pathGetFront(relativePath));
8404 if (child) {
8405 // There exists a child in this node that matches the root path
8406 const deepNode = child.getChild(pathPopFront(relativePath));
8407 compoundWrite = compoundWriteAddWrite(compoundWrite, newEmptyPath(), deepNode);
8408 }
8409 }
8410 }
8411 else ;
8412 }
8413 else {
8414 throw assertionError('WriteRecord should have .snap or .children');
8415 }
8416 }
8417 }
8418 return compoundWrite;
8419}
8420/**
8421 * Given optional, underlying server data, and an optional set of constraints (exclude some sets, include hidden
8422 * writes), attempt to calculate a complete snapshot for the given path
8423 *
8424 * @param writeIdsToExclude - An optional set to be excluded
8425 * @param includeHiddenWrites - Defaults to false, whether or not to layer on writes with visible set to false
8426 */
8427function writeTreeCalcCompleteEventCache(writeTree, treePath, completeServerCache, writeIdsToExclude, includeHiddenWrites) {
8428 if (!writeIdsToExclude && !includeHiddenWrites) {
8429 const shadowingNode = compoundWriteGetCompleteNode(writeTree.visibleWrites, treePath);
8430 if (shadowingNode != null) {
8431 return shadowingNode;
8432 }
8433 else {
8434 const subMerge = compoundWriteChildCompoundWrite(writeTree.visibleWrites, treePath);
8435 if (compoundWriteIsEmpty(subMerge)) {
8436 return completeServerCache;
8437 }
8438 else if (completeServerCache == null &&
8439 !compoundWriteHasCompleteWrite(subMerge, newEmptyPath())) {
8440 // We wouldn't have a complete snapshot, since there's no underlying data and no complete shadow
8441 return null;
8442 }
8443 else {
8444 const layeredCache = completeServerCache || ChildrenNode.EMPTY_NODE;
8445 return compoundWriteApply(subMerge, layeredCache);
8446 }
8447 }
8448 }
8449 else {
8450 const merge = compoundWriteChildCompoundWrite(writeTree.visibleWrites, treePath);
8451 if (!includeHiddenWrites && compoundWriteIsEmpty(merge)) {
8452 return completeServerCache;
8453 }
8454 else {
8455 // If the server cache is null, and we don't have a complete cache, we need to return null
8456 if (!includeHiddenWrites &&
8457 completeServerCache == null &&
8458 !compoundWriteHasCompleteWrite(merge, newEmptyPath())) {
8459 return null;
8460 }
8461 else {
8462 const filter = function (write) {
8463 return ((write.visible || includeHiddenWrites) &&
8464 (!writeIdsToExclude ||
8465 !~writeIdsToExclude.indexOf(write.writeId)) &&
8466 (pathContains(write.path, treePath) ||
8467 pathContains(treePath, write.path)));
8468 };
8469 const mergeAtPath = writeTreeLayerTree_(writeTree.allWrites, filter, treePath);
8470 const layeredCache = completeServerCache || ChildrenNode.EMPTY_NODE;
8471 return compoundWriteApply(mergeAtPath, layeredCache);
8472 }
8473 }
8474 }
8475}
8476/**
8477 * With optional, underlying server data, attempt to return a children node of children that we have complete data for.
8478 * Used when creating new views, to pre-fill their complete event children snapshot.
8479 */
8480function writeTreeCalcCompleteEventChildren(writeTree, treePath, completeServerChildren) {
8481 let completeChildren = ChildrenNode.EMPTY_NODE;
8482 const topLevelSet = compoundWriteGetCompleteNode(writeTree.visibleWrites, treePath);
8483 if (topLevelSet) {
8484 if (!topLevelSet.isLeafNode()) {
8485 // we're shadowing everything. Return the children.
8486 topLevelSet.forEachChild(PRIORITY_INDEX, (childName, childSnap) => {
8487 completeChildren = completeChildren.updateImmediateChild(childName, childSnap);
8488 });
8489 }
8490 return completeChildren;
8491 }
8492 else if (completeServerChildren) {
8493 // Layer any children we have on top of this
8494 // We know we don't have a top-level set, so just enumerate existing children
8495 const merge = compoundWriteChildCompoundWrite(writeTree.visibleWrites, treePath);
8496 completeServerChildren.forEachChild(PRIORITY_INDEX, (childName, childNode) => {
8497 const node = compoundWriteApply(compoundWriteChildCompoundWrite(merge, new Path(childName)), childNode);
8498 completeChildren = completeChildren.updateImmediateChild(childName, node);
8499 });
8500 // Add any complete children we have from the set
8501 compoundWriteGetCompleteChildren(merge).forEach(namedNode => {
8502 completeChildren = completeChildren.updateImmediateChild(namedNode.name, namedNode.node);
8503 });
8504 return completeChildren;
8505 }
8506 else {
8507 // We don't have anything to layer on top of. Layer on any children we have
8508 // Note that we can return an empty snap if we have a defined delete
8509 const merge = compoundWriteChildCompoundWrite(writeTree.visibleWrites, treePath);
8510 compoundWriteGetCompleteChildren(merge).forEach(namedNode => {
8511 completeChildren = completeChildren.updateImmediateChild(namedNode.name, namedNode.node);
8512 });
8513 return completeChildren;
8514 }
8515}
8516/**
8517 * Given that the underlying server data has updated, determine what, if anything, needs to be
8518 * applied to the event cache.
8519 *
8520 * Possibilities:
8521 *
8522 * 1. No writes are shadowing. Events should be raised, the snap to be applied comes from the server data
8523 *
8524 * 2. Some write is completely shadowing. No events to be raised
8525 *
8526 * 3. Is partially shadowed. Events
8527 *
8528 * Either existingEventSnap or existingServerSnap must exist
8529 */
8530function writeTreeCalcEventCacheAfterServerOverwrite(writeTree, treePath, childPath, existingEventSnap, existingServerSnap) {
8531 assert(existingEventSnap || existingServerSnap, 'Either existingEventSnap or existingServerSnap must exist');
8532 const path = pathChild(treePath, childPath);
8533 if (compoundWriteHasCompleteWrite(writeTree.visibleWrites, path)) {
8534 // At this point we can probably guarantee that we're in case 2, meaning no events
8535 // May need to check visibility while doing the findRootMostValueAndPath call
8536 return null;
8537 }
8538 else {
8539 // No complete shadowing. We're either partially shadowing or not shadowing at all.
8540 const childMerge = compoundWriteChildCompoundWrite(writeTree.visibleWrites, path);
8541 if (compoundWriteIsEmpty(childMerge)) {
8542 // We're not shadowing at all. Case 1
8543 return existingServerSnap.getChild(childPath);
8544 }
8545 else {
8546 // This could be more efficient if the serverNode + updates doesn't change the eventSnap
8547 // However this is tricky to find out, since user updates don't necessary change the server
8548 // snap, e.g. priority updates on empty nodes, or deep deletes. Another special case is if the server
8549 // adds nodes, but doesn't change any existing writes. It is therefore not enough to
8550 // only check if the updates change the serverNode.
8551 // Maybe check if the merge tree contains these special cases and only do a full overwrite in that case?
8552 return compoundWriteApply(childMerge, existingServerSnap.getChild(childPath));
8553 }
8554 }
8555}
8556/**
8557 * Returns a complete child for a given server snap after applying all user writes or null if there is no
8558 * complete child for this ChildKey.
8559 */
8560function writeTreeCalcCompleteChild(writeTree, treePath, childKey, existingServerSnap) {
8561 const path = pathChild(treePath, childKey);
8562 const shadowingNode = compoundWriteGetCompleteNode(writeTree.visibleWrites, path);
8563 if (shadowingNode != null) {
8564 return shadowingNode;
8565 }
8566 else {
8567 if (existingServerSnap.isCompleteForChild(childKey)) {
8568 const childMerge = compoundWriteChildCompoundWrite(writeTree.visibleWrites, path);
8569 return compoundWriteApply(childMerge, existingServerSnap.getNode().getImmediateChild(childKey));
8570 }
8571 else {
8572 return null;
8573 }
8574 }
8575}
8576/**
8577 * Returns a node if there is a complete overwrite for this path. More specifically, if there is a write at
8578 * a higher path, this will return the child of that write relative to the write and this path.
8579 * Returns null if there is no write at this path.
8580 */
8581function writeTreeShadowingWrite(writeTree, path) {
8582 return compoundWriteGetCompleteNode(writeTree.visibleWrites, path);
8583}
8584/**
8585 * This method is used when processing child remove events on a query. If we can, we pull in children that were outside
8586 * the window, but may now be in the window.
8587 */
8588function writeTreeCalcIndexedSlice(writeTree, treePath, completeServerData, startPost, count, reverse, index) {
8589 let toIterate;
8590 const merge = compoundWriteChildCompoundWrite(writeTree.visibleWrites, treePath);
8591 const shadowingNode = compoundWriteGetCompleteNode(merge, newEmptyPath());
8592 if (shadowingNode != null) {
8593 toIterate = shadowingNode;
8594 }
8595 else if (completeServerData != null) {
8596 toIterate = compoundWriteApply(merge, completeServerData);
8597 }
8598 else {
8599 // no children to iterate on
8600 return [];
8601 }
8602 toIterate = toIterate.withIndex(index);
8603 if (!toIterate.isEmpty() && !toIterate.isLeafNode()) {
8604 const nodes = [];
8605 const cmp = index.getCompare();
8606 const iter = reverse
8607 ? toIterate.getReverseIteratorFrom(startPost, index)
8608 : toIterate.getIteratorFrom(startPost, index);
8609 let next = iter.getNext();
8610 while (next && nodes.length < count) {
8611 if (cmp(next, startPost) !== 0) {
8612 nodes.push(next);
8613 }
8614 next = iter.getNext();
8615 }
8616 return nodes;
8617 }
8618 else {
8619 return [];
8620 }
8621}
8622function newWriteTree() {
8623 return {
8624 visibleWrites: CompoundWrite.empty(),
8625 allWrites: [],
8626 lastWriteId: -1
8627 };
8628}
8629/**
8630 * If possible, returns a complete event cache, using the underlying server data if possible. In addition, can be used
8631 * to get a cache that includes hidden writes, and excludes arbitrary writes. Note that customizing the returned node
8632 * can lead to a more expensive calculation.
8633 *
8634 * @param writeIdsToExclude - Optional writes to exclude.
8635 * @param includeHiddenWrites - Defaults to false, whether or not to layer on writes with visible set to false
8636 */
8637function writeTreeRefCalcCompleteEventCache(writeTreeRef, completeServerCache, writeIdsToExclude, includeHiddenWrites) {
8638 return writeTreeCalcCompleteEventCache(writeTreeRef.writeTree, writeTreeRef.treePath, completeServerCache, writeIdsToExclude, includeHiddenWrites);
8639}
8640/**
8641 * If possible, returns a children node containing all of the complete children we have data for. The returned data is a
8642 * mix of the given server data and write data.
8643 *
8644 */
8645function writeTreeRefCalcCompleteEventChildren(writeTreeRef, completeServerChildren) {
8646 return writeTreeCalcCompleteEventChildren(writeTreeRef.writeTree, writeTreeRef.treePath, completeServerChildren);
8647}
8648/**
8649 * Given that either the underlying server data has updated or the outstanding writes have updated, determine what,
8650 * if anything, needs to be applied to the event cache.
8651 *
8652 * Possibilities:
8653 *
8654 * 1. No writes are shadowing. Events should be raised, the snap to be applied comes from the server data
8655 *
8656 * 2. Some write is completely shadowing. No events to be raised
8657 *
8658 * 3. Is partially shadowed. Events should be raised
8659 *
8660 * Either existingEventSnap or existingServerSnap must exist, this is validated via an assert
8661 *
8662 *
8663 */
8664function writeTreeRefCalcEventCacheAfterServerOverwrite(writeTreeRef, path, existingEventSnap, existingServerSnap) {
8665 return writeTreeCalcEventCacheAfterServerOverwrite(writeTreeRef.writeTree, writeTreeRef.treePath, path, existingEventSnap, existingServerSnap);
8666}
8667/**
8668 * Returns a node if there is a complete overwrite for this path. More specifically, if there is a write at
8669 * a higher path, this will return the child of that write relative to the write and this path.
8670 * Returns null if there is no write at this path.
8671 *
8672 */
8673function writeTreeRefShadowingWrite(writeTreeRef, path) {
8674 return writeTreeShadowingWrite(writeTreeRef.writeTree, pathChild(writeTreeRef.treePath, path));
8675}
8676/**
8677 * This method is used when processing child remove events on a query. If we can, we pull in children that were outside
8678 * the window, but may now be in the window
8679 */
8680function writeTreeRefCalcIndexedSlice(writeTreeRef, completeServerData, startPost, count, reverse, index) {
8681 return writeTreeCalcIndexedSlice(writeTreeRef.writeTree, writeTreeRef.treePath, completeServerData, startPost, count, reverse, index);
8682}
8683/**
8684 * Returns a complete child for a given server snap after applying all user writes or null if there is no
8685 * complete child for this ChildKey.
8686 */
8687function writeTreeRefCalcCompleteChild(writeTreeRef, childKey, existingServerCache) {
8688 return writeTreeCalcCompleteChild(writeTreeRef.writeTree, writeTreeRef.treePath, childKey, existingServerCache);
8689}
8690/**
8691 * Return a WriteTreeRef for a child.
8692 */
8693function writeTreeRefChild(writeTreeRef, childName) {
8694 return newWriteTreeRef(pathChild(writeTreeRef.treePath, childName), writeTreeRef.writeTree);
8695}
8696function newWriteTreeRef(path, writeTree) {
8697 return {
8698 treePath: path,
8699 writeTree
8700 };
8701}
8702
8703/**
8704 * @license
8705 * Copyright 2017 Google LLC
8706 *
8707 * Licensed under the Apache License, Version 2.0 (the "License");
8708 * you may not use this file except in compliance with the License.
8709 * You may obtain a copy of the License at
8710 *
8711 * http://www.apache.org/licenses/LICENSE-2.0
8712 *
8713 * Unless required by applicable law or agreed to in writing, software
8714 * distributed under the License is distributed on an "AS IS" BASIS,
8715 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
8716 * See the License for the specific language governing permissions and
8717 * limitations under the License.
8718 */
8719class ChildChangeAccumulator {
8720 constructor() {
8721 this.changeMap = new Map();
8722 }
8723 trackChildChange(change) {
8724 const type = change.type;
8725 const childKey = change.childName;
8726 assert(type === "child_added" /* CHILD_ADDED */ ||
8727 type === "child_changed" /* CHILD_CHANGED */ ||
8728 type === "child_removed" /* CHILD_REMOVED */, 'Only child changes supported for tracking');
8729 assert(childKey !== '.priority', 'Only non-priority child changes can be tracked.');
8730 const oldChange = this.changeMap.get(childKey);
8731 if (oldChange) {
8732 const oldType = oldChange.type;
8733 if (type === "child_added" /* CHILD_ADDED */ &&
8734 oldType === "child_removed" /* CHILD_REMOVED */) {
8735 this.changeMap.set(childKey, changeChildChanged(childKey, change.snapshotNode, oldChange.snapshotNode));
8736 }
8737 else if (type === "child_removed" /* CHILD_REMOVED */ &&
8738 oldType === "child_added" /* CHILD_ADDED */) {
8739 this.changeMap.delete(childKey);
8740 }
8741 else if (type === "child_removed" /* CHILD_REMOVED */ &&
8742 oldType === "child_changed" /* CHILD_CHANGED */) {
8743 this.changeMap.set(childKey, changeChildRemoved(childKey, oldChange.oldSnap));
8744 }
8745 else if (type === "child_changed" /* CHILD_CHANGED */ &&
8746 oldType === "child_added" /* CHILD_ADDED */) {
8747 this.changeMap.set(childKey, changeChildAdded(childKey, change.snapshotNode));
8748 }
8749 else if (type === "child_changed" /* CHILD_CHANGED */ &&
8750 oldType === "child_changed" /* CHILD_CHANGED */) {
8751 this.changeMap.set(childKey, changeChildChanged(childKey, change.snapshotNode, oldChange.oldSnap));
8752 }
8753 else {
8754 throw assertionError('Illegal combination of changes: ' +
8755 change +
8756 ' occurred after ' +
8757 oldChange);
8758 }
8759 }
8760 else {
8761 this.changeMap.set(childKey, change);
8762 }
8763 }
8764 getChanges() {
8765 return Array.from(this.changeMap.values());
8766 }
8767}
8768
8769/**
8770 * @license
8771 * Copyright 2017 Google LLC
8772 *
8773 * Licensed under the Apache License, Version 2.0 (the "License");
8774 * you may not use this file except in compliance with the License.
8775 * You may obtain a copy of the License at
8776 *
8777 * http://www.apache.org/licenses/LICENSE-2.0
8778 *
8779 * Unless required by applicable law or agreed to in writing, software
8780 * distributed under the License is distributed on an "AS IS" BASIS,
8781 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
8782 * See the License for the specific language governing permissions and
8783 * limitations under the License.
8784 */
8785/**
8786 * An implementation of CompleteChildSource that never returns any additional children
8787 */
8788// eslint-disable-next-line @typescript-eslint/naming-convention
8789class NoCompleteChildSource_ {
8790 getCompleteChild(childKey) {
8791 return null;
8792 }
8793 getChildAfterChild(index, child, reverse) {
8794 return null;
8795 }
8796}
8797/**
8798 * Singleton instance.
8799 */
8800const NO_COMPLETE_CHILD_SOURCE = new NoCompleteChildSource_();
8801/**
8802 * An implementation of CompleteChildSource that uses a WriteTree in addition to any other server data or
8803 * old event caches available to calculate complete children.
8804 */
8805class WriteTreeCompleteChildSource {
8806 constructor(writes_, viewCache_, optCompleteServerCache_ = null) {
8807 this.writes_ = writes_;
8808 this.viewCache_ = viewCache_;
8809 this.optCompleteServerCache_ = optCompleteServerCache_;
8810 }
8811 getCompleteChild(childKey) {
8812 const node = this.viewCache_.eventCache;
8813 if (node.isCompleteForChild(childKey)) {
8814 return node.getNode().getImmediateChild(childKey);
8815 }
8816 else {
8817 const serverNode = this.optCompleteServerCache_ != null
8818 ? new CacheNode(this.optCompleteServerCache_, true, false)
8819 : this.viewCache_.serverCache;
8820 return writeTreeRefCalcCompleteChild(this.writes_, childKey, serverNode);
8821 }
8822 }
8823 getChildAfterChild(index, child, reverse) {
8824 const completeServerData = this.optCompleteServerCache_ != null
8825 ? this.optCompleteServerCache_
8826 : viewCacheGetCompleteServerSnap(this.viewCache_);
8827 const nodes = writeTreeRefCalcIndexedSlice(this.writes_, completeServerData, child, 1, reverse, index);
8828 if (nodes.length === 0) {
8829 return null;
8830 }
8831 else {
8832 return nodes[0];
8833 }
8834 }
8835}
8836
8837/**
8838 * @license
8839 * Copyright 2017 Google LLC
8840 *
8841 * Licensed under the Apache License, Version 2.0 (the "License");
8842 * you may not use this file except in compliance with the License.
8843 * You may obtain a copy of the License at
8844 *
8845 * http://www.apache.org/licenses/LICENSE-2.0
8846 *
8847 * Unless required by applicable law or agreed to in writing, software
8848 * distributed under the License is distributed on an "AS IS" BASIS,
8849 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
8850 * See the License for the specific language governing permissions and
8851 * limitations under the License.
8852 */
8853function newViewProcessor(filter) {
8854 return { filter };
8855}
8856function viewProcessorAssertIndexed(viewProcessor, viewCache) {
8857 assert(viewCache.eventCache.getNode().isIndexed(viewProcessor.filter.getIndex()), 'Event snap not indexed');
8858 assert(viewCache.serverCache.getNode().isIndexed(viewProcessor.filter.getIndex()), 'Server snap not indexed');
8859}
8860function viewProcessorApplyOperation(viewProcessor, oldViewCache, operation, writesCache, completeCache) {
8861 const accumulator = new ChildChangeAccumulator();
8862 let newViewCache, filterServerNode;
8863 if (operation.type === OperationType.OVERWRITE) {
8864 const overwrite = operation;
8865 if (overwrite.source.fromUser) {
8866 newViewCache = viewProcessorApplyUserOverwrite(viewProcessor, oldViewCache, overwrite.path, overwrite.snap, writesCache, completeCache, accumulator);
8867 }
8868 else {
8869 assert(overwrite.source.fromServer, 'Unknown source.');
8870 // We filter the node if it's a tagged update or the node has been previously filtered and the
8871 // update is not at the root in which case it is ok (and necessary) to mark the node unfiltered
8872 // again
8873 filterServerNode =
8874 overwrite.source.tagged ||
8875 (oldViewCache.serverCache.isFiltered() && !pathIsEmpty(overwrite.path));
8876 newViewCache = viewProcessorApplyServerOverwrite(viewProcessor, oldViewCache, overwrite.path, overwrite.snap, writesCache, completeCache, filterServerNode, accumulator);
8877 }
8878 }
8879 else if (operation.type === OperationType.MERGE) {
8880 const merge = operation;
8881 if (merge.source.fromUser) {
8882 newViewCache = viewProcessorApplyUserMerge(viewProcessor, oldViewCache, merge.path, merge.children, writesCache, completeCache, accumulator);
8883 }
8884 else {
8885 assert(merge.source.fromServer, 'Unknown source.');
8886 // We filter the node if it's a tagged update or the node has been previously filtered
8887 filterServerNode =
8888 merge.source.tagged || oldViewCache.serverCache.isFiltered();
8889 newViewCache = viewProcessorApplyServerMerge(viewProcessor, oldViewCache, merge.path, merge.children, writesCache, completeCache, filterServerNode, accumulator);
8890 }
8891 }
8892 else if (operation.type === OperationType.ACK_USER_WRITE) {
8893 const ackUserWrite = operation;
8894 if (!ackUserWrite.revert) {
8895 newViewCache = viewProcessorAckUserWrite(viewProcessor, oldViewCache, ackUserWrite.path, ackUserWrite.affectedTree, writesCache, completeCache, accumulator);
8896 }
8897 else {
8898 newViewCache = viewProcessorRevertUserWrite(viewProcessor, oldViewCache, ackUserWrite.path, writesCache, completeCache, accumulator);
8899 }
8900 }
8901 else if (operation.type === OperationType.LISTEN_COMPLETE) {
8902 newViewCache = viewProcessorListenComplete(viewProcessor, oldViewCache, operation.path, writesCache, accumulator);
8903 }
8904 else {
8905 throw assertionError('Unknown operation type: ' + operation.type);
8906 }
8907 const changes = accumulator.getChanges();
8908 viewProcessorMaybeAddValueEvent(oldViewCache, newViewCache, changes);
8909 return { viewCache: newViewCache, changes };
8910}
8911function viewProcessorMaybeAddValueEvent(oldViewCache, newViewCache, accumulator) {
8912 const eventSnap = newViewCache.eventCache;
8913 if (eventSnap.isFullyInitialized()) {
8914 const isLeafOrEmpty = eventSnap.getNode().isLeafNode() || eventSnap.getNode().isEmpty();
8915 const oldCompleteSnap = viewCacheGetCompleteEventSnap(oldViewCache);
8916 if (accumulator.length > 0 ||
8917 !oldViewCache.eventCache.isFullyInitialized() ||
8918 (isLeafOrEmpty && !eventSnap.getNode().equals(oldCompleteSnap)) ||
8919 !eventSnap.getNode().getPriority().equals(oldCompleteSnap.getPriority())) {
8920 accumulator.push(changeValue(viewCacheGetCompleteEventSnap(newViewCache)));
8921 }
8922 }
8923}
8924function viewProcessorGenerateEventCacheAfterServerEvent(viewProcessor, viewCache, changePath, writesCache, source, accumulator) {
8925 const oldEventSnap = viewCache.eventCache;
8926 if (writeTreeRefShadowingWrite(writesCache, changePath) != null) {
8927 // we have a shadowing write, ignore changes
8928 return viewCache;
8929 }
8930 else {
8931 let newEventCache, serverNode;
8932 if (pathIsEmpty(changePath)) {
8933 // TODO: figure out how this plays with "sliding ack windows"
8934 assert(viewCache.serverCache.isFullyInitialized(), 'If change path is empty, we must have complete server data');
8935 if (viewCache.serverCache.isFiltered()) {
8936 // We need to special case this, because we need to only apply writes to complete children, or
8937 // we might end up raising events for incomplete children. If the server data is filtered deep
8938 // writes cannot be guaranteed to be complete
8939 const serverCache = viewCacheGetCompleteServerSnap(viewCache);
8940 const completeChildren = serverCache instanceof ChildrenNode
8941 ? serverCache
8942 : ChildrenNode.EMPTY_NODE;
8943 const completeEventChildren = writeTreeRefCalcCompleteEventChildren(writesCache, completeChildren);
8944 newEventCache = viewProcessor.filter.updateFullNode(viewCache.eventCache.getNode(), completeEventChildren, accumulator);
8945 }
8946 else {
8947 const completeNode = writeTreeRefCalcCompleteEventCache(writesCache, viewCacheGetCompleteServerSnap(viewCache));
8948 newEventCache = viewProcessor.filter.updateFullNode(viewCache.eventCache.getNode(), completeNode, accumulator);
8949 }
8950 }
8951 else {
8952 const childKey = pathGetFront(changePath);
8953 if (childKey === '.priority') {
8954 assert(pathGetLength(changePath) === 1, "Can't have a priority with additional path components");
8955 const oldEventNode = oldEventSnap.getNode();
8956 serverNode = viewCache.serverCache.getNode();
8957 // we might have overwrites for this priority
8958 const updatedPriority = writeTreeRefCalcEventCacheAfterServerOverwrite(writesCache, changePath, oldEventNode, serverNode);
8959 if (updatedPriority != null) {
8960 newEventCache = viewProcessor.filter.updatePriority(oldEventNode, updatedPriority);
8961 }
8962 else {
8963 // priority didn't change, keep old node
8964 newEventCache = oldEventSnap.getNode();
8965 }
8966 }
8967 else {
8968 const childChangePath = pathPopFront(changePath);
8969 // update child
8970 let newEventChild;
8971 if (oldEventSnap.isCompleteForChild(childKey)) {
8972 serverNode = viewCache.serverCache.getNode();
8973 const eventChildUpdate = writeTreeRefCalcEventCacheAfterServerOverwrite(writesCache, changePath, oldEventSnap.getNode(), serverNode);
8974 if (eventChildUpdate != null) {
8975 newEventChild = oldEventSnap
8976 .getNode()
8977 .getImmediateChild(childKey)
8978 .updateChild(childChangePath, eventChildUpdate);
8979 }
8980 else {
8981 // Nothing changed, just keep the old child
8982 newEventChild = oldEventSnap.getNode().getImmediateChild(childKey);
8983 }
8984 }
8985 else {
8986 newEventChild = writeTreeRefCalcCompleteChild(writesCache, childKey, viewCache.serverCache);
8987 }
8988 if (newEventChild != null) {
8989 newEventCache = viewProcessor.filter.updateChild(oldEventSnap.getNode(), childKey, newEventChild, childChangePath, source, accumulator);
8990 }
8991 else {
8992 // no complete child available or no change
8993 newEventCache = oldEventSnap.getNode();
8994 }
8995 }
8996 }
8997 return viewCacheUpdateEventSnap(viewCache, newEventCache, oldEventSnap.isFullyInitialized() || pathIsEmpty(changePath), viewProcessor.filter.filtersNodes());
8998 }
8999}
9000function viewProcessorApplyServerOverwrite(viewProcessor, oldViewCache, changePath, changedSnap, writesCache, completeCache, filterServerNode, accumulator) {
9001 const oldServerSnap = oldViewCache.serverCache;
9002 let newServerCache;
9003 const serverFilter = filterServerNode
9004 ? viewProcessor.filter
9005 : viewProcessor.filter.getIndexedFilter();
9006 if (pathIsEmpty(changePath)) {
9007 newServerCache = serverFilter.updateFullNode(oldServerSnap.getNode(), changedSnap, null);
9008 }
9009 else if (serverFilter.filtersNodes() && !oldServerSnap.isFiltered()) {
9010 // we want to filter the server node, but we didn't filter the server node yet, so simulate a full update
9011 const newServerNode = oldServerSnap
9012 .getNode()
9013 .updateChild(changePath, changedSnap);
9014 newServerCache = serverFilter.updateFullNode(oldServerSnap.getNode(), newServerNode, null);
9015 }
9016 else {
9017 const childKey = pathGetFront(changePath);
9018 if (!oldServerSnap.isCompleteForPath(changePath) &&
9019 pathGetLength(changePath) > 1) {
9020 // We don't update incomplete nodes with updates intended for other listeners
9021 return oldViewCache;
9022 }
9023 const childChangePath = pathPopFront(changePath);
9024 const childNode = oldServerSnap.getNode().getImmediateChild(childKey);
9025 const newChildNode = childNode.updateChild(childChangePath, changedSnap);
9026 if (childKey === '.priority') {
9027 newServerCache = serverFilter.updatePriority(oldServerSnap.getNode(), newChildNode);
9028 }
9029 else {
9030 newServerCache = serverFilter.updateChild(oldServerSnap.getNode(), childKey, newChildNode, childChangePath, NO_COMPLETE_CHILD_SOURCE, null);
9031 }
9032 }
9033 const newViewCache = viewCacheUpdateServerSnap(oldViewCache, newServerCache, oldServerSnap.isFullyInitialized() || pathIsEmpty(changePath), serverFilter.filtersNodes());
9034 const source = new WriteTreeCompleteChildSource(writesCache, newViewCache, completeCache);
9035 return viewProcessorGenerateEventCacheAfterServerEvent(viewProcessor, newViewCache, changePath, writesCache, source, accumulator);
9036}
9037function viewProcessorApplyUserOverwrite(viewProcessor, oldViewCache, changePath, changedSnap, writesCache, completeCache, accumulator) {
9038 const oldEventSnap = oldViewCache.eventCache;
9039 let newViewCache, newEventCache;
9040 const source = new WriteTreeCompleteChildSource(writesCache, oldViewCache, completeCache);
9041 if (pathIsEmpty(changePath)) {
9042 newEventCache = viewProcessor.filter.updateFullNode(oldViewCache.eventCache.getNode(), changedSnap, accumulator);
9043 newViewCache = viewCacheUpdateEventSnap(oldViewCache, newEventCache, true, viewProcessor.filter.filtersNodes());
9044 }
9045 else {
9046 const childKey = pathGetFront(changePath);
9047 if (childKey === '.priority') {
9048 newEventCache = viewProcessor.filter.updatePriority(oldViewCache.eventCache.getNode(), changedSnap);
9049 newViewCache = viewCacheUpdateEventSnap(oldViewCache, newEventCache, oldEventSnap.isFullyInitialized(), oldEventSnap.isFiltered());
9050 }
9051 else {
9052 const childChangePath = pathPopFront(changePath);
9053 const oldChild = oldEventSnap.getNode().getImmediateChild(childKey);
9054 let newChild;
9055 if (pathIsEmpty(childChangePath)) {
9056 // Child overwrite, we can replace the child
9057 newChild = changedSnap;
9058 }
9059 else {
9060 const childNode = source.getCompleteChild(childKey);
9061 if (childNode != null) {
9062 if (pathGetBack(childChangePath) === '.priority' &&
9063 childNode.getChild(pathParent(childChangePath)).isEmpty()) {
9064 // This is a priority update on an empty node. If this node exists on the server, the
9065 // server will send down the priority in the update, so ignore for now
9066 newChild = childNode;
9067 }
9068 else {
9069 newChild = childNode.updateChild(childChangePath, changedSnap);
9070 }
9071 }
9072 else {
9073 // There is no complete child node available
9074 newChild = ChildrenNode.EMPTY_NODE;
9075 }
9076 }
9077 if (!oldChild.equals(newChild)) {
9078 const newEventSnap = viewProcessor.filter.updateChild(oldEventSnap.getNode(), childKey, newChild, childChangePath, source, accumulator);
9079 newViewCache = viewCacheUpdateEventSnap(oldViewCache, newEventSnap, oldEventSnap.isFullyInitialized(), viewProcessor.filter.filtersNodes());
9080 }
9081 else {
9082 newViewCache = oldViewCache;
9083 }
9084 }
9085 }
9086 return newViewCache;
9087}
9088function viewProcessorCacheHasChild(viewCache, childKey) {
9089 return viewCache.eventCache.isCompleteForChild(childKey);
9090}
9091function viewProcessorApplyUserMerge(viewProcessor, viewCache, path, changedChildren, writesCache, serverCache, accumulator) {
9092 // HACK: In the case of a limit query, there may be some changes that bump things out of the
9093 // window leaving room for new items. It's important we process these changes first, so we
9094 // iterate the changes twice, first processing any that affect items currently in view.
9095 // TODO: I consider an item "in view" if cacheHasChild is true, which checks both the server
9096 // and event snap. I'm not sure if this will result in edge cases when a child is in one but
9097 // not the other.
9098 let curViewCache = viewCache;
9099 changedChildren.foreach((relativePath, childNode) => {
9100 const writePath = pathChild(path, relativePath);
9101 if (viewProcessorCacheHasChild(viewCache, pathGetFront(writePath))) {
9102 curViewCache = viewProcessorApplyUserOverwrite(viewProcessor, curViewCache, writePath, childNode, writesCache, serverCache, accumulator);
9103 }
9104 });
9105 changedChildren.foreach((relativePath, childNode) => {
9106 const writePath = pathChild(path, relativePath);
9107 if (!viewProcessorCacheHasChild(viewCache, pathGetFront(writePath))) {
9108 curViewCache = viewProcessorApplyUserOverwrite(viewProcessor, curViewCache, writePath, childNode, writesCache, serverCache, accumulator);
9109 }
9110 });
9111 return curViewCache;
9112}
9113function viewProcessorApplyMerge(viewProcessor, node, merge) {
9114 merge.foreach((relativePath, childNode) => {
9115 node = node.updateChild(relativePath, childNode);
9116 });
9117 return node;
9118}
9119function viewProcessorApplyServerMerge(viewProcessor, viewCache, path, changedChildren, writesCache, serverCache, filterServerNode, accumulator) {
9120 // If we don't have a cache yet, this merge was intended for a previously listen in the same location. Ignore it and
9121 // wait for the complete data update coming soon.
9122 if (viewCache.serverCache.getNode().isEmpty() &&
9123 !viewCache.serverCache.isFullyInitialized()) {
9124 return viewCache;
9125 }
9126 // HACK: In the case of a limit query, there may be some changes that bump things out of the
9127 // window leaving room for new items. It's important we process these changes first, so we
9128 // iterate the changes twice, first processing any that affect items currently in view.
9129 // TODO: I consider an item "in view" if cacheHasChild is true, which checks both the server
9130 // and event snap. I'm not sure if this will result in edge cases when a child is in one but
9131 // not the other.
9132 let curViewCache = viewCache;
9133 let viewMergeTree;
9134 if (pathIsEmpty(path)) {
9135 viewMergeTree = changedChildren;
9136 }
9137 else {
9138 viewMergeTree = new ImmutableTree(null).setTree(path, changedChildren);
9139 }
9140 const serverNode = viewCache.serverCache.getNode();
9141 viewMergeTree.children.inorderTraversal((childKey, childTree) => {
9142 if (serverNode.hasChild(childKey)) {
9143 const serverChild = viewCache.serverCache
9144 .getNode()
9145 .getImmediateChild(childKey);
9146 const newChild = viewProcessorApplyMerge(viewProcessor, serverChild, childTree);
9147 curViewCache = viewProcessorApplyServerOverwrite(viewProcessor, curViewCache, new Path(childKey), newChild, writesCache, serverCache, filterServerNode, accumulator);
9148 }
9149 });
9150 viewMergeTree.children.inorderTraversal((childKey, childMergeTree) => {
9151 const isUnknownDeepMerge = !viewCache.serverCache.isCompleteForChild(childKey) &&
9152 childMergeTree.value === undefined;
9153 if (!serverNode.hasChild(childKey) && !isUnknownDeepMerge) {
9154 const serverChild = viewCache.serverCache
9155 .getNode()
9156 .getImmediateChild(childKey);
9157 const newChild = viewProcessorApplyMerge(viewProcessor, serverChild, childMergeTree);
9158 curViewCache = viewProcessorApplyServerOverwrite(viewProcessor, curViewCache, new Path(childKey), newChild, writesCache, serverCache, filterServerNode, accumulator);
9159 }
9160 });
9161 return curViewCache;
9162}
9163function viewProcessorAckUserWrite(viewProcessor, viewCache, ackPath, affectedTree, writesCache, completeCache, accumulator) {
9164 if (writeTreeRefShadowingWrite(writesCache, ackPath) != null) {
9165 return viewCache;
9166 }
9167 // Only filter server node if it is currently filtered
9168 const filterServerNode = viewCache.serverCache.isFiltered();
9169 // Essentially we'll just get our existing server cache for the affected paths and re-apply it as a server update
9170 // now that it won't be shadowed.
9171 const serverCache = viewCache.serverCache;
9172 if (affectedTree.value != null) {
9173 // This is an overwrite.
9174 if ((pathIsEmpty(ackPath) && serverCache.isFullyInitialized()) ||
9175 serverCache.isCompleteForPath(ackPath)) {
9176 return viewProcessorApplyServerOverwrite(viewProcessor, viewCache, ackPath, serverCache.getNode().getChild(ackPath), writesCache, completeCache, filterServerNode, accumulator);
9177 }
9178 else if (pathIsEmpty(ackPath)) {
9179 // This is a goofy edge case where we are acking data at this location but don't have full data. We
9180 // should just re-apply whatever we have in our cache as a merge.
9181 let changedChildren = new ImmutableTree(null);
9182 serverCache.getNode().forEachChild(KEY_INDEX, (name, node) => {
9183 changedChildren = changedChildren.set(new Path(name), node);
9184 });
9185 return viewProcessorApplyServerMerge(viewProcessor, viewCache, ackPath, changedChildren, writesCache, completeCache, filterServerNode, accumulator);
9186 }
9187 else {
9188 return viewCache;
9189 }
9190 }
9191 else {
9192 // This is a merge.
9193 let changedChildren = new ImmutableTree(null);
9194 affectedTree.foreach((mergePath, value) => {
9195 const serverCachePath = pathChild(ackPath, mergePath);
9196 if (serverCache.isCompleteForPath(serverCachePath)) {
9197 changedChildren = changedChildren.set(mergePath, serverCache.getNode().getChild(serverCachePath));
9198 }
9199 });
9200 return viewProcessorApplyServerMerge(viewProcessor, viewCache, ackPath, changedChildren, writesCache, completeCache, filterServerNode, accumulator);
9201 }
9202}
9203function viewProcessorListenComplete(viewProcessor, viewCache, path, writesCache, accumulator) {
9204 const oldServerNode = viewCache.serverCache;
9205 const newViewCache = viewCacheUpdateServerSnap(viewCache, oldServerNode.getNode(), oldServerNode.isFullyInitialized() || pathIsEmpty(path), oldServerNode.isFiltered());
9206 return viewProcessorGenerateEventCacheAfterServerEvent(viewProcessor, newViewCache, path, writesCache, NO_COMPLETE_CHILD_SOURCE, accumulator);
9207}
9208function viewProcessorRevertUserWrite(viewProcessor, viewCache, path, writesCache, completeServerCache, accumulator) {
9209 let complete;
9210 if (writeTreeRefShadowingWrite(writesCache, path) != null) {
9211 return viewCache;
9212 }
9213 else {
9214 const source = new WriteTreeCompleteChildSource(writesCache, viewCache, completeServerCache);
9215 const oldEventCache = viewCache.eventCache.getNode();
9216 let newEventCache;
9217 if (pathIsEmpty(path) || pathGetFront(path) === '.priority') {
9218 let newNode;
9219 if (viewCache.serverCache.isFullyInitialized()) {
9220 newNode = writeTreeRefCalcCompleteEventCache(writesCache, viewCacheGetCompleteServerSnap(viewCache));
9221 }
9222 else {
9223 const serverChildren = viewCache.serverCache.getNode();
9224 assert(serverChildren instanceof ChildrenNode, 'serverChildren would be complete if leaf node');
9225 newNode = writeTreeRefCalcCompleteEventChildren(writesCache, serverChildren);
9226 }
9227 newNode = newNode;
9228 newEventCache = viewProcessor.filter.updateFullNode(oldEventCache, newNode, accumulator);
9229 }
9230 else {
9231 const childKey = pathGetFront(path);
9232 let newChild = writeTreeRefCalcCompleteChild(writesCache, childKey, viewCache.serverCache);
9233 if (newChild == null &&
9234 viewCache.serverCache.isCompleteForChild(childKey)) {
9235 newChild = oldEventCache.getImmediateChild(childKey);
9236 }
9237 if (newChild != null) {
9238 newEventCache = viewProcessor.filter.updateChild(oldEventCache, childKey, newChild, pathPopFront(path), source, accumulator);
9239 }
9240 else if (viewCache.eventCache.getNode().hasChild(childKey)) {
9241 // No complete child available, delete the existing one, if any
9242 newEventCache = viewProcessor.filter.updateChild(oldEventCache, childKey, ChildrenNode.EMPTY_NODE, pathPopFront(path), source, accumulator);
9243 }
9244 else {
9245 newEventCache = oldEventCache;
9246 }
9247 if (newEventCache.isEmpty() &&
9248 viewCache.serverCache.isFullyInitialized()) {
9249 // We might have reverted all child writes. Maybe the old event was a leaf node
9250 complete = writeTreeRefCalcCompleteEventCache(writesCache, viewCacheGetCompleteServerSnap(viewCache));
9251 if (complete.isLeafNode()) {
9252 newEventCache = viewProcessor.filter.updateFullNode(newEventCache, complete, accumulator);
9253 }
9254 }
9255 }
9256 complete =
9257 viewCache.serverCache.isFullyInitialized() ||
9258 writeTreeRefShadowingWrite(writesCache, newEmptyPath()) != null;
9259 return viewCacheUpdateEventSnap(viewCache, newEventCache, complete, viewProcessor.filter.filtersNodes());
9260 }
9261}
9262
9263/**
9264 * @license
9265 * Copyright 2017 Google LLC
9266 *
9267 * Licensed under the Apache License, Version 2.0 (the "License");
9268 * you may not use this file except in compliance with the License.
9269 * You may obtain a copy of the License at
9270 *
9271 * http://www.apache.org/licenses/LICENSE-2.0
9272 *
9273 * Unless required by applicable law or agreed to in writing, software
9274 * distributed under the License is distributed on an "AS IS" BASIS,
9275 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9276 * See the License for the specific language governing permissions and
9277 * limitations under the License.
9278 */
9279/**
9280 * A view represents a specific location and query that has 1 or more event registrations.
9281 *
9282 * It does several things:
9283 * - Maintains the list of event registrations for this location/query.
9284 * - Maintains a cache of the data visible for this location/query.
9285 * - Applies new operations (via applyOperation), updates the cache, and based on the event
9286 * registrations returns the set of events to be raised.
9287 */
9288class View {
9289 constructor(query_, initialViewCache) {
9290 this.query_ = query_;
9291 this.eventRegistrations_ = [];
9292 const params = this.query_._queryParams;
9293 const indexFilter = new IndexedFilter(params.getIndex());
9294 const filter = queryParamsGetNodeFilter(params);
9295 this.processor_ = newViewProcessor(filter);
9296 const initialServerCache = initialViewCache.serverCache;
9297 const initialEventCache = initialViewCache.eventCache;
9298 // Don't filter server node with other filter than index, wait for tagged listen
9299 const serverSnap = indexFilter.updateFullNode(ChildrenNode.EMPTY_NODE, initialServerCache.getNode(), null);
9300 const eventSnap = filter.updateFullNode(ChildrenNode.EMPTY_NODE, initialEventCache.getNode(), null);
9301 const newServerCache = new CacheNode(serverSnap, initialServerCache.isFullyInitialized(), indexFilter.filtersNodes());
9302 const newEventCache = new CacheNode(eventSnap, initialEventCache.isFullyInitialized(), filter.filtersNodes());
9303 this.viewCache_ = newViewCache(newEventCache, newServerCache);
9304 this.eventGenerator_ = new EventGenerator(this.query_);
9305 }
9306 get query() {
9307 return this.query_;
9308 }
9309}
9310function viewGetServerCache(view) {
9311 return view.viewCache_.serverCache.getNode();
9312}
9313function viewGetCompleteNode(view) {
9314 return viewCacheGetCompleteEventSnap(view.viewCache_);
9315}
9316function viewGetCompleteServerCache(view, path) {
9317 const cache = viewCacheGetCompleteServerSnap(view.viewCache_);
9318 if (cache) {
9319 // If this isn't a "loadsAllData" view, then cache isn't actually a complete cache and
9320 // we need to see if it contains the child we're interested in.
9321 if (view.query._queryParams.loadsAllData() ||
9322 (!pathIsEmpty(path) &&
9323 !cache.getImmediateChild(pathGetFront(path)).isEmpty())) {
9324 return cache.getChild(path);
9325 }
9326 }
9327 return null;
9328}
9329function viewIsEmpty(view) {
9330 return view.eventRegistrations_.length === 0;
9331}
9332function viewAddEventRegistration(view, eventRegistration) {
9333 view.eventRegistrations_.push(eventRegistration);
9334}
9335/**
9336 * @param eventRegistration - If null, remove all callbacks.
9337 * @param cancelError - If a cancelError is provided, appropriate cancel events will be returned.
9338 * @returns Cancel events, if cancelError was provided.
9339 */
9340function viewRemoveEventRegistration(view, eventRegistration, cancelError) {
9341 const cancelEvents = [];
9342 if (cancelError) {
9343 assert(eventRegistration == null, 'A cancel should cancel all event registrations.');
9344 const path = view.query._path;
9345 view.eventRegistrations_.forEach(registration => {
9346 const maybeEvent = registration.createCancelEvent(cancelError, path);
9347 if (maybeEvent) {
9348 cancelEvents.push(maybeEvent);
9349 }
9350 });
9351 }
9352 if (eventRegistration) {
9353 let remaining = [];
9354 for (let i = 0; i < view.eventRegistrations_.length; ++i) {
9355 const existing = view.eventRegistrations_[i];
9356 if (!existing.matches(eventRegistration)) {
9357 remaining.push(existing);
9358 }
9359 else if (eventRegistration.hasAnyCallback()) {
9360 // We're removing just this one
9361 remaining = remaining.concat(view.eventRegistrations_.slice(i + 1));
9362 break;
9363 }
9364 }
9365 view.eventRegistrations_ = remaining;
9366 }
9367 else {
9368 view.eventRegistrations_ = [];
9369 }
9370 return cancelEvents;
9371}
9372/**
9373 * Applies the given Operation, updates our cache, and returns the appropriate events.
9374 */
9375function viewApplyOperation(view, operation, writesCache, completeServerCache) {
9376 if (operation.type === OperationType.MERGE &&
9377 operation.source.queryId !== null) {
9378 assert(viewCacheGetCompleteServerSnap(view.viewCache_), 'We should always have a full cache before handling merges');
9379 assert(viewCacheGetCompleteEventSnap(view.viewCache_), 'Missing event cache, even though we have a server cache');
9380 }
9381 const oldViewCache = view.viewCache_;
9382 const result = viewProcessorApplyOperation(view.processor_, oldViewCache, operation, writesCache, completeServerCache);
9383 viewProcessorAssertIndexed(view.processor_, result.viewCache);
9384 assert(result.viewCache.serverCache.isFullyInitialized() ||
9385 !oldViewCache.serverCache.isFullyInitialized(), 'Once a server snap is complete, it should never go back');
9386 view.viewCache_ = result.viewCache;
9387 return viewGenerateEventsForChanges_(view, result.changes, result.viewCache.eventCache.getNode(), null);
9388}
9389function viewGetInitialEvents(view, registration) {
9390 const eventSnap = view.viewCache_.eventCache;
9391 const initialChanges = [];
9392 if (!eventSnap.getNode().isLeafNode()) {
9393 const eventNode = eventSnap.getNode();
9394 eventNode.forEachChild(PRIORITY_INDEX, (key, childNode) => {
9395 initialChanges.push(changeChildAdded(key, childNode));
9396 });
9397 }
9398 if (eventSnap.isFullyInitialized()) {
9399 initialChanges.push(changeValue(eventSnap.getNode()));
9400 }
9401 return viewGenerateEventsForChanges_(view, initialChanges, eventSnap.getNode(), registration);
9402}
9403function viewGenerateEventsForChanges_(view, changes, eventCache, eventRegistration) {
9404 const registrations = eventRegistration
9405 ? [eventRegistration]
9406 : view.eventRegistrations_;
9407 return eventGeneratorGenerateEventsForChanges(view.eventGenerator_, changes, eventCache, registrations);
9408}
9409
9410/**
9411 * @license
9412 * Copyright 2017 Google LLC
9413 *
9414 * Licensed under the Apache License, Version 2.0 (the "License");
9415 * you may not use this file except in compliance with the License.
9416 * You may obtain a copy of the License at
9417 *
9418 * http://www.apache.org/licenses/LICENSE-2.0
9419 *
9420 * Unless required by applicable law or agreed to in writing, software
9421 * distributed under the License is distributed on an "AS IS" BASIS,
9422 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9423 * See the License for the specific language governing permissions and
9424 * limitations under the License.
9425 */
9426let referenceConstructor$1;
9427/**
9428 * SyncPoint represents a single location in a SyncTree with 1 or more event registrations, meaning we need to
9429 * maintain 1 or more Views at this location to cache server data and raise appropriate events for server changes
9430 * and user writes (set, transaction, update).
9431 *
9432 * It's responsible for:
9433 * - Maintaining the set of 1 or more views necessary at this location (a SyncPoint with 0 views should be removed).
9434 * - Proxying user / server operations to the views as appropriate (i.e. applyServerOverwrite,
9435 * applyUserOverwrite, etc.)
9436 */
9437class SyncPoint {
9438 constructor() {
9439 /**
9440 * The Views being tracked at this location in the tree, stored as a map where the key is a
9441 * queryId and the value is the View for that query.
9442 *
9443 * NOTE: This list will be quite small (usually 1, but perhaps 2 or 3; any more is an odd use case).
9444 */
9445 this.views = new Map();
9446 }
9447}
9448function syncPointSetReferenceConstructor(val) {
9449 assert(!referenceConstructor$1, '__referenceConstructor has already been defined');
9450 referenceConstructor$1 = val;
9451}
9452function syncPointGetReferenceConstructor() {
9453 assert(referenceConstructor$1, 'Reference.ts has not been loaded');
9454 return referenceConstructor$1;
9455}
9456function syncPointIsEmpty(syncPoint) {
9457 return syncPoint.views.size === 0;
9458}
9459function syncPointApplyOperation(syncPoint, operation, writesCache, optCompleteServerCache) {
9460 const queryId = operation.source.queryId;
9461 if (queryId !== null) {
9462 const view = syncPoint.views.get(queryId);
9463 assert(view != null, 'SyncTree gave us an op for an invalid query.');
9464 return viewApplyOperation(view, operation, writesCache, optCompleteServerCache);
9465 }
9466 else {
9467 let events = [];
9468 for (const view of syncPoint.views.values()) {
9469 events = events.concat(viewApplyOperation(view, operation, writesCache, optCompleteServerCache));
9470 }
9471 return events;
9472 }
9473}
9474/**
9475 * Get a view for the specified query.
9476 *
9477 * @param query - The query to return a view for
9478 * @param writesCache
9479 * @param serverCache
9480 * @param serverCacheComplete
9481 * @returns Events to raise.
9482 */
9483function syncPointGetView(syncPoint, query, writesCache, serverCache, serverCacheComplete) {
9484 const queryId = query._queryIdentifier;
9485 const view = syncPoint.views.get(queryId);
9486 if (!view) {
9487 // TODO: make writesCache take flag for complete server node
9488 let eventCache = writeTreeRefCalcCompleteEventCache(writesCache, serverCacheComplete ? serverCache : null);
9489 let eventCacheComplete = false;
9490 if (eventCache) {
9491 eventCacheComplete = true;
9492 }
9493 else if (serverCache instanceof ChildrenNode) {
9494 eventCache = writeTreeRefCalcCompleteEventChildren(writesCache, serverCache);
9495 eventCacheComplete = false;
9496 }
9497 else {
9498 eventCache = ChildrenNode.EMPTY_NODE;
9499 eventCacheComplete = false;
9500 }
9501 const viewCache = newViewCache(new CacheNode(eventCache, eventCacheComplete, false), new CacheNode(serverCache, serverCacheComplete, false));
9502 return new View(query, viewCache);
9503 }
9504 return view;
9505}
9506/**
9507 * Add an event callback for the specified query.
9508 *
9509 * @param query
9510 * @param eventRegistration
9511 * @param writesCache
9512 * @param serverCache - Complete server cache, if we have it.
9513 * @param serverCacheComplete
9514 * @returns Events to raise.
9515 */
9516function syncPointAddEventRegistration(syncPoint, query, eventRegistration, writesCache, serverCache, serverCacheComplete) {
9517 const view = syncPointGetView(syncPoint, query, writesCache, serverCache, serverCacheComplete);
9518 if (!syncPoint.views.has(query._queryIdentifier)) {
9519 syncPoint.views.set(query._queryIdentifier, view);
9520 }
9521 // This is guaranteed to exist now, we just created anything that was missing
9522 viewAddEventRegistration(view, eventRegistration);
9523 return viewGetInitialEvents(view, eventRegistration);
9524}
9525/**
9526 * Remove event callback(s). Return cancelEvents if a cancelError is specified.
9527 *
9528 * If query is the default query, we'll check all views for the specified eventRegistration.
9529 * If eventRegistration is null, we'll remove all callbacks for the specified view(s).
9530 *
9531 * @param eventRegistration - If null, remove all callbacks.
9532 * @param cancelError - If a cancelError is provided, appropriate cancel events will be returned.
9533 * @returns removed queries and any cancel events
9534 */
9535function syncPointRemoveEventRegistration(syncPoint, query, eventRegistration, cancelError) {
9536 const queryId = query._queryIdentifier;
9537 const removed = [];
9538 let cancelEvents = [];
9539 const hadCompleteView = syncPointHasCompleteView(syncPoint);
9540 if (queryId === 'default') {
9541 // When you do ref.off(...), we search all views for the registration to remove.
9542 for (const [viewQueryId, view] of syncPoint.views.entries()) {
9543 cancelEvents = cancelEvents.concat(viewRemoveEventRegistration(view, eventRegistration, cancelError));
9544 if (viewIsEmpty(view)) {
9545 syncPoint.views.delete(viewQueryId);
9546 // We'll deal with complete views later.
9547 if (!view.query._queryParams.loadsAllData()) {
9548 removed.push(view.query);
9549 }
9550 }
9551 }
9552 }
9553 else {
9554 // remove the callback from the specific view.
9555 const view = syncPoint.views.get(queryId);
9556 if (view) {
9557 cancelEvents = cancelEvents.concat(viewRemoveEventRegistration(view, eventRegistration, cancelError));
9558 if (viewIsEmpty(view)) {
9559 syncPoint.views.delete(queryId);
9560 // We'll deal with complete views later.
9561 if (!view.query._queryParams.loadsAllData()) {
9562 removed.push(view.query);
9563 }
9564 }
9565 }
9566 }
9567 if (hadCompleteView && !syncPointHasCompleteView(syncPoint)) {
9568 // We removed our last complete view.
9569 removed.push(new (syncPointGetReferenceConstructor())(query._repo, query._path));
9570 }
9571 return { removed, events: cancelEvents };
9572}
9573function syncPointGetQueryViews(syncPoint) {
9574 const result = [];
9575 for (const view of syncPoint.views.values()) {
9576 if (!view.query._queryParams.loadsAllData()) {
9577 result.push(view);
9578 }
9579 }
9580 return result;
9581}
9582/**
9583 * @param path - The path to the desired complete snapshot
9584 * @returns A complete cache, if it exists
9585 */
9586function syncPointGetCompleteServerCache(syncPoint, path) {
9587 let serverCache = null;
9588 for (const view of syncPoint.views.values()) {
9589 serverCache = serverCache || viewGetCompleteServerCache(view, path);
9590 }
9591 return serverCache;
9592}
9593function syncPointViewForQuery(syncPoint, query) {
9594 const params = query._queryParams;
9595 if (params.loadsAllData()) {
9596 return syncPointGetCompleteView(syncPoint);
9597 }
9598 else {
9599 const queryId = query._queryIdentifier;
9600 return syncPoint.views.get(queryId);
9601 }
9602}
9603function syncPointViewExistsForQuery(syncPoint, query) {
9604 return syncPointViewForQuery(syncPoint, query) != null;
9605}
9606function syncPointHasCompleteView(syncPoint) {
9607 return syncPointGetCompleteView(syncPoint) != null;
9608}
9609function syncPointGetCompleteView(syncPoint) {
9610 for (const view of syncPoint.views.values()) {
9611 if (view.query._queryParams.loadsAllData()) {
9612 return view;
9613 }
9614 }
9615 return null;
9616}
9617
9618/**
9619 * @license
9620 * Copyright 2017 Google LLC
9621 *
9622 * Licensed under the Apache License, Version 2.0 (the "License");
9623 * you may not use this file except in compliance with the License.
9624 * You may obtain a copy of the License at
9625 *
9626 * http://www.apache.org/licenses/LICENSE-2.0
9627 *
9628 * Unless required by applicable law or agreed to in writing, software
9629 * distributed under the License is distributed on an "AS IS" BASIS,
9630 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9631 * See the License for the specific language governing permissions and
9632 * limitations under the License.
9633 */
9634let referenceConstructor;
9635function syncTreeSetReferenceConstructor(val) {
9636 assert(!referenceConstructor, '__referenceConstructor has already been defined');
9637 referenceConstructor = val;
9638}
9639function syncTreeGetReferenceConstructor() {
9640 assert(referenceConstructor, 'Reference.ts has not been loaded');
9641 return referenceConstructor;
9642}
9643/**
9644 * Static tracker for next query tag.
9645 */
9646let syncTreeNextQueryTag_ = 1;
9647/**
9648 * SyncTree is the central class for managing event callback registration, data caching, views
9649 * (query processing), and event generation. There are typically two SyncTree instances for
9650 * each Repo, one for the normal Firebase data, and one for the .info data.
9651 *
9652 * It has a number of responsibilities, including:
9653 * - Tracking all user event callbacks (registered via addEventRegistration() and removeEventRegistration()).
9654 * - Applying and caching data changes for user set(), transaction(), and update() calls
9655 * (applyUserOverwrite(), applyUserMerge()).
9656 * - Applying and caching data changes for server data changes (applyServerOverwrite(),
9657 * applyServerMerge()).
9658 * - Generating user-facing events for server and user changes (all of the apply* methods
9659 * return the set of events that need to be raised as a result).
9660 * - Maintaining the appropriate set of server listens to ensure we are always subscribed
9661 * to the correct set of paths and queries to satisfy the current set of user event
9662 * callbacks (listens are started/stopped using the provided listenProvider).
9663 *
9664 * NOTE: Although SyncTree tracks event callbacks and calculates events to raise, the actual
9665 * events are returned to the caller rather than raised synchronously.
9666 *
9667 */
9668class SyncTree {
9669 /**
9670 * @param listenProvider_ - Used by SyncTree to start / stop listening
9671 * to server data.
9672 */
9673 constructor(listenProvider_) {
9674 this.listenProvider_ = listenProvider_;
9675 /**
9676 * Tree of SyncPoints. There's a SyncPoint at any location that has 1 or more views.
9677 */
9678 this.syncPointTree_ = new ImmutableTree(null);
9679 /**
9680 * A tree of all pending user writes (user-initiated set()'s, transaction()'s, update()'s, etc.).
9681 */
9682 this.pendingWriteTree_ = newWriteTree();
9683 this.tagToQueryMap = new Map();
9684 this.queryToTagMap = new Map();
9685 }
9686}
9687/**
9688 * Apply the data changes for a user-generated set() or transaction() call.
9689 *
9690 * @returns Events to raise.
9691 */
9692function syncTreeApplyUserOverwrite(syncTree, path, newData, writeId, visible) {
9693 // Record pending write.
9694 writeTreeAddOverwrite(syncTree.pendingWriteTree_, path, newData, writeId, visible);
9695 if (!visible) {
9696 return [];
9697 }
9698 else {
9699 return syncTreeApplyOperationToSyncPoints_(syncTree, new Overwrite(newOperationSourceUser(), path, newData));
9700 }
9701}
9702/**
9703 * Apply the data from a user-generated update() call
9704 *
9705 * @returns Events to raise.
9706 */
9707function syncTreeApplyUserMerge(syncTree, path, changedChildren, writeId) {
9708 // Record pending merge.
9709 writeTreeAddMerge(syncTree.pendingWriteTree_, path, changedChildren, writeId);
9710 const changeTree = ImmutableTree.fromObject(changedChildren);
9711 return syncTreeApplyOperationToSyncPoints_(syncTree, new Merge(newOperationSourceUser(), path, changeTree));
9712}
9713/**
9714 * Acknowledge a pending user write that was previously registered with applyUserOverwrite() or applyUserMerge().
9715 *
9716 * @param revert - True if the given write failed and needs to be reverted
9717 * @returns Events to raise.
9718 */
9719function syncTreeAckUserWrite(syncTree, writeId, revert = false) {
9720 const write = writeTreeGetWrite(syncTree.pendingWriteTree_, writeId);
9721 const needToReevaluate = writeTreeRemoveWrite(syncTree.pendingWriteTree_, writeId);
9722 if (!needToReevaluate) {
9723 return [];
9724 }
9725 else {
9726 let affectedTree = new ImmutableTree(null);
9727 if (write.snap != null) {
9728 // overwrite
9729 affectedTree = affectedTree.set(newEmptyPath(), true);
9730 }
9731 else {
9732 each(write.children, (pathString) => {
9733 affectedTree = affectedTree.set(new Path(pathString), true);
9734 });
9735 }
9736 return syncTreeApplyOperationToSyncPoints_(syncTree, new AckUserWrite(write.path, affectedTree, revert));
9737 }
9738}
9739/**
9740 * Apply new server data for the specified path..
9741 *
9742 * @returns Events to raise.
9743 */
9744function syncTreeApplyServerOverwrite(syncTree, path, newData) {
9745 return syncTreeApplyOperationToSyncPoints_(syncTree, new Overwrite(newOperationSourceServer(), path, newData));
9746}
9747/**
9748 * Apply new server data to be merged in at the specified path.
9749 *
9750 * @returns Events to raise.
9751 */
9752function syncTreeApplyServerMerge(syncTree, path, changedChildren) {
9753 const changeTree = ImmutableTree.fromObject(changedChildren);
9754 return syncTreeApplyOperationToSyncPoints_(syncTree, new Merge(newOperationSourceServer(), path, changeTree));
9755}
9756/**
9757 * Apply a listen complete for a query
9758 *
9759 * @returns Events to raise.
9760 */
9761function syncTreeApplyListenComplete(syncTree, path) {
9762 return syncTreeApplyOperationToSyncPoints_(syncTree, new ListenComplete(newOperationSourceServer(), path));
9763}
9764/**
9765 * Apply a listen complete for a tagged query
9766 *
9767 * @returns Events to raise.
9768 */
9769function syncTreeApplyTaggedListenComplete(syncTree, path, tag) {
9770 const queryKey = syncTreeQueryKeyForTag_(syncTree, tag);
9771 if (queryKey) {
9772 const r = syncTreeParseQueryKey_(queryKey);
9773 const queryPath = r.path, queryId = r.queryId;
9774 const relativePath = newRelativePath(queryPath, path);
9775 const op = new ListenComplete(newOperationSourceServerTaggedQuery(queryId), relativePath);
9776 return syncTreeApplyTaggedOperation_(syncTree, queryPath, op);
9777 }
9778 else {
9779 // We've already removed the query. No big deal, ignore the update
9780 return [];
9781 }
9782}
9783/**
9784 * Remove event callback(s).
9785 *
9786 * If query is the default query, we'll check all queries for the specified eventRegistration.
9787 * If eventRegistration is null, we'll remove all callbacks for the specified query/queries.
9788 *
9789 * @param eventRegistration - If null, all callbacks are removed.
9790 * @param cancelError - If a cancelError is provided, appropriate cancel events will be returned.
9791 * @returns Cancel events, if cancelError was provided.
9792 */
9793function syncTreeRemoveEventRegistration(syncTree, query, eventRegistration, cancelError) {
9794 // Find the syncPoint first. Then deal with whether or not it has matching listeners
9795 const path = query._path;
9796 const maybeSyncPoint = syncTree.syncPointTree_.get(path);
9797 let cancelEvents = [];
9798 // A removal on a default query affects all queries at that location. A removal on an indexed query, even one without
9799 // other query constraints, does *not* affect all queries at that location. So this check must be for 'default', and
9800 // not loadsAllData().
9801 if (maybeSyncPoint &&
9802 (query._queryIdentifier === 'default' ||
9803 syncPointViewExistsForQuery(maybeSyncPoint, query))) {
9804 const removedAndEvents = syncPointRemoveEventRegistration(maybeSyncPoint, query, eventRegistration, cancelError);
9805 if (syncPointIsEmpty(maybeSyncPoint)) {
9806 syncTree.syncPointTree_ = syncTree.syncPointTree_.remove(path);
9807 }
9808 const removed = removedAndEvents.removed;
9809 cancelEvents = removedAndEvents.events;
9810 // We may have just removed one of many listeners and can short-circuit this whole process
9811 // We may also not have removed a default listener, in which case all of the descendant listeners should already be
9812 // properly set up.
9813 //
9814 // Since indexed queries can shadow if they don't have other query constraints, check for loadsAllData(), instead of
9815 // queryId === 'default'
9816 const removingDefault = -1 !==
9817 removed.findIndex(query => {
9818 return query._queryParams.loadsAllData();
9819 });
9820 const covered = syncTree.syncPointTree_.findOnPath(path, (relativePath, parentSyncPoint) => syncPointHasCompleteView(parentSyncPoint));
9821 if (removingDefault && !covered) {
9822 const subtree = syncTree.syncPointTree_.subtree(path);
9823 // There are potentially child listeners. Determine what if any listens we need to send before executing the
9824 // removal
9825 if (!subtree.isEmpty()) {
9826 // We need to fold over our subtree and collect the listeners to send
9827 const newViews = syncTreeCollectDistinctViewsForSubTree_(subtree);
9828 // Ok, we've collected all the listens we need. Set them up.
9829 for (let i = 0; i < newViews.length; ++i) {
9830 const view = newViews[i], newQuery = view.query;
9831 const listener = syncTreeCreateListenerForView_(syncTree, view);
9832 syncTree.listenProvider_.startListening(syncTreeQueryForListening_(newQuery), syncTreeTagForQuery_(syncTree, newQuery), listener.hashFn, listener.onComplete);
9833 }
9834 }
9835 }
9836 // If we removed anything and we're not covered by a higher up listen, we need to stop listening on this query
9837 // The above block has us covered in terms of making sure we're set up on listens lower in the tree.
9838 // Also, note that if we have a cancelError, it's already been removed at the provider level.
9839 if (!covered && removed.length > 0 && !cancelError) {
9840 // If we removed a default, then we weren't listening on any of the other queries here. Just cancel the one
9841 // default. Otherwise, we need to iterate through and cancel each individual query
9842 if (removingDefault) {
9843 // We don't tag default listeners
9844 const defaultTag = null;
9845 syncTree.listenProvider_.stopListening(syncTreeQueryForListening_(query), defaultTag);
9846 }
9847 else {
9848 removed.forEach((queryToRemove) => {
9849 const tagToRemove = syncTree.queryToTagMap.get(syncTreeMakeQueryKey_(queryToRemove));
9850 syncTree.listenProvider_.stopListening(syncTreeQueryForListening_(queryToRemove), tagToRemove);
9851 });
9852 }
9853 }
9854 // Now, clear all of the tags we're tracking for the removed listens
9855 syncTreeRemoveTags_(syncTree, removed);
9856 }
9857 return cancelEvents;
9858}
9859/**
9860 * Apply new server data for the specified tagged query.
9861 *
9862 * @returns Events to raise.
9863 */
9864function syncTreeApplyTaggedQueryOverwrite(syncTree, path, snap, tag) {
9865 const queryKey = syncTreeQueryKeyForTag_(syncTree, tag);
9866 if (queryKey != null) {
9867 const r = syncTreeParseQueryKey_(queryKey);
9868 const queryPath = r.path, queryId = r.queryId;
9869 const relativePath = newRelativePath(queryPath, path);
9870 const op = new Overwrite(newOperationSourceServerTaggedQuery(queryId), relativePath, snap);
9871 return syncTreeApplyTaggedOperation_(syncTree, queryPath, op);
9872 }
9873 else {
9874 // Query must have been removed already
9875 return [];
9876 }
9877}
9878/**
9879 * Apply server data to be merged in for the specified tagged query.
9880 *
9881 * @returns Events to raise.
9882 */
9883function syncTreeApplyTaggedQueryMerge(syncTree, path, changedChildren, tag) {
9884 const queryKey = syncTreeQueryKeyForTag_(syncTree, tag);
9885 if (queryKey) {
9886 const r = syncTreeParseQueryKey_(queryKey);
9887 const queryPath = r.path, queryId = r.queryId;
9888 const relativePath = newRelativePath(queryPath, path);
9889 const changeTree = ImmutableTree.fromObject(changedChildren);
9890 const op = new Merge(newOperationSourceServerTaggedQuery(queryId), relativePath, changeTree);
9891 return syncTreeApplyTaggedOperation_(syncTree, queryPath, op);
9892 }
9893 else {
9894 // We've already removed the query. No big deal, ignore the update
9895 return [];
9896 }
9897}
9898/**
9899 * Add an event callback for the specified query.
9900 *
9901 * @returns Events to raise.
9902 */
9903function syncTreeAddEventRegistration(syncTree, query, eventRegistration) {
9904 const path = query._path;
9905 let serverCache = null;
9906 let foundAncestorDefaultView = false;
9907 // Any covering writes will necessarily be at the root, so really all we need to find is the server cache.
9908 // Consider optimizing this once there's a better understanding of what actual behavior will be.
9909 syncTree.syncPointTree_.foreachOnPath(path, (pathToSyncPoint, sp) => {
9910 const relativePath = newRelativePath(pathToSyncPoint, path);
9911 serverCache =
9912 serverCache || syncPointGetCompleteServerCache(sp, relativePath);
9913 foundAncestorDefaultView =
9914 foundAncestorDefaultView || syncPointHasCompleteView(sp);
9915 });
9916 let syncPoint = syncTree.syncPointTree_.get(path);
9917 if (!syncPoint) {
9918 syncPoint = new SyncPoint();
9919 syncTree.syncPointTree_ = syncTree.syncPointTree_.set(path, syncPoint);
9920 }
9921 else {
9922 foundAncestorDefaultView =
9923 foundAncestorDefaultView || syncPointHasCompleteView(syncPoint);
9924 serverCache =
9925 serverCache || syncPointGetCompleteServerCache(syncPoint, newEmptyPath());
9926 }
9927 let serverCacheComplete;
9928 if (serverCache != null) {
9929 serverCacheComplete = true;
9930 }
9931 else {
9932 serverCacheComplete = false;
9933 serverCache = ChildrenNode.EMPTY_NODE;
9934 const subtree = syncTree.syncPointTree_.subtree(path);
9935 subtree.foreachChild((childName, childSyncPoint) => {
9936 const completeCache = syncPointGetCompleteServerCache(childSyncPoint, newEmptyPath());
9937 if (completeCache) {
9938 serverCache = serverCache.updateImmediateChild(childName, completeCache);
9939 }
9940 });
9941 }
9942 const viewAlreadyExists = syncPointViewExistsForQuery(syncPoint, query);
9943 if (!viewAlreadyExists && !query._queryParams.loadsAllData()) {
9944 // We need to track a tag for this query
9945 const queryKey = syncTreeMakeQueryKey_(query);
9946 assert(!syncTree.queryToTagMap.has(queryKey), 'View does not exist, but we have a tag');
9947 const tag = syncTreeGetNextQueryTag_();
9948 syncTree.queryToTagMap.set(queryKey, tag);
9949 syncTree.tagToQueryMap.set(tag, queryKey);
9950 }
9951 const writesCache = writeTreeChildWrites(syncTree.pendingWriteTree_, path);
9952 let events = syncPointAddEventRegistration(syncPoint, query, eventRegistration, writesCache, serverCache, serverCacheComplete);
9953 if (!viewAlreadyExists && !foundAncestorDefaultView) {
9954 const view = syncPointViewForQuery(syncPoint, query);
9955 events = events.concat(syncTreeSetupListener_(syncTree, query, view));
9956 }
9957 return events;
9958}
9959/**
9960 * Returns a complete cache, if we have one, of the data at a particular path. If the location does not have a
9961 * listener above it, we will get a false "null". This shouldn't be a problem because transactions will always
9962 * have a listener above, and atomic operations would correctly show a jitter of <increment value> ->
9963 * <incremented total> as the write is applied locally and then acknowledged at the server.
9964 *
9965 * Note: this method will *include* hidden writes from transaction with applyLocally set to false.
9966 *
9967 * @param path - The path to the data we want
9968 * @param writeIdsToExclude - A specific set to be excluded
9969 */
9970function syncTreeCalcCompleteEventCache(syncTree, path, writeIdsToExclude) {
9971 const includeHiddenSets = true;
9972 const writeTree = syncTree.pendingWriteTree_;
9973 const serverCache = syncTree.syncPointTree_.findOnPath(path, (pathSoFar, syncPoint) => {
9974 const relativePath = newRelativePath(pathSoFar, path);
9975 const serverCache = syncPointGetCompleteServerCache(syncPoint, relativePath);
9976 if (serverCache) {
9977 return serverCache;
9978 }
9979 });
9980 return writeTreeCalcCompleteEventCache(writeTree, path, serverCache, writeIdsToExclude, includeHiddenSets);
9981}
9982function syncTreeGetServerValue(syncTree, query) {
9983 const path = query._path;
9984 let serverCache = null;
9985 // Any covering writes will necessarily be at the root, so really all we need to find is the server cache.
9986 // Consider optimizing this once there's a better understanding of what actual behavior will be.
9987 syncTree.syncPointTree_.foreachOnPath(path, (pathToSyncPoint, sp) => {
9988 const relativePath = newRelativePath(pathToSyncPoint, path);
9989 serverCache =
9990 serverCache || syncPointGetCompleteServerCache(sp, relativePath);
9991 });
9992 let syncPoint = syncTree.syncPointTree_.get(path);
9993 if (!syncPoint) {
9994 syncPoint = new SyncPoint();
9995 syncTree.syncPointTree_ = syncTree.syncPointTree_.set(path, syncPoint);
9996 }
9997 else {
9998 serverCache =
9999 serverCache || syncPointGetCompleteServerCache(syncPoint, newEmptyPath());
10000 }
10001 const serverCacheComplete = serverCache != null;
10002 const serverCacheNode = serverCacheComplete
10003 ? new CacheNode(serverCache, true, false)
10004 : null;
10005 const writesCache = writeTreeChildWrites(syncTree.pendingWriteTree_, query._path);
10006 const view = syncPointGetView(syncPoint, query, writesCache, serverCacheComplete ? serverCacheNode.getNode() : ChildrenNode.EMPTY_NODE, serverCacheComplete);
10007 return viewGetCompleteNode(view);
10008}
10009/**
10010 * A helper method that visits all descendant and ancestor SyncPoints, applying the operation.
10011 *
10012 * NOTES:
10013 * - Descendant SyncPoints will be visited first (since we raise events depth-first).
10014 *
10015 * - We call applyOperation() on each SyncPoint passing three things:
10016 * 1. A version of the Operation that has been made relative to the SyncPoint location.
10017 * 2. A WriteTreeRef of any writes we have cached at the SyncPoint location.
10018 * 3. A snapshot Node with cached server data, if we have it.
10019 *
10020 * - We concatenate all of the events returned by each SyncPoint and return the result.
10021 */
10022function syncTreeApplyOperationToSyncPoints_(syncTree, operation) {
10023 return syncTreeApplyOperationHelper_(operation, syncTree.syncPointTree_,
10024 /*serverCache=*/ null, writeTreeChildWrites(syncTree.pendingWriteTree_, newEmptyPath()));
10025}
10026/**
10027 * Recursive helper for applyOperationToSyncPoints_
10028 */
10029function syncTreeApplyOperationHelper_(operation, syncPointTree, serverCache, writesCache) {
10030 if (pathIsEmpty(operation.path)) {
10031 return syncTreeApplyOperationDescendantsHelper_(operation, syncPointTree, serverCache, writesCache);
10032 }
10033 else {
10034 const syncPoint = syncPointTree.get(newEmptyPath());
10035 // If we don't have cached server data, see if we can get it from this SyncPoint.
10036 if (serverCache == null && syncPoint != null) {
10037 serverCache = syncPointGetCompleteServerCache(syncPoint, newEmptyPath());
10038 }
10039 let events = [];
10040 const childName = pathGetFront(operation.path);
10041 const childOperation = operation.operationForChild(childName);
10042 const childTree = syncPointTree.children.get(childName);
10043 if (childTree && childOperation) {
10044 const childServerCache = serverCache
10045 ? serverCache.getImmediateChild(childName)
10046 : null;
10047 const childWritesCache = writeTreeRefChild(writesCache, childName);
10048 events = events.concat(syncTreeApplyOperationHelper_(childOperation, childTree, childServerCache, childWritesCache));
10049 }
10050 if (syncPoint) {
10051 events = events.concat(syncPointApplyOperation(syncPoint, operation, writesCache, serverCache));
10052 }
10053 return events;
10054 }
10055}
10056/**
10057 * Recursive helper for applyOperationToSyncPoints_
10058 */
10059function syncTreeApplyOperationDescendantsHelper_(operation, syncPointTree, serverCache, writesCache) {
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 syncPointTree.children.inorderTraversal((childName, childTree) => {
10067 const childServerCache = serverCache
10068 ? serverCache.getImmediateChild(childName)
10069 : null;
10070 const childWritesCache = writeTreeRefChild(writesCache, childName);
10071 const childOperation = operation.operationForChild(childName);
10072 if (childOperation) {
10073 events = events.concat(syncTreeApplyOperationDescendantsHelper_(childOperation, childTree, childServerCache, childWritesCache));
10074 }
10075 });
10076 if (syncPoint) {
10077 events = events.concat(syncPointApplyOperation(syncPoint, operation, writesCache, serverCache));
10078 }
10079 return events;
10080}
10081function syncTreeCreateListenerForView_(syncTree, view) {
10082 const query = view.query;
10083 const tag = syncTreeTagForQuery_(syncTree, query);
10084 return {
10085 hashFn: () => {
10086 const cache = viewGetServerCache(view) || ChildrenNode.EMPTY_NODE;
10087 return cache.hash();
10088 },
10089 onComplete: (status) => {
10090 if (status === 'ok') {
10091 if (tag) {
10092 return syncTreeApplyTaggedListenComplete(syncTree, query._path, tag);
10093 }
10094 else {
10095 return syncTreeApplyListenComplete(syncTree, query._path);
10096 }
10097 }
10098 else {
10099 // If a listen failed, kill all of the listeners here, not just the one that triggered the error.
10100 // Note that this may need to be scoped to just this listener if we change permissions on filtered children
10101 const error = errorForServerCode(status, query);
10102 return syncTreeRemoveEventRegistration(syncTree, query,
10103 /*eventRegistration*/ null, error);
10104 }
10105 }
10106 };
10107}
10108/**
10109 * Return the tag associated with the given query.
10110 */
10111function syncTreeTagForQuery_(syncTree, query) {
10112 const queryKey = syncTreeMakeQueryKey_(query);
10113 return syncTree.queryToTagMap.get(queryKey);
10114}
10115/**
10116 * Given a query, computes a "queryKey" suitable for use in our queryToTagMap_.
10117 */
10118function syncTreeMakeQueryKey_(query) {
10119 return query._path.toString() + '$' + query._queryIdentifier;
10120}
10121/**
10122 * Return the query associated with the given tag, if we have one
10123 */
10124function syncTreeQueryKeyForTag_(syncTree, tag) {
10125 return syncTree.tagToQueryMap.get(tag);
10126}
10127/**
10128 * Given a queryKey (created by makeQueryKey), parse it back into a path and queryId.
10129 */
10130function syncTreeParseQueryKey_(queryKey) {
10131 const splitIndex = queryKey.indexOf('$');
10132 assert(splitIndex !== -1 && splitIndex < queryKey.length - 1, 'Bad queryKey.');
10133 return {
10134 queryId: queryKey.substr(splitIndex + 1),
10135 path: new Path(queryKey.substr(0, splitIndex))
10136 };
10137}
10138/**
10139 * A helper method to apply tagged operations
10140 */
10141function syncTreeApplyTaggedOperation_(syncTree, queryPath, operation) {
10142 const syncPoint = syncTree.syncPointTree_.get(queryPath);
10143 assert(syncPoint, "Missing sync point for query tag that we're tracking");
10144 const writesCache = writeTreeChildWrites(syncTree.pendingWriteTree_, queryPath);
10145 return syncPointApplyOperation(syncPoint, operation, writesCache, null);
10146}
10147/**
10148 * This collapses multiple unfiltered views into a single view, since we only need a single
10149 * listener for them.
10150 */
10151function syncTreeCollectDistinctViewsForSubTree_(subtree) {
10152 return subtree.fold((relativePath, maybeChildSyncPoint, childMap) => {
10153 if (maybeChildSyncPoint && syncPointHasCompleteView(maybeChildSyncPoint)) {
10154 const completeView = syncPointGetCompleteView(maybeChildSyncPoint);
10155 return [completeView];
10156 }
10157 else {
10158 // No complete view here, flatten any deeper listens into an array
10159 let views = [];
10160 if (maybeChildSyncPoint) {
10161 views = syncPointGetQueryViews(maybeChildSyncPoint);
10162 }
10163 each(childMap, (_key, childViews) => {
10164 views = views.concat(childViews);
10165 });
10166 return views;
10167 }
10168 });
10169}
10170/**
10171 * Normalizes a query to a query we send the server for listening
10172 *
10173 * @returns The normalized query
10174 */
10175function syncTreeQueryForListening_(query) {
10176 if (query._queryParams.loadsAllData() && !query._queryParams.isDefault()) {
10177 // We treat queries that load all data as default queries
10178 // Cast is necessary because ref() technically returns Firebase which is actually fb.api.Firebase which inherits
10179 // from Query
10180 return new (syncTreeGetReferenceConstructor())(query._repo, query._path);
10181 }
10182 else {
10183 return query;
10184 }
10185}
10186function syncTreeRemoveTags_(syncTree, queries) {
10187 for (let j = 0; j < queries.length; ++j) {
10188 const removedQuery = queries[j];
10189 if (!removedQuery._queryParams.loadsAllData()) {
10190 // We should have a tag for this
10191 const removedQueryKey = syncTreeMakeQueryKey_(removedQuery);
10192 const removedQueryTag = syncTree.queryToTagMap.get(removedQueryKey);
10193 syncTree.queryToTagMap.delete(removedQueryKey);
10194 syncTree.tagToQueryMap.delete(removedQueryTag);
10195 }
10196 }
10197}
10198/**
10199 * Static accessor for query tags.
10200 */
10201function syncTreeGetNextQueryTag_() {
10202 return syncTreeNextQueryTag_++;
10203}
10204/**
10205 * For a given new listen, manage the de-duplication of outstanding subscriptions.
10206 *
10207 * @returns This method can return events to support synchronous data sources
10208 */
10209function syncTreeSetupListener_(syncTree, query, view) {
10210 const path = query._path;
10211 const tag = syncTreeTagForQuery_(syncTree, query);
10212 const listener = syncTreeCreateListenerForView_(syncTree, view);
10213 const events = syncTree.listenProvider_.startListening(syncTreeQueryForListening_(query), tag, listener.hashFn, listener.onComplete);
10214 const subtree = syncTree.syncPointTree_.subtree(path);
10215 // The root of this subtree has our query. We're here because we definitely need to send a listen for that, but we
10216 // may need to shadow other listens as well.
10217 if (tag) {
10218 assert(!syncPointHasCompleteView(subtree.value), "If we're adding a query, it shouldn't be shadowed");
10219 }
10220 else {
10221 // Shadow everything at or below this location, this is a default listener.
10222 const queriesToStop = subtree.fold((relativePath, maybeChildSyncPoint, childMap) => {
10223 if (!pathIsEmpty(relativePath) &&
10224 maybeChildSyncPoint &&
10225 syncPointHasCompleteView(maybeChildSyncPoint)) {
10226 return [syncPointGetCompleteView(maybeChildSyncPoint).query];
10227 }
10228 else {
10229 // No default listener here, flatten any deeper queries into an array
10230 let queries = [];
10231 if (maybeChildSyncPoint) {
10232 queries = queries.concat(syncPointGetQueryViews(maybeChildSyncPoint).map(view => view.query));
10233 }
10234 each(childMap, (_key, childQueries) => {
10235 queries = queries.concat(childQueries);
10236 });
10237 return queries;
10238 }
10239 });
10240 for (let i = 0; i < queriesToStop.length; ++i) {
10241 const queryToStop = queriesToStop[i];
10242 syncTree.listenProvider_.stopListening(syncTreeQueryForListening_(queryToStop), syncTreeTagForQuery_(syncTree, queryToStop));
10243 }
10244 }
10245 return events;
10246}
10247
10248/**
10249 * @license
10250 * Copyright 2017 Google LLC
10251 *
10252 * Licensed under the Apache License, Version 2.0 (the "License");
10253 * you may not use this file except in compliance with the License.
10254 * You may obtain a copy of the License at
10255 *
10256 * http://www.apache.org/licenses/LICENSE-2.0
10257 *
10258 * Unless required by applicable law or agreed to in writing, software
10259 * distributed under the License is distributed on an "AS IS" BASIS,
10260 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10261 * See the License for the specific language governing permissions and
10262 * limitations under the License.
10263 */
10264class ExistingValueProvider {
10265 constructor(node_) {
10266 this.node_ = node_;
10267 }
10268 getImmediateChild(childName) {
10269 const child = this.node_.getImmediateChild(childName);
10270 return new ExistingValueProvider(child);
10271 }
10272 node() {
10273 return this.node_;
10274 }
10275}
10276class DeferredValueProvider {
10277 constructor(syncTree, path) {
10278 this.syncTree_ = syncTree;
10279 this.path_ = path;
10280 }
10281 getImmediateChild(childName) {
10282 const childPath = pathChild(this.path_, childName);
10283 return new DeferredValueProvider(this.syncTree_, childPath);
10284 }
10285 node() {
10286 return syncTreeCalcCompleteEventCache(this.syncTree_, this.path_);
10287 }
10288}
10289/**
10290 * Generate placeholders for deferred values.
10291 */
10292const generateWithValues = function (values) {
10293 values = values || {};
10294 values['timestamp'] = values['timestamp'] || new Date().getTime();
10295 return values;
10296};
10297/**
10298 * Value to use when firing local events. When writing server values, fire
10299 * local events with an approximate value, otherwise return value as-is.
10300 */
10301const resolveDeferredLeafValue = function (value, existingVal, serverValues) {
10302 if (!value || typeof value !== 'object') {
10303 return value;
10304 }
10305 assert('.sv' in value, 'Unexpected leaf node or priority contents');
10306 if (typeof value['.sv'] === 'string') {
10307 return resolveScalarDeferredValue(value['.sv'], existingVal, serverValues);
10308 }
10309 else if (typeof value['.sv'] === 'object') {
10310 return resolveComplexDeferredValue(value['.sv'], existingVal);
10311 }
10312 else {
10313 assert(false, 'Unexpected server value: ' + JSON.stringify(value, null, 2));
10314 }
10315};
10316const resolveScalarDeferredValue = function (op, existing, serverValues) {
10317 switch (op) {
10318 case 'timestamp':
10319 return serverValues['timestamp'];
10320 default:
10321 assert(false, 'Unexpected server value: ' + op);
10322 }
10323};
10324const resolveComplexDeferredValue = function (op, existing, unused) {
10325 if (!op.hasOwnProperty('increment')) {
10326 assert(false, 'Unexpected server value: ' + JSON.stringify(op, null, 2));
10327 }
10328 const delta = op['increment'];
10329 if (typeof delta !== 'number') {
10330 assert(false, 'Unexpected increment value: ' + delta);
10331 }
10332 const existingNode = existing.node();
10333 assert(existingNode !== null && typeof existingNode !== 'undefined', 'Expected ChildrenNode.EMPTY_NODE for nulls');
10334 // Incrementing a non-number sets the value to the incremented amount
10335 if (!existingNode.isLeafNode()) {
10336 return delta;
10337 }
10338 const leaf = existingNode;
10339 const existingVal = leaf.getValue();
10340 if (typeof existingVal !== 'number') {
10341 return delta;
10342 }
10343 // No need to do over/underflow arithmetic here because JS only handles floats under the covers
10344 return existingVal + delta;
10345};
10346/**
10347 * Recursively replace all deferred values and priorities in the tree with the
10348 * specified generated replacement values.
10349 * @param path - path to which write is relative
10350 * @param node - new data written at path
10351 * @param syncTree - current data
10352 */
10353const resolveDeferredValueTree = function (path, node, syncTree, serverValues) {
10354 return resolveDeferredValue(node, new DeferredValueProvider(syncTree, path), serverValues);
10355};
10356/**
10357 * Recursively replace all deferred values and priorities in the node with the
10358 * specified generated replacement values. If there are no server values in the node,
10359 * it'll be returned as-is.
10360 */
10361const resolveDeferredValueSnapshot = function (node, existing, serverValues) {
10362 return resolveDeferredValue(node, new ExistingValueProvider(existing), serverValues);
10363};
10364function resolveDeferredValue(node, existingVal, serverValues) {
10365 const rawPri = node.getPriority().val();
10366 const priority = resolveDeferredLeafValue(rawPri, existingVal.getImmediateChild('.priority'), serverValues);
10367 let newNode;
10368 if (node.isLeafNode()) {
10369 const leafNode = node;
10370 const value = resolveDeferredLeafValue(leafNode.getValue(), existingVal, serverValues);
10371 if (value !== leafNode.getValue() ||
10372 priority !== leafNode.getPriority().val()) {
10373 return new LeafNode(value, nodeFromJSON(priority));
10374 }
10375 else {
10376 return node;
10377 }
10378 }
10379 else {
10380 const childrenNode = node;
10381 newNode = childrenNode;
10382 if (priority !== childrenNode.getPriority().val()) {
10383 newNode = newNode.updatePriority(new LeafNode(priority));
10384 }
10385 childrenNode.forEachChild(PRIORITY_INDEX, (childName, childNode) => {
10386 const newChildNode = resolveDeferredValue(childNode, existingVal.getImmediateChild(childName), serverValues);
10387 if (newChildNode !== childNode) {
10388 newNode = newNode.updateImmediateChild(childName, newChildNode);
10389 }
10390 });
10391 return newNode;
10392 }
10393}
10394
10395/**
10396 * @license
10397 * Copyright 2017 Google LLC
10398 *
10399 * Licensed under the Apache License, Version 2.0 (the "License");
10400 * you may not use this file except in compliance with the License.
10401 * You may obtain a copy of the License at
10402 *
10403 * http://www.apache.org/licenses/LICENSE-2.0
10404 *
10405 * Unless required by applicable law or agreed to in writing, software
10406 * distributed under the License is distributed on an "AS IS" BASIS,
10407 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10408 * See the License for the specific language governing permissions and
10409 * limitations under the License.
10410 */
10411/**
10412 * A light-weight tree, traversable by path. Nodes can have both values and children.
10413 * Nodes are not enumerated (by forEachChild) unless they have a value or non-empty
10414 * children.
10415 */
10416class Tree {
10417 /**
10418 * @param name - Optional name of the node.
10419 * @param parent - Optional parent node.
10420 * @param node - Optional node to wrap.
10421 */
10422 constructor(name = '', parent = null, node = { children: {}, childCount: 0 }) {
10423 this.name = name;
10424 this.parent = parent;
10425 this.node = node;
10426 }
10427}
10428/**
10429 * Returns a sub-Tree for the given path.
10430 *
10431 * @param pathObj - Path to look up.
10432 * @returns Tree for path.
10433 */
10434function treeSubTree(tree, pathObj) {
10435 // TODO: Require pathObj to be Path?
10436 let path = pathObj instanceof Path ? pathObj : new Path(pathObj);
10437 let child = tree, next = pathGetFront(path);
10438 while (next !== null) {
10439 const childNode = safeGet(child.node.children, next) || {
10440 children: {},
10441 childCount: 0
10442 };
10443 child = new Tree(next, child, childNode);
10444 path = pathPopFront(path);
10445 next = pathGetFront(path);
10446 }
10447 return child;
10448}
10449/**
10450 * Returns the data associated with this tree node.
10451 *
10452 * @returns The data or null if no data exists.
10453 */
10454function treeGetValue(tree) {
10455 return tree.node.value;
10456}
10457/**
10458 * Sets data to this tree node.
10459 *
10460 * @param value - Value to set.
10461 */
10462function treeSetValue(tree, value) {
10463 tree.node.value = value;
10464 treeUpdateParents(tree);
10465}
10466/**
10467 * @returns Whether the tree has any children.
10468 */
10469function treeHasChildren(tree) {
10470 return tree.node.childCount > 0;
10471}
10472/**
10473 * @returns Whethe rthe tree is empty (no value or children).
10474 */
10475function treeIsEmpty(tree) {
10476 return treeGetValue(tree) === undefined && !treeHasChildren(tree);
10477}
10478/**
10479 * Calls action for each child of this tree node.
10480 *
10481 * @param action - Action to be called for each child.
10482 */
10483function treeForEachChild(tree, action) {
10484 each(tree.node.children, (child, childTree) => {
10485 action(new Tree(child, tree, childTree));
10486 });
10487}
10488/**
10489 * Does a depth-first traversal of this node's descendants, calling action for each one.
10490 *
10491 * @param action - Action to be called for each child.
10492 * @param includeSelf - Whether to call action on this node as well. Defaults to
10493 * false.
10494 * @param childrenFirst - Whether to call action on children before calling it on
10495 * parent.
10496 */
10497function treeForEachDescendant(tree, action, includeSelf, childrenFirst) {
10498 if (includeSelf && !childrenFirst) {
10499 action(tree);
10500 }
10501 treeForEachChild(tree, child => {
10502 treeForEachDescendant(child, action, true, childrenFirst);
10503 });
10504 if (includeSelf && childrenFirst) {
10505 action(tree);
10506 }
10507}
10508/**
10509 * Calls action on each ancestor node.
10510 *
10511 * @param action - Action to be called on each parent; return
10512 * true to abort.
10513 * @param includeSelf - Whether to call action on this node as well.
10514 * @returns true if the action callback returned true.
10515 */
10516function treeForEachAncestor(tree, action, includeSelf) {
10517 let node = includeSelf ? tree : tree.parent;
10518 while (node !== null) {
10519 if (action(node)) {
10520 return true;
10521 }
10522 node = node.parent;
10523 }
10524 return false;
10525}
10526/**
10527 * @returns The path of this tree node, as a Path.
10528 */
10529function treeGetPath(tree) {
10530 return new Path(tree.parent === null
10531 ? tree.name
10532 : treeGetPath(tree.parent) + '/' + tree.name);
10533}
10534/**
10535 * Adds or removes this child from its parent based on whether it's empty or not.
10536 */
10537function treeUpdateParents(tree) {
10538 if (tree.parent !== null) {
10539 treeUpdateChild(tree.parent, tree.name, tree);
10540 }
10541}
10542/**
10543 * Adds or removes the passed child to this tree node, depending on whether it's empty.
10544 *
10545 * @param childName - The name of the child to update.
10546 * @param child - The child to update.
10547 */
10548function treeUpdateChild(tree, childName, child) {
10549 const childEmpty = treeIsEmpty(child);
10550 const childExists = contains(tree.node.children, childName);
10551 if (childEmpty && childExists) {
10552 delete tree.node.children[childName];
10553 tree.node.childCount--;
10554 treeUpdateParents(tree);
10555 }
10556 else if (!childEmpty && !childExists) {
10557 tree.node.children[childName] = child.node;
10558 tree.node.childCount++;
10559 treeUpdateParents(tree);
10560 }
10561}
10562
10563/**
10564 * @license
10565 * Copyright 2017 Google LLC
10566 *
10567 * Licensed under the Apache License, Version 2.0 (the "License");
10568 * you may not use this file except in compliance with the License.
10569 * You may obtain a copy of the License at
10570 *
10571 * http://www.apache.org/licenses/LICENSE-2.0
10572 *
10573 * Unless required by applicable law or agreed to in writing, software
10574 * distributed under the License is distributed on an "AS IS" BASIS,
10575 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10576 * See the License for the specific language governing permissions and
10577 * limitations under the License.
10578 */
10579/**
10580 * True for invalid Firebase keys
10581 */
10582const INVALID_KEY_REGEX_ = /[\[\].#$\/\u0000-\u001F\u007F]/;
10583/**
10584 * True for invalid Firebase paths.
10585 * Allows '/' in paths.
10586 */
10587const INVALID_PATH_REGEX_ = /[\[\].#$\u0000-\u001F\u007F]/;
10588/**
10589 * Maximum number of characters to allow in leaf value
10590 */
10591const MAX_LEAF_SIZE_ = 10 * 1024 * 1024;
10592const isValidKey = function (key) {
10593 return (typeof key === 'string' && key.length !== 0 && !INVALID_KEY_REGEX_.test(key));
10594};
10595const isValidPathString = function (pathString) {
10596 return (typeof pathString === 'string' &&
10597 pathString.length !== 0 &&
10598 !INVALID_PATH_REGEX_.test(pathString));
10599};
10600const isValidRootPathString = function (pathString) {
10601 if (pathString) {
10602 // Allow '/.info/' at the beginning.
10603 pathString = pathString.replace(/^\/*\.info(\/|$)/, '/');
10604 }
10605 return isValidPathString(pathString);
10606};
10607const isValidPriority = function (priority) {
10608 return (priority === null ||
10609 typeof priority === 'string' ||
10610 (typeof priority === 'number' && !isInvalidJSONNumber(priority)) ||
10611 (priority &&
10612 typeof priority === 'object' &&
10613 // eslint-disable-next-line @typescript-eslint/no-explicit-any
10614 contains(priority, '.sv')));
10615};
10616/**
10617 * Pre-validate a datum passed as an argument to Firebase function.
10618 */
10619const validateFirebaseDataArg = function (fnName, value, path, optional) {
10620 if (optional && value === undefined) {
10621 return;
10622 }
10623 validateFirebaseData(errorPrefix(fnName, 'value'), value, path);
10624};
10625/**
10626 * Validate a data object client-side before sending to server.
10627 */
10628const validateFirebaseData = function (errorPrefix, data, path_) {
10629 const path = path_ instanceof Path ? new ValidationPath(path_, errorPrefix) : path_;
10630 if (data === undefined) {
10631 throw new Error(errorPrefix + 'contains undefined ' + validationPathToErrorString(path));
10632 }
10633 if (typeof data === 'function') {
10634 throw new Error(errorPrefix +
10635 'contains a function ' +
10636 validationPathToErrorString(path) +
10637 ' with contents = ' +
10638 data.toString());
10639 }
10640 if (isInvalidJSONNumber(data)) {
10641 throw new Error(errorPrefix +
10642 'contains ' +
10643 data.toString() +
10644 ' ' +
10645 validationPathToErrorString(path));
10646 }
10647 // Check max leaf size, but try to avoid the utf8 conversion if we can.
10648 if (typeof data === 'string' &&
10649 data.length > MAX_LEAF_SIZE_ / 3 &&
10650 stringLength(data) > MAX_LEAF_SIZE_) {
10651 throw new Error(errorPrefix +
10652 'contains a string greater than ' +
10653 MAX_LEAF_SIZE_ +
10654 ' utf8 bytes ' +
10655 validationPathToErrorString(path) +
10656 " ('" +
10657 data.substring(0, 50) +
10658 "...')");
10659 }
10660 // TODO = Perf = Consider combining the recursive validation of keys into NodeFromJSON
10661 // to save extra walking of large objects.
10662 if (data && typeof data === 'object') {
10663 let hasDotValue = false;
10664 let hasActualChild = false;
10665 each(data, (key, value) => {
10666 if (key === '.value') {
10667 hasDotValue = true;
10668 }
10669 else if (key !== '.priority' && key !== '.sv') {
10670 hasActualChild = true;
10671 if (!isValidKey(key)) {
10672 throw new Error(errorPrefix +
10673 ' contains an invalid key (' +
10674 key +
10675 ') ' +
10676 validationPathToErrorString(path) +
10677 '. Keys must be non-empty strings ' +
10678 'and can\'t contain ".", "#", "$", "/", "[", or "]"');
10679 }
10680 }
10681 validationPathPush(path, key);
10682 validateFirebaseData(errorPrefix, value, path);
10683 validationPathPop(path);
10684 });
10685 if (hasDotValue && hasActualChild) {
10686 throw new Error(errorPrefix +
10687 ' contains ".value" child ' +
10688 validationPathToErrorString(path) +
10689 ' in addition to actual children.');
10690 }
10691 }
10692};
10693/**
10694 * Pre-validate paths passed in the firebase function.
10695 */
10696const validateFirebaseMergePaths = function (errorPrefix, mergePaths) {
10697 let i, curPath;
10698 for (i = 0; i < mergePaths.length; i++) {
10699 curPath = mergePaths[i];
10700 const keys = pathSlice(curPath);
10701 for (let j = 0; j < keys.length; j++) {
10702 if (keys[j] === '.priority' && j === keys.length - 1) ;
10703 else if (!isValidKey(keys[j])) {
10704 throw new Error(errorPrefix +
10705 'contains an invalid key (' +
10706 keys[j] +
10707 ') in path ' +
10708 curPath.toString() +
10709 '. Keys must be non-empty strings ' +
10710 'and can\'t contain ".", "#", "$", "/", "[", or "]"');
10711 }
10712 }
10713 }
10714 // Check that update keys are not descendants of each other.
10715 // We rely on the property that sorting guarantees that ancestors come
10716 // right before descendants.
10717 mergePaths.sort(pathCompare);
10718 let prevPath = null;
10719 for (i = 0; i < mergePaths.length; i++) {
10720 curPath = mergePaths[i];
10721 if (prevPath !== null && pathContains(prevPath, curPath)) {
10722 throw new Error(errorPrefix +
10723 'contains a path ' +
10724 prevPath.toString() +
10725 ' that is ancestor of another path ' +
10726 curPath.toString());
10727 }
10728 prevPath = curPath;
10729 }
10730};
10731/**
10732 * pre-validate an object passed as an argument to firebase function (
10733 * must be an object - e.g. for firebase.update()).
10734 */
10735const validateFirebaseMergeDataArg = function (fnName, data, path, optional) {
10736 if (optional && data === undefined) {
10737 return;
10738 }
10739 const errorPrefix$1 = errorPrefix(fnName, 'values');
10740 if (!(data && typeof data === 'object') || Array.isArray(data)) {
10741 throw new Error(errorPrefix$1 + ' must be an object containing the children to replace.');
10742 }
10743 const mergePaths = [];
10744 each(data, (key, value) => {
10745 const curPath = new Path(key);
10746 validateFirebaseData(errorPrefix$1, value, pathChild(path, curPath));
10747 if (pathGetBack(curPath) === '.priority') {
10748 if (!isValidPriority(value)) {
10749 throw new Error(errorPrefix$1 +
10750 "contains an invalid value for '" +
10751 curPath.toString() +
10752 "', which must be a valid " +
10753 'Firebase priority (a string, finite number, server value, or null).');
10754 }
10755 }
10756 mergePaths.push(curPath);
10757 });
10758 validateFirebaseMergePaths(errorPrefix$1, mergePaths);
10759};
10760const validatePriority = function (fnName, priority, optional) {
10761 if (optional && priority === undefined) {
10762 return;
10763 }
10764 if (isInvalidJSONNumber(priority)) {
10765 throw new Error(errorPrefix(fnName, 'priority') +
10766 'is ' +
10767 priority.toString() +
10768 ', but must be a valid Firebase priority (a string, finite number, ' +
10769 'server value, or null).');
10770 }
10771 // Special case to allow importing data with a .sv.
10772 if (!isValidPriority(priority)) {
10773 throw new Error(errorPrefix(fnName, 'priority') +
10774 'must be a valid Firebase priority ' +
10775 '(a string, finite number, server value, or null).');
10776 }
10777};
10778const validateKey = function (fnName, argumentName, key, optional) {
10779 if (optional && key === undefined) {
10780 return;
10781 }
10782 if (!isValidKey(key)) {
10783 throw new Error(errorPrefix(fnName, argumentName) +
10784 'was an invalid key = "' +
10785 key +
10786 '". Firebase keys must be non-empty strings and ' +
10787 'can\'t contain ".", "#", "$", "/", "[", or "]").');
10788 }
10789};
10790/**
10791 * @internal
10792 */
10793const validatePathString = function (fnName, argumentName, pathString, optional) {
10794 if (optional && pathString === undefined) {
10795 return;
10796 }
10797 if (!isValidPathString(pathString)) {
10798 throw new Error(errorPrefix(fnName, argumentName) +
10799 'was an invalid path = "' +
10800 pathString +
10801 '". Paths must be non-empty strings and ' +
10802 'can\'t contain ".", "#", "$", "[", or "]"');
10803 }
10804};
10805const validateRootPathString = function (fnName, argumentName, pathString, optional) {
10806 if (pathString) {
10807 // Allow '/.info/' at the beginning.
10808 pathString = pathString.replace(/^\/*\.info(\/|$)/, '/');
10809 }
10810 validatePathString(fnName, argumentName, pathString, optional);
10811};
10812/**
10813 * @internal
10814 */
10815const validateWritablePath = function (fnName, path) {
10816 if (pathGetFront(path) === '.info') {
10817 throw new Error(fnName + " failed = Can't modify data under /.info/");
10818 }
10819};
10820const validateUrl = function (fnName, parsedUrl) {
10821 // TODO = Validate server better.
10822 const pathString = parsedUrl.path.toString();
10823 if (!(typeof parsedUrl.repoInfo.host === 'string') ||
10824 parsedUrl.repoInfo.host.length === 0 ||
10825 (!isValidKey(parsedUrl.repoInfo.namespace) &&
10826 parsedUrl.repoInfo.host.split(':')[0] !== 'localhost') ||
10827 (pathString.length !== 0 && !isValidRootPathString(pathString))) {
10828 throw new Error(errorPrefix(fnName, 'url') +
10829 'must be a valid firebase URL and ' +
10830 'the path can\'t contain ".", "#", "$", "[", or "]".');
10831 }
10832};
10833
10834/**
10835 * @license
10836 * Copyright 2017 Google LLC
10837 *
10838 * Licensed under the Apache License, Version 2.0 (the "License");
10839 * you may not use this file except in compliance with the License.
10840 * You may obtain a copy of the License at
10841 *
10842 * http://www.apache.org/licenses/LICENSE-2.0
10843 *
10844 * Unless required by applicable law or agreed to in writing, software
10845 * distributed under the License is distributed on an "AS IS" BASIS,
10846 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10847 * See the License for the specific language governing permissions and
10848 * limitations under the License.
10849 */
10850/**
10851 * The event queue serves a few purposes:
10852 * 1. It ensures we maintain event order in the face of event callbacks doing operations that result in more
10853 * events being queued.
10854 * 2. raiseQueuedEvents() handles being called reentrantly nicely. That is, if in the course of raising events,
10855 * raiseQueuedEvents() is called again, the "inner" call will pick up raising events where the "outer" call
10856 * left off, ensuring that the events are still raised synchronously and in order.
10857 * 3. You can use raiseEventsAtPath and raiseEventsForChangedPath to ensure only relevant previously-queued
10858 * events are raised synchronously.
10859 *
10860 * NOTE: This can all go away if/when we move to async events.
10861 *
10862 */
10863class EventQueue {
10864 constructor() {
10865 this.eventLists_ = [];
10866 /**
10867 * Tracks recursion depth of raiseQueuedEvents_, for debugging purposes.
10868 */
10869 this.recursionDepth_ = 0;
10870 }
10871}
10872/**
10873 * @param eventDataList - The new events to queue.
10874 */
10875function eventQueueQueueEvents(eventQueue, eventDataList) {
10876 // We group events by path, storing them in a single EventList, to make it easier to skip over them quickly.
10877 let currList = null;
10878 for (let i = 0; i < eventDataList.length; i++) {
10879 const data = eventDataList[i];
10880 const path = data.getPath();
10881 if (currList !== null && !pathEquals(path, currList.path)) {
10882 eventQueue.eventLists_.push(currList);
10883 currList = null;
10884 }
10885 if (currList === null) {
10886 currList = { events: [], path };
10887 }
10888 currList.events.push(data);
10889 }
10890 if (currList) {
10891 eventQueue.eventLists_.push(currList);
10892 }
10893}
10894/**
10895 * Queues the specified events and synchronously raises all events (including previously queued ones)
10896 * for the specified path.
10897 *
10898 * It is assumed that the new events are all for the specified path.
10899 *
10900 * @param path - The path to raise events for.
10901 * @param eventDataList - The new events to raise.
10902 */
10903function eventQueueRaiseEventsAtPath(eventQueue, path, eventDataList) {
10904 eventQueueQueueEvents(eventQueue, eventDataList);
10905 eventQueueRaiseQueuedEventsMatchingPredicate(eventQueue, eventPath => pathEquals(eventPath, path));
10906}
10907/**
10908 * Queues the specified events and synchronously raises all events (including previously queued ones) for
10909 * locations related to the specified change path (i.e. all ancestors and descendants).
10910 *
10911 * It is assumed that the new events are all related (ancestor or descendant) to the specified path.
10912 *
10913 * @param changedPath - The path to raise events for.
10914 * @param eventDataList - The events to raise
10915 */
10916function eventQueueRaiseEventsForChangedPath(eventQueue, changedPath, eventDataList) {
10917 eventQueueQueueEvents(eventQueue, eventDataList);
10918 eventQueueRaiseQueuedEventsMatchingPredicate(eventQueue, eventPath => pathContains(eventPath, changedPath) ||
10919 pathContains(changedPath, eventPath));
10920}
10921function eventQueueRaiseQueuedEventsMatchingPredicate(eventQueue, predicate) {
10922 eventQueue.recursionDepth_++;
10923 let sentAll = true;
10924 for (let i = 0; i < eventQueue.eventLists_.length; i++) {
10925 const eventList = eventQueue.eventLists_[i];
10926 if (eventList) {
10927 const eventPath = eventList.path;
10928 if (predicate(eventPath)) {
10929 eventListRaise(eventQueue.eventLists_[i]);
10930 eventQueue.eventLists_[i] = null;
10931 }
10932 else {
10933 sentAll = false;
10934 }
10935 }
10936 }
10937 if (sentAll) {
10938 eventQueue.eventLists_ = [];
10939 }
10940 eventQueue.recursionDepth_--;
10941}
10942/**
10943 * Iterates through the list and raises each event
10944 */
10945function eventListRaise(eventList) {
10946 for (let i = 0; i < eventList.events.length; i++) {
10947 const eventData = eventList.events[i];
10948 if (eventData !== null) {
10949 eventList.events[i] = null;
10950 const eventFn = eventData.getEventRunner();
10951 if (logger) {
10952 log('event: ' + eventData.toString());
10953 }
10954 exceptionGuard(eventFn);
10955 }
10956 }
10957}
10958
10959/**
10960 * @license
10961 * Copyright 2017 Google LLC
10962 *
10963 * Licensed under the Apache License, Version 2.0 (the "License");
10964 * you may not use this file except in compliance with the License.
10965 * You may obtain a copy of the License at
10966 *
10967 * http://www.apache.org/licenses/LICENSE-2.0
10968 *
10969 * Unless required by applicable law or agreed to in writing, software
10970 * distributed under the License is distributed on an "AS IS" BASIS,
10971 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10972 * See the License for the specific language governing permissions and
10973 * limitations under the License.
10974 */
10975const INTERRUPT_REASON = 'repo_interrupt';
10976/**
10977 * If a transaction does not succeed after 25 retries, we abort it. Among other
10978 * things this ensure that if there's ever a bug causing a mismatch between
10979 * client / server hashes for some data, we won't retry indefinitely.
10980 */
10981const MAX_TRANSACTION_RETRIES = 25;
10982/**
10983 * A connection to a single data repository.
10984 */
10985class Repo {
10986 constructor(repoInfo_, forceRestClient_, authTokenProvider_, appCheckProvider_) {
10987 this.repoInfo_ = repoInfo_;
10988 this.forceRestClient_ = forceRestClient_;
10989 this.authTokenProvider_ = authTokenProvider_;
10990 this.appCheckProvider_ = appCheckProvider_;
10991 this.dataUpdateCount = 0;
10992 this.statsListener_ = null;
10993 this.eventQueue_ = new EventQueue();
10994 this.nextWriteId_ = 1;
10995 this.interceptServerDataCallback_ = null;
10996 /** A list of data pieces and paths to be set when this client disconnects. */
10997 this.onDisconnect_ = newSparseSnapshotTree();
10998 /** Stores queues of outstanding transactions for Firebase locations. */
10999 this.transactionQueueTree_ = new Tree();
11000 // TODO: This should be @private but it's used by test_access.js and internal.js
11001 this.persistentConnection_ = null;
11002 // This key is intentionally not updated if RepoInfo is later changed or replaced
11003 this.key = this.repoInfo_.toURLString();
11004 }
11005 /**
11006 * @returns The URL corresponding to the root of this Firebase.
11007 */
11008 toString() {
11009 return ((this.repoInfo_.secure ? 'https://' : 'http://') + this.repoInfo_.host);
11010 }
11011}
11012function repoStart(repo, appId, authOverride) {
11013 repo.stats_ = statsManagerGetCollection(repo.repoInfo_);
11014 if (repo.forceRestClient_ || beingCrawled()) {
11015 repo.server_ = new ReadonlyRestClient(repo.repoInfo_, (pathString, data, isMerge, tag) => {
11016 repoOnDataUpdate(repo, pathString, data, isMerge, tag);
11017 }, repo.authTokenProvider_, repo.appCheckProvider_);
11018 // Minor hack: Fire onConnect immediately, since there's no actual connection.
11019 setTimeout(() => repoOnConnectStatus(repo, /* connectStatus= */ true), 0);
11020 }
11021 else {
11022 // Validate authOverride
11023 if (typeof authOverride !== 'undefined' && authOverride !== null) {
11024 if (typeof authOverride !== 'object') {
11025 throw new Error('Only objects are supported for option databaseAuthVariableOverride');
11026 }
11027 try {
11028 stringify(authOverride);
11029 }
11030 catch (e) {
11031 throw new Error('Invalid authOverride provided: ' + e);
11032 }
11033 }
11034 repo.persistentConnection_ = new PersistentConnection(repo.repoInfo_, appId, (pathString, data, isMerge, tag) => {
11035 repoOnDataUpdate(repo, pathString, data, isMerge, tag);
11036 }, (connectStatus) => {
11037 repoOnConnectStatus(repo, connectStatus);
11038 }, (updates) => {
11039 repoOnServerInfoUpdate(repo, updates);
11040 }, repo.authTokenProvider_, repo.appCheckProvider_, authOverride);
11041 repo.server_ = repo.persistentConnection_;
11042 }
11043 repo.authTokenProvider_.addTokenChangeListener(token => {
11044 repo.server_.refreshAuthToken(token);
11045 });
11046 repo.appCheckProvider_.addTokenChangeListener(result => {
11047 repo.server_.refreshAppCheckToken(result.token);
11048 });
11049 // In the case of multiple Repos for the same repoInfo (i.e. there are multiple Firebase.Contexts being used),
11050 // we only want to create one StatsReporter. As such, we'll report stats over the first Repo created.
11051 repo.statsReporter_ = statsManagerGetOrCreateReporter(repo.repoInfo_, () => new StatsReporter(repo.stats_, repo.server_));
11052 // Used for .info.
11053 repo.infoData_ = new SnapshotHolder();
11054 repo.infoSyncTree_ = new SyncTree({
11055 startListening: (query, tag, currentHashFn, onComplete) => {
11056 let infoEvents = [];
11057 const node = repo.infoData_.getNode(query._path);
11058 // This is possibly a hack, but we have different semantics for .info endpoints. We don't raise null events
11059 // on initial data...
11060 if (!node.isEmpty()) {
11061 infoEvents = syncTreeApplyServerOverwrite(repo.infoSyncTree_, query._path, node);
11062 setTimeout(() => {
11063 onComplete('ok');
11064 }, 0);
11065 }
11066 return infoEvents;
11067 },
11068 stopListening: () => { }
11069 });
11070 repoUpdateInfo(repo, 'connected', false);
11071 repo.serverSyncTree_ = new SyncTree({
11072 startListening: (query, tag, currentHashFn, onComplete) => {
11073 repo.server_.listen(query, currentHashFn, tag, (status, data) => {
11074 const events = onComplete(status, data);
11075 eventQueueRaiseEventsForChangedPath(repo.eventQueue_, query._path, events);
11076 });
11077 // No synchronous events for network-backed sync trees
11078 return [];
11079 },
11080 stopListening: (query, tag) => {
11081 repo.server_.unlisten(query, tag);
11082 }
11083 });
11084}
11085/**
11086 * @returns The time in milliseconds, taking the server offset into account if we have one.
11087 */
11088function repoServerTime(repo) {
11089 const offsetNode = repo.infoData_.getNode(new Path('.info/serverTimeOffset'));
11090 const offset = offsetNode.val() || 0;
11091 return new Date().getTime() + offset;
11092}
11093/**
11094 * Generate ServerValues using some variables from the repo object.
11095 */
11096function repoGenerateServerValues(repo) {
11097 return generateWithValues({
11098 timestamp: repoServerTime(repo)
11099 });
11100}
11101/**
11102 * Called by realtime when we get new messages from the server.
11103 */
11104function repoOnDataUpdate(repo, pathString, data, isMerge, tag) {
11105 // For testing.
11106 repo.dataUpdateCount++;
11107 const path = new Path(pathString);
11108 data = repo.interceptServerDataCallback_
11109 ? repo.interceptServerDataCallback_(pathString, data)
11110 : data;
11111 let events = [];
11112 if (tag) {
11113 if (isMerge) {
11114 const taggedChildren = map(data, (raw) => nodeFromJSON(raw));
11115 events = syncTreeApplyTaggedQueryMerge(repo.serverSyncTree_, path, taggedChildren, tag);
11116 }
11117 else {
11118 const taggedSnap = nodeFromJSON(data);
11119 events = syncTreeApplyTaggedQueryOverwrite(repo.serverSyncTree_, path, taggedSnap, tag);
11120 }
11121 }
11122 else if (isMerge) {
11123 const changedChildren = map(data, (raw) => nodeFromJSON(raw));
11124 events = syncTreeApplyServerMerge(repo.serverSyncTree_, path, changedChildren);
11125 }
11126 else {
11127 const snap = nodeFromJSON(data);
11128 events = syncTreeApplyServerOverwrite(repo.serverSyncTree_, path, snap);
11129 }
11130 let affectedPath = path;
11131 if (events.length > 0) {
11132 // Since we have a listener outstanding for each transaction, receiving any events
11133 // is a proxy for some change having occurred.
11134 affectedPath = repoRerunTransactions(repo, path);
11135 }
11136 eventQueueRaiseEventsForChangedPath(repo.eventQueue_, affectedPath, events);
11137}
11138function repoOnConnectStatus(repo, connectStatus) {
11139 repoUpdateInfo(repo, 'connected', connectStatus);
11140 if (connectStatus === false) {
11141 repoRunOnDisconnectEvents(repo);
11142 }
11143}
11144function repoOnServerInfoUpdate(repo, updates) {
11145 each(updates, (key, value) => {
11146 repoUpdateInfo(repo, key, value);
11147 });
11148}
11149function repoUpdateInfo(repo, pathString, value) {
11150 const path = new Path('/.info/' + pathString);
11151 const newNode = nodeFromJSON(value);
11152 repo.infoData_.updateSnapshot(path, newNode);
11153 const events = syncTreeApplyServerOverwrite(repo.infoSyncTree_, path, newNode);
11154 eventQueueRaiseEventsForChangedPath(repo.eventQueue_, path, events);
11155}
11156function repoGetNextWriteId(repo) {
11157 return repo.nextWriteId_++;
11158}
11159/**
11160 * The purpose of `getValue` is to return the latest known value
11161 * satisfying `query`.
11162 *
11163 * This method will first check for in-memory cached values
11164 * belonging to active listeners. If they are found, such values
11165 * are considered to be the most up-to-date.
11166 *
11167 * If the client is not connected, this method will try to
11168 * establish a connection and request the value for `query`. If
11169 * the client is not able to retrieve the query result, it reports
11170 * an error.
11171 *
11172 * @param query - The query to surface a value for.
11173 */
11174function repoGetValue(repo, query) {
11175 // Only active queries are cached. There is no persisted cache.
11176 const cached = syncTreeGetServerValue(repo.serverSyncTree_, query);
11177 if (cached != null) {
11178 return Promise.resolve(cached);
11179 }
11180 return repo.server_.get(query).then(payload => {
11181 const node = nodeFromJSON(payload).withIndex(query._queryParams.getIndex());
11182 const events = syncTreeApplyServerOverwrite(repo.serverSyncTree_, query._path, node);
11183 eventQueueRaiseEventsAtPath(repo.eventQueue_, query._path, events);
11184 return Promise.resolve(node);
11185 }, err => {
11186 repoLog(repo, 'get for query ' + stringify(query) + ' failed: ' + err);
11187 return Promise.reject(new Error(err));
11188 });
11189}
11190function repoSetWithPriority(repo, path, newVal, newPriority, onComplete) {
11191 repoLog(repo, 'set', {
11192 path: path.toString(),
11193 value: newVal,
11194 priority: newPriority
11195 });
11196 // TODO: Optimize this behavior to either (a) store flag to skip resolving where possible and / or
11197 // (b) store unresolved paths on JSON parse
11198 const serverValues = repoGenerateServerValues(repo);
11199 const newNodeUnresolved = nodeFromJSON(newVal, newPriority);
11200 const existing = syncTreeCalcCompleteEventCache(repo.serverSyncTree_, path);
11201 const newNode = resolveDeferredValueSnapshot(newNodeUnresolved, existing, serverValues);
11202 const writeId = repoGetNextWriteId(repo);
11203 const events = syncTreeApplyUserOverwrite(repo.serverSyncTree_, path, newNode, writeId, true);
11204 eventQueueQueueEvents(repo.eventQueue_, events);
11205 repo.server_.put(path.toString(), newNodeUnresolved.val(/*export=*/ true), (status, errorReason) => {
11206 const success = status === 'ok';
11207 if (!success) {
11208 warn('set at ' + path + ' failed: ' + status);
11209 }
11210 const clearEvents = syncTreeAckUserWrite(repo.serverSyncTree_, writeId, !success);
11211 eventQueueRaiseEventsForChangedPath(repo.eventQueue_, path, clearEvents);
11212 repoCallOnCompleteCallback(repo, onComplete, status, errorReason);
11213 });
11214 const affectedPath = repoAbortTransactions(repo, path);
11215 repoRerunTransactions(repo, affectedPath);
11216 // We queued the events above, so just flush the queue here
11217 eventQueueRaiseEventsForChangedPath(repo.eventQueue_, affectedPath, []);
11218}
11219function repoUpdate(repo, path, childrenToMerge, onComplete) {
11220 repoLog(repo, 'update', { path: path.toString(), value: childrenToMerge });
11221 // Start with our existing data and merge each child into it.
11222 let empty = true;
11223 const serverValues = repoGenerateServerValues(repo);
11224 const changedChildren = {};
11225 each(childrenToMerge, (changedKey, changedValue) => {
11226 empty = false;
11227 changedChildren[changedKey] = resolveDeferredValueTree(pathChild(path, changedKey), nodeFromJSON(changedValue), repo.serverSyncTree_, serverValues);
11228 });
11229 if (!empty) {
11230 const writeId = repoGetNextWriteId(repo);
11231 const events = syncTreeApplyUserMerge(repo.serverSyncTree_, path, changedChildren, writeId);
11232 eventQueueQueueEvents(repo.eventQueue_, events);
11233 repo.server_.merge(path.toString(), childrenToMerge, (status, errorReason) => {
11234 const success = status === 'ok';
11235 if (!success) {
11236 warn('update at ' + path + ' failed: ' + status);
11237 }
11238 const clearEvents = syncTreeAckUserWrite(repo.serverSyncTree_, writeId, !success);
11239 const affectedPath = clearEvents.length > 0 ? repoRerunTransactions(repo, path) : path;
11240 eventQueueRaiseEventsForChangedPath(repo.eventQueue_, affectedPath, clearEvents);
11241 repoCallOnCompleteCallback(repo, onComplete, status, errorReason);
11242 });
11243 each(childrenToMerge, (changedPath) => {
11244 const affectedPath = repoAbortTransactions(repo, pathChild(path, changedPath));
11245 repoRerunTransactions(repo, affectedPath);
11246 });
11247 // We queued the events above, so just flush the queue here
11248 eventQueueRaiseEventsForChangedPath(repo.eventQueue_, path, []);
11249 }
11250 else {
11251 log("update() called with empty data. Don't do anything.");
11252 repoCallOnCompleteCallback(repo, onComplete, 'ok', undefined);
11253 }
11254}
11255/**
11256 * Applies all of the changes stored up in the onDisconnect_ tree.
11257 */
11258function repoRunOnDisconnectEvents(repo) {
11259 repoLog(repo, 'onDisconnectEvents');
11260 const serverValues = repoGenerateServerValues(repo);
11261 const resolvedOnDisconnectTree = newSparseSnapshotTree();
11262 sparseSnapshotTreeForEachTree(repo.onDisconnect_, newEmptyPath(), (path, node) => {
11263 const resolved = resolveDeferredValueTree(path, node, repo.serverSyncTree_, serverValues);
11264 sparseSnapshotTreeRemember(resolvedOnDisconnectTree, path, resolved);
11265 });
11266 let events = [];
11267 sparseSnapshotTreeForEachTree(resolvedOnDisconnectTree, newEmptyPath(), (path, snap) => {
11268 events = events.concat(syncTreeApplyServerOverwrite(repo.serverSyncTree_, path, snap));
11269 const affectedPath = repoAbortTransactions(repo, path);
11270 repoRerunTransactions(repo, affectedPath);
11271 });
11272 repo.onDisconnect_ = newSparseSnapshotTree();
11273 eventQueueRaiseEventsForChangedPath(repo.eventQueue_, newEmptyPath(), events);
11274}
11275function repoOnDisconnectCancel(repo, path, onComplete) {
11276 repo.server_.onDisconnectCancel(path.toString(), (status, errorReason) => {
11277 if (status === 'ok') {
11278 sparseSnapshotTreeForget(repo.onDisconnect_, path);
11279 }
11280 repoCallOnCompleteCallback(repo, onComplete, status, errorReason);
11281 });
11282}
11283function repoOnDisconnectSet(repo, path, value, onComplete) {
11284 const newNode = nodeFromJSON(value);
11285 repo.server_.onDisconnectPut(path.toString(), newNode.val(/*export=*/ true), (status, errorReason) => {
11286 if (status === 'ok') {
11287 sparseSnapshotTreeRemember(repo.onDisconnect_, path, newNode);
11288 }
11289 repoCallOnCompleteCallback(repo, onComplete, status, errorReason);
11290 });
11291}
11292function repoOnDisconnectSetWithPriority(repo, path, value, priority, onComplete) {
11293 const newNode = nodeFromJSON(value, priority);
11294 repo.server_.onDisconnectPut(path.toString(), newNode.val(/*export=*/ true), (status, errorReason) => {
11295 if (status === 'ok') {
11296 sparseSnapshotTreeRemember(repo.onDisconnect_, path, newNode);
11297 }
11298 repoCallOnCompleteCallback(repo, onComplete, status, errorReason);
11299 });
11300}
11301function repoOnDisconnectUpdate(repo, path, childrenToMerge, onComplete) {
11302 if (isEmpty(childrenToMerge)) {
11303 log("onDisconnect().update() called with empty data. Don't do anything.");
11304 repoCallOnCompleteCallback(repo, onComplete, 'ok', undefined);
11305 return;
11306 }
11307 repo.server_.onDisconnectMerge(path.toString(), childrenToMerge, (status, errorReason) => {
11308 if (status === 'ok') {
11309 each(childrenToMerge, (childName, childNode) => {
11310 const newChildNode = nodeFromJSON(childNode);
11311 sparseSnapshotTreeRemember(repo.onDisconnect_, pathChild(path, childName), newChildNode);
11312 });
11313 }
11314 repoCallOnCompleteCallback(repo, onComplete, status, errorReason);
11315 });
11316}
11317function repoAddEventCallbackForQuery(repo, query, eventRegistration) {
11318 let events;
11319 if (pathGetFront(query._path) === '.info') {
11320 events = syncTreeAddEventRegistration(repo.infoSyncTree_, query, eventRegistration);
11321 }
11322 else {
11323 events = syncTreeAddEventRegistration(repo.serverSyncTree_, query, eventRegistration);
11324 }
11325 eventQueueRaiseEventsAtPath(repo.eventQueue_, query._path, events);
11326}
11327function repoRemoveEventCallbackForQuery(repo, query, eventRegistration) {
11328 // These are guaranteed not to raise events, since we're not passing in a cancelError. However, we can future-proof
11329 // a little bit by handling the return values anyways.
11330 let events;
11331 if (pathGetFront(query._path) === '.info') {
11332 events = syncTreeRemoveEventRegistration(repo.infoSyncTree_, query, eventRegistration);
11333 }
11334 else {
11335 events = syncTreeRemoveEventRegistration(repo.serverSyncTree_, query, eventRegistration);
11336 }
11337 eventQueueRaiseEventsAtPath(repo.eventQueue_, query._path, events);
11338}
11339function repoInterrupt(repo) {
11340 if (repo.persistentConnection_) {
11341 repo.persistentConnection_.interrupt(INTERRUPT_REASON);
11342 }
11343}
11344function repoResume(repo) {
11345 if (repo.persistentConnection_) {
11346 repo.persistentConnection_.resume(INTERRUPT_REASON);
11347 }
11348}
11349function repoLog(repo, ...varArgs) {
11350 let prefix = '';
11351 if (repo.persistentConnection_) {
11352 prefix = repo.persistentConnection_.id + ':';
11353 }
11354 log(prefix, ...varArgs);
11355}
11356function repoCallOnCompleteCallback(repo, callback, status, errorReason) {
11357 if (callback) {
11358 exceptionGuard(() => {
11359 if (status === 'ok') {
11360 callback(null);
11361 }
11362 else {
11363 const code = (status || 'error').toUpperCase();
11364 let message = code;
11365 if (errorReason) {
11366 message += ': ' + errorReason;
11367 }
11368 const error = new Error(message);
11369 // eslint-disable-next-line @typescript-eslint/no-explicit-any
11370 error.code = code;
11371 callback(error);
11372 }
11373 });
11374 }
11375}
11376/**
11377 * Creates a new transaction, adds it to the transactions we're tracking, and
11378 * sends it to the server if possible.
11379 *
11380 * @param path - Path at which to do transaction.
11381 * @param transactionUpdate - Update callback.
11382 * @param onComplete - Completion callback.
11383 * @param unwatcher - Function that will be called when the transaction no longer
11384 * need data updates for `path`.
11385 * @param applyLocally - Whether or not to make intermediate results visible
11386 */
11387function repoStartTransaction(repo, path, transactionUpdate, onComplete, unwatcher, applyLocally) {
11388 repoLog(repo, 'transaction on ' + path);
11389 // Initialize transaction.
11390 const transaction = {
11391 path,
11392 update: transactionUpdate,
11393 onComplete,
11394 // One of TransactionStatus enums.
11395 status: null,
11396 // Used when combining transactions at different locations to figure out
11397 // which one goes first.
11398 order: LUIDGenerator(),
11399 // Whether to raise local events for this transaction.
11400 applyLocally,
11401 // Count of how many times we've retried the transaction.
11402 retryCount: 0,
11403 // Function to call to clean up our .on() listener.
11404 unwatcher,
11405 // Stores why a transaction was aborted.
11406 abortReason: null,
11407 currentWriteId: null,
11408 currentInputSnapshot: null,
11409 currentOutputSnapshotRaw: null,
11410 currentOutputSnapshotResolved: null
11411 };
11412 // Run transaction initially.
11413 const currentState = repoGetLatestState(repo, path, undefined);
11414 transaction.currentInputSnapshot = currentState;
11415 const newVal = transaction.update(currentState.val());
11416 if (newVal === undefined) {
11417 // Abort transaction.
11418 transaction.unwatcher();
11419 transaction.currentOutputSnapshotRaw = null;
11420 transaction.currentOutputSnapshotResolved = null;
11421 if (transaction.onComplete) {
11422 transaction.onComplete(null, false, transaction.currentInputSnapshot);
11423 }
11424 }
11425 else {
11426 validateFirebaseData('transaction failed: Data returned ', newVal, transaction.path);
11427 // Mark as run and add to our queue.
11428 transaction.status = 0 /* RUN */;
11429 const queueNode = treeSubTree(repo.transactionQueueTree_, path);
11430 const nodeQueue = treeGetValue(queueNode) || [];
11431 nodeQueue.push(transaction);
11432 treeSetValue(queueNode, nodeQueue);
11433 // Update visibleData and raise events
11434 // Note: We intentionally raise events after updating all of our
11435 // transaction state, since the user could start new transactions from the
11436 // event callbacks.
11437 let priorityForNode;
11438 if (typeof newVal === 'object' &&
11439 newVal !== null &&
11440 contains(newVal, '.priority')) {
11441 // eslint-disable-next-line @typescript-eslint/no-explicit-any
11442 priorityForNode = safeGet(newVal, '.priority');
11443 assert(isValidPriority(priorityForNode), 'Invalid priority returned by transaction. ' +
11444 'Priority must be a valid string, finite number, server value, or null.');
11445 }
11446 else {
11447 const currentNode = syncTreeCalcCompleteEventCache(repo.serverSyncTree_, path) ||
11448 ChildrenNode.EMPTY_NODE;
11449 priorityForNode = currentNode.getPriority().val();
11450 }
11451 const serverValues = repoGenerateServerValues(repo);
11452 const newNodeUnresolved = nodeFromJSON(newVal, priorityForNode);
11453 const newNode = resolveDeferredValueSnapshot(newNodeUnresolved, currentState, serverValues);
11454 transaction.currentOutputSnapshotRaw = newNodeUnresolved;
11455 transaction.currentOutputSnapshotResolved = newNode;
11456 transaction.currentWriteId = repoGetNextWriteId(repo);
11457 const events = syncTreeApplyUserOverwrite(repo.serverSyncTree_, path, newNode, transaction.currentWriteId, transaction.applyLocally);
11458 eventQueueRaiseEventsForChangedPath(repo.eventQueue_, path, events);
11459 repoSendReadyTransactions(repo, repo.transactionQueueTree_);
11460 }
11461}
11462/**
11463 * @param excludeSets - A specific set to exclude
11464 */
11465function repoGetLatestState(repo, path, excludeSets) {
11466 return (syncTreeCalcCompleteEventCache(repo.serverSyncTree_, path, excludeSets) ||
11467 ChildrenNode.EMPTY_NODE);
11468}
11469/**
11470 * Sends any already-run transactions that aren't waiting for outstanding
11471 * transactions to complete.
11472 *
11473 * Externally it's called with no arguments, but it calls itself recursively
11474 * with a particular transactionQueueTree node to recurse through the tree.
11475 *
11476 * @param node - transactionQueueTree node to start at.
11477 */
11478function repoSendReadyTransactions(repo, node = repo.transactionQueueTree_) {
11479 // Before recursing, make sure any completed transactions are removed.
11480 if (!node) {
11481 repoPruneCompletedTransactionsBelowNode(repo, node);
11482 }
11483 if (treeGetValue(node)) {
11484 const queue = repoBuildTransactionQueue(repo, node);
11485 assert(queue.length > 0, 'Sending zero length transaction queue');
11486 const allRun = queue.every((transaction) => transaction.status === 0 /* RUN */);
11487 // If they're all run (and not sent), we can send them. Else, we must wait.
11488 if (allRun) {
11489 repoSendTransactionQueue(repo, treeGetPath(node), queue);
11490 }
11491 }
11492 else if (treeHasChildren(node)) {
11493 treeForEachChild(node, childNode => {
11494 repoSendReadyTransactions(repo, childNode);
11495 });
11496 }
11497}
11498/**
11499 * Given a list of run transactions, send them to the server and then handle
11500 * the result (success or failure).
11501 *
11502 * @param path - The location of the queue.
11503 * @param queue - Queue of transactions under the specified location.
11504 */
11505function repoSendTransactionQueue(repo, path, queue) {
11506 // Mark transactions as sent and increment retry count!
11507 const setsToIgnore = queue.map(txn => {
11508 return txn.currentWriteId;
11509 });
11510 const latestState = repoGetLatestState(repo, path, setsToIgnore);
11511 let snapToSend = latestState;
11512 const latestHash = latestState.hash();
11513 for (let i = 0; i < queue.length; i++) {
11514 const txn = queue[i];
11515 assert(txn.status === 0 /* RUN */, 'tryToSendTransactionQueue_: items in queue should all be run.');
11516 txn.status = 1 /* SENT */;
11517 txn.retryCount++;
11518 const relativePath = newRelativePath(path, txn.path);
11519 // If we've gotten to this point, the output snapshot must be defined.
11520 snapToSend = snapToSend.updateChild(relativePath /** @type {!Node} */, txn.currentOutputSnapshotRaw);
11521 }
11522 const dataToSend = snapToSend.val(true);
11523 const pathToSend = path;
11524 // Send the put.
11525 repo.server_.put(pathToSend.toString(), dataToSend, (status) => {
11526 repoLog(repo, 'transaction put response', {
11527 path: pathToSend.toString(),
11528 status
11529 });
11530 let events = [];
11531 if (status === 'ok') {
11532 // Queue up the callbacks and fire them after cleaning up all of our
11533 // transaction state, since the callback could trigger more
11534 // transactions or sets.
11535 const callbacks = [];
11536 for (let i = 0; i < queue.length; i++) {
11537 queue[i].status = 2 /* COMPLETED */;
11538 events = events.concat(syncTreeAckUserWrite(repo.serverSyncTree_, queue[i].currentWriteId));
11539 if (queue[i].onComplete) {
11540 // We never unset the output snapshot, and given that this
11541 // transaction is complete, it should be set
11542 callbacks.push(() => queue[i].onComplete(null, true, queue[i].currentOutputSnapshotResolved));
11543 }
11544 queue[i].unwatcher();
11545 }
11546 // Now remove the completed transactions.
11547 repoPruneCompletedTransactionsBelowNode(repo, treeSubTree(repo.transactionQueueTree_, path));
11548 // There may be pending transactions that we can now send.
11549 repoSendReadyTransactions(repo, repo.transactionQueueTree_);
11550 eventQueueRaiseEventsForChangedPath(repo.eventQueue_, path, events);
11551 // Finally, trigger onComplete callbacks.
11552 for (let i = 0; i < callbacks.length; i++) {
11553 exceptionGuard(callbacks[i]);
11554 }
11555 }
11556 else {
11557 // transactions are no longer sent. Update their status appropriately.
11558 if (status === 'datastale') {
11559 for (let i = 0; i < queue.length; i++) {
11560 if (queue[i].status === 3 /* SENT_NEEDS_ABORT */) {
11561 queue[i].status = 4 /* NEEDS_ABORT */;
11562 }
11563 else {
11564 queue[i].status = 0 /* RUN */;
11565 }
11566 }
11567 }
11568 else {
11569 warn('transaction at ' + pathToSend.toString() + ' failed: ' + status);
11570 for (let i = 0; i < queue.length; i++) {
11571 queue[i].status = 4 /* NEEDS_ABORT */;
11572 queue[i].abortReason = status;
11573 }
11574 }
11575 repoRerunTransactions(repo, path);
11576 }
11577 }, latestHash);
11578}
11579/**
11580 * Finds all transactions dependent on the data at changedPath and reruns them.
11581 *
11582 * Should be called any time cached data changes.
11583 *
11584 * Return the highest path that was affected by rerunning transactions. This
11585 * is the path at which events need to be raised for.
11586 *
11587 * @param changedPath - The path in mergedData that changed.
11588 * @returns The rootmost path that was affected by rerunning transactions.
11589 */
11590function repoRerunTransactions(repo, changedPath) {
11591 const rootMostTransactionNode = repoGetAncestorTransactionNode(repo, changedPath);
11592 const path = treeGetPath(rootMostTransactionNode);
11593 const queue = repoBuildTransactionQueue(repo, rootMostTransactionNode);
11594 repoRerunTransactionQueue(repo, queue, path);
11595 return path;
11596}
11597/**
11598 * Does all the work of rerunning transactions (as well as cleans up aborted
11599 * transactions and whatnot).
11600 *
11601 * @param queue - The queue of transactions to run.
11602 * @param path - The path the queue is for.
11603 */
11604function repoRerunTransactionQueue(repo, queue, path) {
11605 if (queue.length === 0) {
11606 return; // Nothing to do!
11607 }
11608 // Queue up the callbacks and fire them after cleaning up all of our
11609 // transaction state, since the callback could trigger more transactions or
11610 // sets.
11611 const callbacks = [];
11612 let events = [];
11613 // Ignore all of the sets we're going to re-run.
11614 const txnsToRerun = queue.filter(q => {
11615 return q.status === 0 /* RUN */;
11616 });
11617 const setsToIgnore = txnsToRerun.map(q => {
11618 return q.currentWriteId;
11619 });
11620 for (let i = 0; i < queue.length; i++) {
11621 const transaction = queue[i];
11622 const relativePath = newRelativePath(path, transaction.path);
11623 let abortTransaction = false, abortReason;
11624 assert(relativePath !== null, 'rerunTransactionsUnderNode_: relativePath should not be null.');
11625 if (transaction.status === 4 /* NEEDS_ABORT */) {
11626 abortTransaction = true;
11627 abortReason = transaction.abortReason;
11628 events = events.concat(syncTreeAckUserWrite(repo.serverSyncTree_, transaction.currentWriteId, true));
11629 }
11630 else if (transaction.status === 0 /* RUN */) {
11631 if (transaction.retryCount >= MAX_TRANSACTION_RETRIES) {
11632 abortTransaction = true;
11633 abortReason = 'maxretry';
11634 events = events.concat(syncTreeAckUserWrite(repo.serverSyncTree_, transaction.currentWriteId, true));
11635 }
11636 else {
11637 // This code reruns a transaction
11638 const currentNode = repoGetLatestState(repo, transaction.path, setsToIgnore);
11639 transaction.currentInputSnapshot = currentNode;
11640 const newData = queue[i].update(currentNode.val());
11641 if (newData !== undefined) {
11642 validateFirebaseData('transaction failed: Data returned ', newData, transaction.path);
11643 let newDataNode = nodeFromJSON(newData);
11644 const hasExplicitPriority = typeof newData === 'object' &&
11645 newData != null &&
11646 contains(newData, '.priority');
11647 if (!hasExplicitPriority) {
11648 // Keep the old priority if there wasn't a priority explicitly specified.
11649 newDataNode = newDataNode.updatePriority(currentNode.getPriority());
11650 }
11651 const oldWriteId = transaction.currentWriteId;
11652 const serverValues = repoGenerateServerValues(repo);
11653 const newNodeResolved = resolveDeferredValueSnapshot(newDataNode, currentNode, serverValues);
11654 transaction.currentOutputSnapshotRaw = newDataNode;
11655 transaction.currentOutputSnapshotResolved = newNodeResolved;
11656 transaction.currentWriteId = repoGetNextWriteId(repo);
11657 // Mutates setsToIgnore in place
11658 setsToIgnore.splice(setsToIgnore.indexOf(oldWriteId), 1);
11659 events = events.concat(syncTreeApplyUserOverwrite(repo.serverSyncTree_, transaction.path, newNodeResolved, transaction.currentWriteId, transaction.applyLocally));
11660 events = events.concat(syncTreeAckUserWrite(repo.serverSyncTree_, oldWriteId, true));
11661 }
11662 else {
11663 abortTransaction = true;
11664 abortReason = 'nodata';
11665 events = events.concat(syncTreeAckUserWrite(repo.serverSyncTree_, transaction.currentWriteId, true));
11666 }
11667 }
11668 }
11669 eventQueueRaiseEventsForChangedPath(repo.eventQueue_, path, events);
11670 events = [];
11671 if (abortTransaction) {
11672 // Abort.
11673 queue[i].status = 2 /* COMPLETED */;
11674 // Removing a listener can trigger pruning which can muck with
11675 // mergedData/visibleData (as it prunes data). So defer the unwatcher
11676 // until we're done.
11677 (function (unwatcher) {
11678 setTimeout(unwatcher, Math.floor(0));
11679 })(queue[i].unwatcher);
11680 if (queue[i].onComplete) {
11681 if (abortReason === 'nodata') {
11682 callbacks.push(() => queue[i].onComplete(null, false, queue[i].currentInputSnapshot));
11683 }
11684 else {
11685 callbacks.push(() => queue[i].onComplete(new Error(abortReason), false, null));
11686 }
11687 }
11688 }
11689 }
11690 // Clean up completed transactions.
11691 repoPruneCompletedTransactionsBelowNode(repo, repo.transactionQueueTree_);
11692 // Now fire callbacks, now that we're in a good, known state.
11693 for (let i = 0; i < callbacks.length; i++) {
11694 exceptionGuard(callbacks[i]);
11695 }
11696 // Try to send the transaction result to the server.
11697 repoSendReadyTransactions(repo, repo.transactionQueueTree_);
11698}
11699/**
11700 * Returns the rootmost ancestor node of the specified path that has a pending
11701 * transaction on it, or just returns the node for the given path if there are
11702 * no pending transactions on any ancestor.
11703 *
11704 * @param path - The location to start at.
11705 * @returns The rootmost node with a transaction.
11706 */
11707function repoGetAncestorTransactionNode(repo, path) {
11708 let front;
11709 // Start at the root and walk deeper into the tree towards path until we
11710 // find a node with pending transactions.
11711 let transactionNode = repo.transactionQueueTree_;
11712 front = pathGetFront(path);
11713 while (front !== null && treeGetValue(transactionNode) === undefined) {
11714 transactionNode = treeSubTree(transactionNode, front);
11715 path = pathPopFront(path);
11716 front = pathGetFront(path);
11717 }
11718 return transactionNode;
11719}
11720/**
11721 * Builds the queue of all transactions at or below the specified
11722 * transactionNode.
11723 *
11724 * @param transactionNode
11725 * @returns The generated queue.
11726 */
11727function repoBuildTransactionQueue(repo, transactionNode) {
11728 // Walk any child transaction queues and aggregate them into a single queue.
11729 const transactionQueue = [];
11730 repoAggregateTransactionQueuesForNode(repo, transactionNode, transactionQueue);
11731 // Sort them by the order the transactions were created.
11732 transactionQueue.sort((a, b) => a.order - b.order);
11733 return transactionQueue;
11734}
11735function repoAggregateTransactionQueuesForNode(repo, node, queue) {
11736 const nodeQueue = treeGetValue(node);
11737 if (nodeQueue) {
11738 for (let i = 0; i < nodeQueue.length; i++) {
11739 queue.push(nodeQueue[i]);
11740 }
11741 }
11742 treeForEachChild(node, child => {
11743 repoAggregateTransactionQueuesForNode(repo, child, queue);
11744 });
11745}
11746/**
11747 * Remove COMPLETED transactions at or below this node in the transactionQueueTree_.
11748 */
11749function repoPruneCompletedTransactionsBelowNode(repo, node) {
11750 const queue = treeGetValue(node);
11751 if (queue) {
11752 let to = 0;
11753 for (let from = 0; from < queue.length; from++) {
11754 if (queue[from].status !== 2 /* COMPLETED */) {
11755 queue[to] = queue[from];
11756 to++;
11757 }
11758 }
11759 queue.length = to;
11760 treeSetValue(node, queue.length > 0 ? queue : undefined);
11761 }
11762 treeForEachChild(node, childNode => {
11763 repoPruneCompletedTransactionsBelowNode(repo, childNode);
11764 });
11765}
11766/**
11767 * Aborts all transactions on ancestors or descendants of the specified path.
11768 * Called when doing a set() or update() since we consider them incompatible
11769 * with transactions.
11770 *
11771 * @param path - Path for which we want to abort related transactions.
11772 */
11773function repoAbortTransactions(repo, path) {
11774 const affectedPath = treeGetPath(repoGetAncestorTransactionNode(repo, path));
11775 const transactionNode = treeSubTree(repo.transactionQueueTree_, path);
11776 treeForEachAncestor(transactionNode, (node) => {
11777 repoAbortTransactionsOnNode(repo, node);
11778 });
11779 repoAbortTransactionsOnNode(repo, transactionNode);
11780 treeForEachDescendant(transactionNode, (node) => {
11781 repoAbortTransactionsOnNode(repo, node);
11782 });
11783 return affectedPath;
11784}
11785/**
11786 * Abort transactions stored in this transaction queue node.
11787 *
11788 * @param node - Node to abort transactions for.
11789 */
11790function repoAbortTransactionsOnNode(repo, node) {
11791 const queue = treeGetValue(node);
11792 if (queue) {
11793 // Queue up the callbacks and fire them after cleaning up all of our
11794 // transaction state, since the callback could trigger more transactions
11795 // or sets.
11796 const callbacks = [];
11797 // Go through queue. Any already-sent transactions must be marked for
11798 // abort, while the unsent ones can be immediately aborted and removed.
11799 let events = [];
11800 let lastSent = -1;
11801 for (let i = 0; i < queue.length; i++) {
11802 if (queue[i].status === 3 /* SENT_NEEDS_ABORT */) ;
11803 else if (queue[i].status === 1 /* SENT */) {
11804 assert(lastSent === i - 1, 'All SENT items should be at beginning of queue.');
11805 lastSent = i;
11806 // Mark transaction for abort when it comes back.
11807 queue[i].status = 3 /* SENT_NEEDS_ABORT */;
11808 queue[i].abortReason = 'set';
11809 }
11810 else {
11811 assert(queue[i].status === 0 /* RUN */, 'Unexpected transaction status in abort');
11812 // We can abort it immediately.
11813 queue[i].unwatcher();
11814 events = events.concat(syncTreeAckUserWrite(repo.serverSyncTree_, queue[i].currentWriteId, true));
11815 if (queue[i].onComplete) {
11816 callbacks.push(queue[i].onComplete.bind(null, new Error('set'), false, null));
11817 }
11818 }
11819 }
11820 if (lastSent === -1) {
11821 // We're not waiting for any sent transactions. We can clear the queue.
11822 treeSetValue(node, undefined);
11823 }
11824 else {
11825 // Remove the transactions we aborted.
11826 queue.length = lastSent + 1;
11827 }
11828 // Now fire the callbacks.
11829 eventQueueRaiseEventsForChangedPath(repo.eventQueue_, treeGetPath(node), events);
11830 for (let i = 0; i < callbacks.length; i++) {
11831 exceptionGuard(callbacks[i]);
11832 }
11833 }
11834}
11835
11836/**
11837 * @license
11838 * Copyright 2017 Google LLC
11839 *
11840 * Licensed under the Apache License, Version 2.0 (the "License");
11841 * you may not use this file except in compliance with the License.
11842 * You may obtain a copy of the License at
11843 *
11844 * http://www.apache.org/licenses/LICENSE-2.0
11845 *
11846 * Unless required by applicable law or agreed to in writing, software
11847 * distributed under the License is distributed on an "AS IS" BASIS,
11848 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11849 * See the License for the specific language governing permissions and
11850 * limitations under the License.
11851 */
11852function decodePath(pathString) {
11853 let pathStringDecoded = '';
11854 const pieces = pathString.split('/');
11855 for (let i = 0; i < pieces.length; i++) {
11856 if (pieces[i].length > 0) {
11857 let piece = pieces[i];
11858 try {
11859 piece = decodeURIComponent(piece.replace(/\+/g, ' '));
11860 }
11861 catch (e) { }
11862 pathStringDecoded += '/' + piece;
11863 }
11864 }
11865 return pathStringDecoded;
11866}
11867/**
11868 * @returns key value hash
11869 */
11870function decodeQuery(queryString) {
11871 const results = {};
11872 if (queryString.charAt(0) === '?') {
11873 queryString = queryString.substring(1);
11874 }
11875 for (const segment of queryString.split('&')) {
11876 if (segment.length === 0) {
11877 continue;
11878 }
11879 const kv = segment.split('=');
11880 if (kv.length === 2) {
11881 results[decodeURIComponent(kv[0])] = decodeURIComponent(kv[1]);
11882 }
11883 else {
11884 warn(`Invalid query segment '${segment}' in query '${queryString}'`);
11885 }
11886 }
11887 return results;
11888}
11889const parseRepoInfo = function (dataURL, nodeAdmin) {
11890 const parsedUrl = parseDatabaseURL(dataURL), namespace = parsedUrl.namespace;
11891 if (parsedUrl.domain === 'firebase.com') {
11892 fatal(parsedUrl.host +
11893 ' is no longer supported. ' +
11894 'Please use <YOUR FIREBASE>.firebaseio.com instead');
11895 }
11896 // Catch common error of uninitialized namespace value.
11897 if ((!namespace || namespace === 'undefined') &&
11898 parsedUrl.domain !== 'localhost') {
11899 fatal('Cannot parse Firebase url. Please use https://<YOUR FIREBASE>.firebaseio.com');
11900 }
11901 if (!parsedUrl.secure) {
11902 warnIfPageIsSecure();
11903 }
11904 const webSocketOnly = parsedUrl.scheme === 'ws' || parsedUrl.scheme === 'wss';
11905 return {
11906 repoInfo: new RepoInfo(parsedUrl.host, parsedUrl.secure, namespace, nodeAdmin, webSocketOnly,
11907 /*persistenceKey=*/ '',
11908 /*includeNamespaceInQueryParams=*/ namespace !== parsedUrl.subdomain),
11909 path: new Path(parsedUrl.pathString)
11910 };
11911};
11912const parseDatabaseURL = function (dataURL) {
11913 // Default to empty strings in the event of a malformed string.
11914 let host = '', domain = '', subdomain = '', pathString = '', namespace = '';
11915 // Always default to SSL, unless otherwise specified.
11916 let secure = true, scheme = 'https', port = 443;
11917 // Don't do any validation here. The caller is responsible for validating the result of parsing.
11918 if (typeof dataURL === 'string') {
11919 // Parse scheme.
11920 let colonInd = dataURL.indexOf('//');
11921 if (colonInd >= 0) {
11922 scheme = dataURL.substring(0, colonInd - 1);
11923 dataURL = dataURL.substring(colonInd + 2);
11924 }
11925 // Parse host, path, and query string.
11926 let slashInd = dataURL.indexOf('/');
11927 if (slashInd === -1) {
11928 slashInd = dataURL.length;
11929 }
11930 let questionMarkInd = dataURL.indexOf('?');
11931 if (questionMarkInd === -1) {
11932 questionMarkInd = dataURL.length;
11933 }
11934 host = dataURL.substring(0, Math.min(slashInd, questionMarkInd));
11935 if (slashInd < questionMarkInd) {
11936 // For pathString, questionMarkInd will always come after slashInd
11937 pathString = decodePath(dataURL.substring(slashInd, questionMarkInd));
11938 }
11939 const queryParams = decodeQuery(dataURL.substring(Math.min(dataURL.length, questionMarkInd)));
11940 // If we have a port, use scheme for determining if it's secure.
11941 colonInd = host.indexOf(':');
11942 if (colonInd >= 0) {
11943 secure = scheme === 'https' || scheme === 'wss';
11944 port = parseInt(host.substring(colonInd + 1), 10);
11945 }
11946 else {
11947 colonInd = host.length;
11948 }
11949 const hostWithoutPort = host.slice(0, colonInd);
11950 if (hostWithoutPort.toLowerCase() === 'localhost') {
11951 domain = 'localhost';
11952 }
11953 else if (hostWithoutPort.split('.').length <= 2) {
11954 domain = hostWithoutPort;
11955 }
11956 else {
11957 // Interpret the subdomain of a 3 or more component URL as the namespace name.
11958 const dotInd = host.indexOf('.');
11959 subdomain = host.substring(0, dotInd).toLowerCase();
11960 domain = host.substring(dotInd + 1);
11961 // Normalize namespaces to lowercase to share storage / connection.
11962 namespace = subdomain;
11963 }
11964 // Always treat the value of the `ns` as the namespace name if it is present.
11965 if ('ns' in queryParams) {
11966 namespace = queryParams['ns'];
11967 }
11968 }
11969 return {
11970 host,
11971 port,
11972 domain,
11973 subdomain,
11974 secure,
11975 scheme,
11976 pathString,
11977 namespace
11978 };
11979};
11980
11981/**
11982 * @license
11983 * Copyright 2017 Google LLC
11984 *
11985 * Licensed under the Apache License, Version 2.0 (the "License");
11986 * you may not use this file except in compliance with the License.
11987 * You may obtain a copy of the License at
11988 *
11989 * http://www.apache.org/licenses/LICENSE-2.0
11990 *
11991 * Unless required by applicable law or agreed to in writing, software
11992 * distributed under the License is distributed on an "AS IS" BASIS,
11993 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11994 * See the License for the specific language governing permissions and
11995 * limitations under the License.
11996 */
11997/**
11998 * Encapsulates the data needed to raise an event
11999 */
12000class DataEvent {
12001 /**
12002 * @param eventType - One of: value, child_added, child_changed, child_moved, child_removed
12003 * @param eventRegistration - The function to call to with the event data. User provided
12004 * @param snapshot - The data backing the event
12005 * @param prevName - Optional, the name of the previous child for child_* events.
12006 */
12007 constructor(eventType, eventRegistration, snapshot, prevName) {
12008 this.eventType = eventType;
12009 this.eventRegistration = eventRegistration;
12010 this.snapshot = snapshot;
12011 this.prevName = prevName;
12012 }
12013 getPath() {
12014 const ref = this.snapshot.ref;
12015 if (this.eventType === 'value') {
12016 return ref._path;
12017 }
12018 else {
12019 return ref.parent._path;
12020 }
12021 }
12022 getEventType() {
12023 return this.eventType;
12024 }
12025 getEventRunner() {
12026 return this.eventRegistration.getEventRunner(this);
12027 }
12028 toString() {
12029 return (this.getPath().toString() +
12030 ':' +
12031 this.eventType +
12032 ':' +
12033 stringify(this.snapshot.exportVal()));
12034 }
12035}
12036class CancelEvent {
12037 constructor(eventRegistration, error, path) {
12038 this.eventRegistration = eventRegistration;
12039 this.error = error;
12040 this.path = path;
12041 }
12042 getPath() {
12043 return this.path;
12044 }
12045 getEventType() {
12046 return 'cancel';
12047 }
12048 getEventRunner() {
12049 return this.eventRegistration.getEventRunner(this);
12050 }
12051 toString() {
12052 return this.path.toString() + ':cancel';
12053 }
12054}
12055
12056/**
12057 * @license
12058 * Copyright 2017 Google LLC
12059 *
12060 * Licensed under the Apache License, Version 2.0 (the "License");
12061 * you may not use this file except in compliance with the License.
12062 * You may obtain a copy of the License at
12063 *
12064 * http://www.apache.org/licenses/LICENSE-2.0
12065 *
12066 * Unless required by applicable law or agreed to in writing, software
12067 * distributed under the License is distributed on an "AS IS" BASIS,
12068 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12069 * See the License for the specific language governing permissions and
12070 * limitations under the License.
12071 */
12072/**
12073 * A wrapper class that converts events from the database@exp SDK to the legacy
12074 * Database SDK. Events are not converted directly as event registration relies
12075 * on reference comparison of the original user callback (see `matches()`) and
12076 * relies on equality of the legacy SDK's `context` object.
12077 */
12078class CallbackContext {
12079 constructor(snapshotCallback, cancelCallback) {
12080 this.snapshotCallback = snapshotCallback;
12081 this.cancelCallback = cancelCallback;
12082 }
12083 onValue(expDataSnapshot, previousChildName) {
12084 this.snapshotCallback.call(null, expDataSnapshot, previousChildName);
12085 }
12086 onCancel(error) {
12087 assert(this.hasCancelCallback, 'Raising a cancel event on a listener with no cancel callback');
12088 return this.cancelCallback.call(null, error);
12089 }
12090 get hasCancelCallback() {
12091 return !!this.cancelCallback;
12092 }
12093 matches(other) {
12094 return (this.snapshotCallback === other.snapshotCallback ||
12095 (this.snapshotCallback.userCallback !== undefined &&
12096 this.snapshotCallback.userCallback ===
12097 other.snapshotCallback.userCallback &&
12098 this.snapshotCallback.context === other.snapshotCallback.context));
12099 }
12100}
12101
12102/**
12103 * @license
12104 * Copyright 2021 Google LLC
12105 *
12106 * Licensed under the Apache License, Version 2.0 (the "License");
12107 * you may not use this file except in compliance with the License.
12108 * You may obtain a copy of the License at
12109 *
12110 * http://www.apache.org/licenses/LICENSE-2.0
12111 *
12112 * Unless required by applicable law or agreed to in writing, software
12113 * distributed under the License is distributed on an "AS IS" BASIS,
12114 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12115 * See the License for the specific language governing permissions and
12116 * limitations under the License.
12117 */
12118/**
12119 * The `onDisconnect` class allows you to write or clear data when your client
12120 * disconnects from the Database server. These updates occur whether your
12121 * client disconnects cleanly or not, so you can rely on them to clean up data
12122 * even if a connection is dropped or a client crashes.
12123 *
12124 * The `onDisconnect` class is most commonly used to manage presence in
12125 * applications where it is useful to detect how many clients are connected and
12126 * when other clients disconnect. See
12127 * {@link https://firebase.google.com/docs/database/web/offline-capabilities | Enabling Offline Capabilities in JavaScript}
12128 * for more information.
12129 *
12130 * To avoid problems when a connection is dropped before the requests can be
12131 * transferred to the Database server, these functions should be called before
12132 * writing any data.
12133 *
12134 * Note that `onDisconnect` operations are only triggered once. If you want an
12135 * operation to occur each time a disconnect occurs, you'll need to re-establish
12136 * the `onDisconnect` operations each time you reconnect.
12137 */
12138class OnDisconnect {
12139 /** @hideconstructor */
12140 constructor(_repo, _path) {
12141 this._repo = _repo;
12142 this._path = _path;
12143 }
12144 /**
12145 * Cancels all previously queued `onDisconnect()` set or update events for this
12146 * location and all children.
12147 *
12148 * If a write has been queued for this location via a `set()` or `update()` at a
12149 * parent location, the write at this location will be canceled, though writes
12150 * to sibling locations will still occur.
12151 *
12152 * @returns Resolves when synchronization to the server is complete.
12153 */
12154 cancel() {
12155 const deferred = new Deferred();
12156 repoOnDisconnectCancel(this._repo, this._path, deferred.wrapCallback(() => { }));
12157 return deferred.promise;
12158 }
12159 /**
12160 * Ensures the data at this location is deleted when the client is disconnected
12161 * (due to closing the browser, navigating to a new page, or network issues).
12162 *
12163 * @returns Resolves when synchronization to the server is complete.
12164 */
12165 remove() {
12166 validateWritablePath('OnDisconnect.remove', this._path);
12167 const deferred = new Deferred();
12168 repoOnDisconnectSet(this._repo, this._path, null, deferred.wrapCallback(() => { }));
12169 return deferred.promise;
12170 }
12171 /**
12172 * Ensures the data at this location is set to the specified value when the
12173 * client is disconnected (due to closing the browser, navigating to a new page,
12174 * or network issues).
12175 *
12176 * `set()` is especially useful for implementing "presence" systems, where a
12177 * value should be changed or cleared when a user disconnects so that they
12178 * appear "offline" to other users. See
12179 * {@link https://firebase.google.com/docs/database/web/offline-capabilities | Enabling Offline Capabilities in JavaScript}
12180 * for more information.
12181 *
12182 * Note that `onDisconnect` operations are only triggered once. If you want an
12183 * operation to occur each time a disconnect occurs, you'll need to re-establish
12184 * the `onDisconnect` operations each time.
12185 *
12186 * @param value - The value to be written to this location on disconnect (can
12187 * be an object, array, string, number, boolean, or null).
12188 * @returns Resolves when synchronization to the Database is complete.
12189 */
12190 set(value) {
12191 validateWritablePath('OnDisconnect.set', this._path);
12192 validateFirebaseDataArg('OnDisconnect.set', value, this._path, false);
12193 const deferred = new Deferred();
12194 repoOnDisconnectSet(this._repo, this._path, value, deferred.wrapCallback(() => { }));
12195 return deferred.promise;
12196 }
12197 /**
12198 * Ensures the data at this location is set to the specified value and priority
12199 * when the client is disconnected (due to closing the browser, navigating to a
12200 * new page, or network issues).
12201 *
12202 * @param value - The value to be written to this location on disconnect (can
12203 * be an object, array, string, number, boolean, or null).
12204 * @param priority - The priority to be written (string, number, or null).
12205 * @returns Resolves when synchronization to the Database is complete.
12206 */
12207 setWithPriority(value, priority) {
12208 validateWritablePath('OnDisconnect.setWithPriority', this._path);
12209 validateFirebaseDataArg('OnDisconnect.setWithPriority', value, this._path, false);
12210 validatePriority('OnDisconnect.setWithPriority', priority, false);
12211 const deferred = new Deferred();
12212 repoOnDisconnectSetWithPriority(this._repo, this._path, value, priority, deferred.wrapCallback(() => { }));
12213 return deferred.promise;
12214 }
12215 /**
12216 * Writes multiple values at this location when the client is disconnected (due
12217 * to closing the browser, navigating to a new page, or network issues).
12218 *
12219 * The `values` argument contains multiple property-value pairs that will be
12220 * written to the Database together. Each child property can either be a simple
12221 * property (for example, "name") or a relative path (for example, "name/first")
12222 * from the current location to the data to update.
12223 *
12224 * As opposed to the `set()` method, `update()` can be use to selectively update
12225 * only the referenced properties at the current location (instead of replacing
12226 * all the child properties at the current location).
12227 *
12228 * @param values - Object containing multiple values.
12229 * @returns Resolves when synchronization to the Database is complete.
12230 */
12231 update(values) {
12232 validateWritablePath('OnDisconnect.update', this._path);
12233 validateFirebaseMergeDataArg('OnDisconnect.update', values, this._path, false);
12234 const deferred = new Deferred();
12235 repoOnDisconnectUpdate(this._repo, this._path, values, deferred.wrapCallback(() => { }));
12236 return deferred.promise;
12237 }
12238}
12239
12240/**
12241 * @license
12242 * Copyright 2020 Google LLC
12243 *
12244 * Licensed under the Apache License, Version 2.0 (the "License");
12245 * you may not use this file except in compliance with the License.
12246 * You may obtain a copy of the License at
12247 *
12248 * http://www.apache.org/licenses/LICENSE-2.0
12249 *
12250 * Unless required by applicable law or agreed to in writing, software
12251 * distributed under the License is distributed on an "AS IS" BASIS,
12252 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12253 * See the License for the specific language governing permissions and
12254 * limitations under the License.
12255 */
12256/**
12257 * @internal
12258 */
12259class QueryImpl {
12260 /**
12261 * @hideconstructor
12262 */
12263 constructor(_repo, _path, _queryParams, _orderByCalled) {
12264 this._repo = _repo;
12265 this._path = _path;
12266 this._queryParams = _queryParams;
12267 this._orderByCalled = _orderByCalled;
12268 }
12269 get key() {
12270 if (pathIsEmpty(this._path)) {
12271 return null;
12272 }
12273 else {
12274 return pathGetBack(this._path);
12275 }
12276 }
12277 get ref() {
12278 return new ReferenceImpl(this._repo, this._path);
12279 }
12280 get _queryIdentifier() {
12281 const obj = queryParamsGetQueryObject(this._queryParams);
12282 const id = ObjectToUniqueKey(obj);
12283 return id === '{}' ? 'default' : id;
12284 }
12285 /**
12286 * An object representation of the query parameters used by this Query.
12287 */
12288 get _queryObject() {
12289 return queryParamsGetQueryObject(this._queryParams);
12290 }
12291 isEqual(other) {
12292 other = getModularInstance(other);
12293 if (!(other instanceof QueryImpl)) {
12294 return false;
12295 }
12296 const sameRepo = this._repo === other._repo;
12297 const samePath = pathEquals(this._path, other._path);
12298 const sameQueryIdentifier = this._queryIdentifier === other._queryIdentifier;
12299 return sameRepo && samePath && sameQueryIdentifier;
12300 }
12301 toJSON() {
12302 return this.toString();
12303 }
12304 toString() {
12305 return this._repo.toString() + pathToUrlEncodedString(this._path);
12306 }
12307}
12308/**
12309 * Validates that no other order by call has been made
12310 */
12311function validateNoPreviousOrderByCall(query, fnName) {
12312 if (query._orderByCalled === true) {
12313 throw new Error(fnName + ": You can't combine multiple orderBy calls.");
12314 }
12315}
12316/**
12317 * Validates start/end values for queries.
12318 */
12319function validateQueryEndpoints(params) {
12320 let startNode = null;
12321 let endNode = null;
12322 if (params.hasStart()) {
12323 startNode = params.getIndexStartValue();
12324 }
12325 if (params.hasEnd()) {
12326 endNode = params.getIndexEndValue();
12327 }
12328 if (params.getIndex() === KEY_INDEX) {
12329 const tooManyArgsError = 'Query: When ordering by key, you may only pass one argument to ' +
12330 'startAt(), endAt(), or equalTo().';
12331 const wrongArgTypeError = 'Query: When ordering by key, the argument passed to startAt(), startAfter(), ' +
12332 'endAt(), endBefore(), or equalTo() must be a string.';
12333 if (params.hasStart()) {
12334 const startName = params.getIndexStartName();
12335 if (startName !== MIN_NAME) {
12336 throw new Error(tooManyArgsError);
12337 }
12338 else if (typeof startNode !== 'string') {
12339 throw new Error(wrongArgTypeError);
12340 }
12341 }
12342 if (params.hasEnd()) {
12343 const endName = params.getIndexEndName();
12344 if (endName !== MAX_NAME) {
12345 throw new Error(tooManyArgsError);
12346 }
12347 else if (typeof endNode !== 'string') {
12348 throw new Error(wrongArgTypeError);
12349 }
12350 }
12351 }
12352 else if (params.getIndex() === PRIORITY_INDEX) {
12353 if ((startNode != null && !isValidPriority(startNode)) ||
12354 (endNode != null && !isValidPriority(endNode))) {
12355 throw new Error('Query: When ordering by priority, the first argument passed to startAt(), ' +
12356 'startAfter() endAt(), endBefore(), or equalTo() must be a valid priority value ' +
12357 '(null, a number, or a string).');
12358 }
12359 }
12360 else {
12361 assert(params.getIndex() instanceof PathIndex ||
12362 params.getIndex() === VALUE_INDEX, 'unknown index type.');
12363 if ((startNode != null && typeof startNode === 'object') ||
12364 (endNode != null && typeof endNode === 'object')) {
12365 throw new Error('Query: First argument passed to startAt(), startAfter(), endAt(), endBefore(), or ' +
12366 'equalTo() cannot be an object.');
12367 }
12368 }
12369}
12370/**
12371 * Validates that limit* has been called with the correct combination of parameters
12372 */
12373function validateLimit(params) {
12374 if (params.hasStart() &&
12375 params.hasEnd() &&
12376 params.hasLimit() &&
12377 !params.hasAnchoredLimit()) {
12378 throw new Error("Query: Can't combine startAt(), startAfter(), endAt(), endBefore(), and limit(). Use " +
12379 'limitToFirst() or limitToLast() instead.');
12380 }
12381}
12382/**
12383 * @internal
12384 */
12385class ReferenceImpl extends QueryImpl {
12386 /** @hideconstructor */
12387 constructor(repo, path) {
12388 super(repo, path, new QueryParams(), false);
12389 }
12390 get parent() {
12391 const parentPath = pathParent(this._path);
12392 return parentPath === null
12393 ? null
12394 : new ReferenceImpl(this._repo, parentPath);
12395 }
12396 get root() {
12397 let ref = this;
12398 while (ref.parent !== null) {
12399 ref = ref.parent;
12400 }
12401 return ref;
12402 }
12403}
12404/**
12405 * A `DataSnapshot` contains data from a Database location.
12406 *
12407 * Any time you read data from the Database, you receive the data as a
12408 * `DataSnapshot`. A `DataSnapshot` is passed to the event callbacks you attach
12409 * with `on()` or `once()`. You can extract the contents of the snapshot as a
12410 * JavaScript object by calling the `val()` method. Alternatively, you can
12411 * traverse into the snapshot by calling `child()` to return child snapshots
12412 * (which you could then call `val()` on).
12413 *
12414 * A `DataSnapshot` is an efficiently generated, immutable copy of the data at
12415 * a Database location. It cannot be modified and will never change (to modify
12416 * data, you always call the `set()` method on a `Reference` directly).
12417 */
12418class DataSnapshot {
12419 /**
12420 * @param _node - A SnapshotNode to wrap.
12421 * @param ref - The location this snapshot came from.
12422 * @param _index - The iteration order for this snapshot
12423 * @hideconstructor
12424 */
12425 constructor(_node,
12426 /**
12427 * The location of this DataSnapshot.
12428 */
12429 ref, _index) {
12430 this._node = _node;
12431 this.ref = ref;
12432 this._index = _index;
12433 }
12434 /**
12435 * Gets the priority value of the data in this `DataSnapshot`.
12436 *
12437 * Applications need not use priority but can order collections by
12438 * ordinary properties (see
12439 * {@link https://firebase.google.com/docs/database/web/lists-of-data#sorting_and_filtering_data |Sorting and filtering data}
12440 * ).
12441 */
12442 get priority() {
12443 // typecast here because we never return deferred values or internal priorities (MAX_PRIORITY)
12444 return this._node.getPriority().val();
12445 }
12446 /**
12447 * The key (last part of the path) of the location of this `DataSnapshot`.
12448 *
12449 * The last token in a Database location is considered its key. For example,
12450 * "ada" is the key for the /users/ada/ node. Accessing the key on any
12451 * `DataSnapshot` will return the key for the location that generated it.
12452 * However, accessing the key on the root URL of a Database will return
12453 * `null`.
12454 */
12455 get key() {
12456 return this.ref.key;
12457 }
12458 /** Returns the number of child properties of this `DataSnapshot`. */
12459 get size() {
12460 return this._node.numChildren();
12461 }
12462 /**
12463 * Gets another `DataSnapshot` for the location at the specified relative path.
12464 *
12465 * Passing a relative path to the `child()` method of a DataSnapshot returns
12466 * another `DataSnapshot` for the location at the specified relative path. The
12467 * relative path can either be a simple child name (for example, "ada") or a
12468 * deeper, slash-separated path (for example, "ada/name/first"). If the child
12469 * location has no data, an empty `DataSnapshot` (that is, a `DataSnapshot`
12470 * whose value is `null`) is returned.
12471 *
12472 * @param path - A relative path to the location of child data.
12473 */
12474 child(path) {
12475 const childPath = new Path(path);
12476 const childRef = child(this.ref, path);
12477 return new DataSnapshot(this._node.getChild(childPath), childRef, PRIORITY_INDEX);
12478 }
12479 /**
12480 * Returns true if this `DataSnapshot` contains any data. It is slightly more
12481 * efficient than using `snapshot.val() !== null`.
12482 */
12483 exists() {
12484 return !this._node.isEmpty();
12485 }
12486 /**
12487 * Exports the entire contents of the DataSnapshot as a JavaScript object.
12488 *
12489 * The `exportVal()` method is similar to `val()`, except priority information
12490 * is included (if available), making it suitable for backing up your data.
12491 *
12492 * @returns The DataSnapshot's contents as a JavaScript value (Object,
12493 * Array, string, number, boolean, or `null`).
12494 */
12495 // eslint-disable-next-line @typescript-eslint/no-explicit-any
12496 exportVal() {
12497 return this._node.val(true);
12498 }
12499 /**
12500 * Enumerates the top-level children in the `DataSnapshot`.
12501 *
12502 * Because of the way JavaScript objects work, the ordering of data in the
12503 * JavaScript object returned by `val()` is not guaranteed to match the
12504 * ordering on the server nor the ordering of `onChildAdded()` events. That is
12505 * where `forEach()` comes in handy. It guarantees the children of a
12506 * `DataSnapshot` will be iterated in their query order.
12507 *
12508 * If no explicit `orderBy*()` method is used, results are returned
12509 * ordered by key (unless priorities are used, in which case, results are
12510 * returned by priority).
12511 *
12512 * @param action - A function that will be called for each child DataSnapshot.
12513 * The callback can return true to cancel further enumeration.
12514 * @returns true if enumeration was canceled due to your callback returning
12515 * true.
12516 */
12517 forEach(action) {
12518 if (this._node.isLeafNode()) {
12519 return false;
12520 }
12521 const childrenNode = this._node;
12522 // Sanitize the return value to a boolean. ChildrenNode.forEachChild has a weird return type...
12523 return !!childrenNode.forEachChild(this._index, (key, node) => {
12524 return action(new DataSnapshot(node, child(this.ref, key), PRIORITY_INDEX));
12525 });
12526 }
12527 /**
12528 * Returns true if the specified child path has (non-null) data.
12529 *
12530 * @param path - A relative path to the location of a potential child.
12531 * @returns `true` if data exists at the specified child path; else
12532 * `false`.
12533 */
12534 hasChild(path) {
12535 const childPath = new Path(path);
12536 return !this._node.getChild(childPath).isEmpty();
12537 }
12538 /**
12539 * Returns whether or not the `DataSnapshot` has any non-`null` child
12540 * properties.
12541 *
12542 * You can use `hasChildren()` to determine if a `DataSnapshot` has any
12543 * children. If it does, you can enumerate them using `forEach()`. If it
12544 * doesn't, then either this snapshot contains a primitive value (which can be
12545 * retrieved with `val()`) or it is empty (in which case, `val()` will return
12546 * `null`).
12547 *
12548 * @returns true if this snapshot has any children; else false.
12549 */
12550 hasChildren() {
12551 if (this._node.isLeafNode()) {
12552 return false;
12553 }
12554 else {
12555 return !this._node.isEmpty();
12556 }
12557 }
12558 /**
12559 * Returns a JSON-serializable representation of this object.
12560 */
12561 toJSON() {
12562 return this.exportVal();
12563 }
12564 /**
12565 * Extracts a JavaScript value from a `DataSnapshot`.
12566 *
12567 * Depending on the data in a `DataSnapshot`, the `val()` method may return a
12568 * scalar type (string, number, or boolean), an array, or an object. It may
12569 * also return null, indicating that the `DataSnapshot` is empty (contains no
12570 * data).
12571 *
12572 * @returns The DataSnapshot's contents as a JavaScript value (Object,
12573 * Array, string, number, boolean, or `null`).
12574 */
12575 // eslint-disable-next-line @typescript-eslint/no-explicit-any
12576 val() {
12577 return this._node.val();
12578 }
12579}
12580/**
12581 *
12582 * Returns a `Reference` representing the location in the Database
12583 * corresponding to the provided path. If no path is provided, the `Reference`
12584 * will point to the root of the Database.
12585 *
12586 * @param db - The database instance to obtain a reference for.
12587 * @param path - Optional path representing the location the returned
12588 * `Reference` will point. If not provided, the returned `Reference` will
12589 * point to the root of the Database.
12590 * @returns If a path is provided, a `Reference`
12591 * pointing to the provided path. Otherwise, a `Reference` pointing to the
12592 * root of the Database.
12593 */
12594function ref(db, path) {
12595 db = getModularInstance(db);
12596 db._checkNotDeleted('ref');
12597 return path !== undefined ? child(db._root, path) : db._root;
12598}
12599/**
12600 * Returns a `Reference` representing the location in the Database
12601 * corresponding to the provided Firebase URL.
12602 *
12603 * An exception is thrown if the URL is not a valid Firebase Database URL or it
12604 * has a different domain than the current `Database` instance.
12605 *
12606 * Note that all query parameters (`orderBy`, `limitToLast`, etc.) are ignored
12607 * and are not applied to the returned `Reference`.
12608 *
12609 * @param db - The database instance to obtain a reference for.
12610 * @param url - The Firebase URL at which the returned `Reference` will
12611 * point.
12612 * @returns A `Reference` pointing to the provided
12613 * Firebase URL.
12614 */
12615function refFromURL(db, url) {
12616 db = getModularInstance(db);
12617 db._checkNotDeleted('refFromURL');
12618 const parsedURL = parseRepoInfo(url, db._repo.repoInfo_.nodeAdmin);
12619 validateUrl('refFromURL', parsedURL);
12620 const repoInfo = parsedURL.repoInfo;
12621 if (!db._repo.repoInfo_.isCustomHost() &&
12622 repoInfo.host !== db._repo.repoInfo_.host) {
12623 fatal('refFromURL' +
12624 ': Host name does not match the current database: ' +
12625 '(found ' +
12626 repoInfo.host +
12627 ' but expected ' +
12628 db._repo.repoInfo_.host +
12629 ')');
12630 }
12631 return ref(db, parsedURL.path.toString());
12632}
12633/**
12634 * Gets a `Reference` for the location at the specified relative path.
12635 *
12636 * The relative path can either be a simple child name (for example, "ada") or
12637 * a deeper slash-separated path (for example, "ada/name/first").
12638 *
12639 * @param parent - The parent location.
12640 * @param path - A relative path from this location to the desired child
12641 * location.
12642 * @returns The specified child location.
12643 */
12644function child(parent, path) {
12645 parent = getModularInstance(parent);
12646 if (pathGetFront(parent._path) === null) {
12647 validateRootPathString('child', 'path', path, false);
12648 }
12649 else {
12650 validatePathString('child', 'path', path, false);
12651 }
12652 return new ReferenceImpl(parent._repo, pathChild(parent._path, path));
12653}
12654/**
12655 * Returns an `OnDisconnect` object - see
12656 * {@link https://firebase.google.com/docs/database/web/offline-capabilities | Enabling Offline Capabilities in JavaScript}
12657 * for more information on how to use it.
12658 *
12659 * @param ref - The reference to add OnDisconnect triggers for.
12660 */
12661function onDisconnect(ref) {
12662 ref = getModularInstance(ref);
12663 return new OnDisconnect(ref._repo, ref._path);
12664}
12665/**
12666 * Generates a new child location using a unique key and returns its
12667 * `Reference`.
12668 *
12669 * This is the most common pattern for adding data to a collection of items.
12670 *
12671 * If you provide a value to `push()`, the value is written to the
12672 * generated location. If you don't pass a value, nothing is written to the
12673 * database and the child remains empty (but you can use the `Reference`
12674 * elsewhere).
12675 *
12676 * The unique keys generated by `push()` are ordered by the current time, so the
12677 * resulting list of items is chronologically sorted. The keys are also
12678 * designed to be unguessable (they contain 72 random bits of entropy).
12679 *
12680 * See {@link https://firebase.google.com/docs/database/web/lists-of-data#append_to_a_list_of_data | Append to a list of data}
12681 * </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}
12682 *
12683 * @param parent - The parent location.
12684 * @param value - Optional value to be written at the generated location.
12685 * @returns Combined `Promise` and `Reference`; resolves when write is complete,
12686 * but can be used immediately as the `Reference` to the child location.
12687 */
12688function push(parent, value) {
12689 parent = getModularInstance(parent);
12690 validateWritablePath('push', parent._path);
12691 validateFirebaseDataArg('push', value, parent._path, true);
12692 const now = repoServerTime(parent._repo);
12693 const name = nextPushId(now);
12694 // push() returns a ThennableReference whose promise is fulfilled with a
12695 // regular Reference. We use child() to create handles to two different
12696 // references. The first is turned into a ThennableReference below by adding
12697 // then() and catch() methods and is used as the return value of push(). The
12698 // second remains a regular Reference and is used as the fulfilled value of
12699 // the first ThennableReference.
12700 const thennablePushRef = child(parent, name);
12701 const pushRef = child(parent, name);
12702 let promise;
12703 if (value != null) {
12704 promise = set(pushRef, value).then(() => pushRef);
12705 }
12706 else {
12707 promise = Promise.resolve(pushRef);
12708 }
12709 thennablePushRef.then = promise.then.bind(promise);
12710 thennablePushRef.catch = promise.then.bind(promise, undefined);
12711 return thennablePushRef;
12712}
12713/**
12714 * Removes the data at this Database location.
12715 *
12716 * Any data at child locations will also be deleted.
12717 *
12718 * The effect of the remove will be visible immediately and the corresponding
12719 * event 'value' will be triggered. Synchronization of the remove to the
12720 * Firebase servers will also be started, and the returned Promise will resolve
12721 * when complete. If provided, the onComplete callback will be called
12722 * asynchronously after synchronization has finished.
12723 *
12724 * @param ref - The location to remove.
12725 * @returns Resolves when remove on server is complete.
12726 */
12727function remove(ref) {
12728 validateWritablePath('remove', ref._path);
12729 return set(ref, null);
12730}
12731/**
12732 * Writes data to this Database location.
12733 *
12734 * This will overwrite any data at this location and all child locations.
12735 *
12736 * The effect of the write will be visible immediately, and the corresponding
12737 * events ("value", "child_added", etc.) will be triggered. Synchronization of
12738 * the data to the Firebase servers will also be started, and the returned
12739 * Promise will resolve when complete. If provided, the `onComplete` callback
12740 * will be called asynchronously after synchronization has finished.
12741 *
12742 * Passing `null` for the new value is equivalent to calling `remove()`; namely,
12743 * all data at this location and all child locations will be deleted.
12744 *
12745 * `set()` will remove any priority stored at this location, so if priority is
12746 * meant to be preserved, you need to use `setWithPriority()` instead.
12747 *
12748 * Note that modifying data with `set()` will cancel any pending transactions
12749 * at that location, so extreme care should be taken if mixing `set()` and
12750 * `transaction()` to modify the same data.
12751 *
12752 * A single `set()` will generate a single "value" event at the location where
12753 * the `set()` was performed.
12754 *
12755 * @param ref - The location to write to.
12756 * @param value - The value to be written (string, number, boolean, object,
12757 * array, or null).
12758 * @returns Resolves when write to server is complete.
12759 */
12760function set(ref, value) {
12761 ref = getModularInstance(ref);
12762 validateWritablePath('set', ref._path);
12763 validateFirebaseDataArg('set', value, ref._path, false);
12764 const deferred = new Deferred();
12765 repoSetWithPriority(ref._repo, ref._path, value,
12766 /*priority=*/ null, deferred.wrapCallback(() => { }));
12767 return deferred.promise;
12768}
12769/**
12770 * Sets a priority for the data at this Database location.
12771 *
12772 * Applications need not use priority but can order collections by
12773 * ordinary properties (see
12774 * {@link https://firebase.google.com/docs/database/web/lists-of-data#sorting_and_filtering_data | Sorting and filtering data}
12775 * ).
12776 *
12777 * @param ref - The location to write to.
12778 * @param priority - The priority to be written (string, number, or null).
12779 * @returns Resolves when write to server is complete.
12780 */
12781function setPriority(ref, priority) {
12782 ref = getModularInstance(ref);
12783 validateWritablePath('setPriority', ref._path);
12784 validatePriority('setPriority', priority, false);
12785 const deferred = new Deferred();
12786 repoSetWithPriority(ref._repo, pathChild(ref._path, '.priority'), priority, null, deferred.wrapCallback(() => { }));
12787 return deferred.promise;
12788}
12789/**
12790 * Writes data the Database location. Like `set()` but also specifies the
12791 * priority for that data.
12792 *
12793 * Applications need not use priority but can order collections by
12794 * ordinary properties (see
12795 * {@link https://firebase.google.com/docs/database/web/lists-of-data#sorting_and_filtering_data | Sorting and filtering data}
12796 * ).
12797 *
12798 * @param ref - The location to write to.
12799 * @param value - The value to be written (string, number, boolean, object,
12800 * array, or null).
12801 * @param priority - The priority to be written (string, number, or null).
12802 * @returns Resolves when write to server is complete.
12803 */
12804function setWithPriority(ref, value, priority) {
12805 validateWritablePath('setWithPriority', ref._path);
12806 validateFirebaseDataArg('setWithPriority', value, ref._path, false);
12807 validatePriority('setWithPriority', priority, false);
12808 if (ref.key === '.length' || ref.key === '.keys') {
12809 throw 'setWithPriority failed: ' + ref.key + ' is a read-only object.';
12810 }
12811 const deferred = new Deferred();
12812 repoSetWithPriority(ref._repo, ref._path, value, priority, deferred.wrapCallback(() => { }));
12813 return deferred.promise;
12814}
12815/**
12816 * Writes multiple values to the Database at once.
12817 *
12818 * The `values` argument contains multiple property-value pairs that will be
12819 * written to the Database together. Each child property can either be a simple
12820 * property (for example, "name") or a relative path (for example,
12821 * "name/first") from the current location to the data to update.
12822 *
12823 * As opposed to the `set()` method, `update()` can be use to selectively update
12824 * only the referenced properties at the current location (instead of replacing
12825 * all the child properties at the current location).
12826 *
12827 * The effect of the write will be visible immediately, and the corresponding
12828 * events ('value', 'child_added', etc.) will be triggered. Synchronization of
12829 * the data to the Firebase servers will also be started, and the returned
12830 * Promise will resolve when complete. If provided, the `onComplete` callback
12831 * will be called asynchronously after synchronization has finished.
12832 *
12833 * A single `update()` will generate a single "value" event at the location
12834 * where the `update()` was performed, regardless of how many children were
12835 * modified.
12836 *
12837 * Note that modifying data with `update()` will cancel any pending
12838 * transactions at that location, so extreme care should be taken if mixing
12839 * `update()` and `transaction()` to modify the same data.
12840 *
12841 * Passing `null` to `update()` will remove the data at this location.
12842 *
12843 * See
12844 * {@link https://firebase.googleblog.com/2015/09/introducing-multi-location-updates-and_86.html | Introducing multi-location updates and more}.
12845 *
12846 * @param ref - The location to write to.
12847 * @param values - Object containing multiple values.
12848 * @returns Resolves when update on server is complete.
12849 */
12850function update(ref, values) {
12851 validateFirebaseMergeDataArg('update', values, ref._path, false);
12852 const deferred = new Deferred();
12853 repoUpdate(ref._repo, ref._path, values, deferred.wrapCallback(() => { }));
12854 return deferred.promise;
12855}
12856/**
12857 * Gets the most up-to-date result for this query.
12858 *
12859 * @param query - The query to run.
12860 * @returns A `Promise` which resolves to the resulting DataSnapshot if a value is
12861 * available, or rejects if the client is unable to return a value (e.g., if the
12862 * server is unreachable and there is nothing cached).
12863 */
12864function get(query) {
12865 query = getModularInstance(query);
12866 return repoGetValue(query._repo, query).then(node => {
12867 return new DataSnapshot(node, new ReferenceImpl(query._repo, query._path), query._queryParams.getIndex());
12868 });
12869}
12870/**
12871 * Represents registration for 'value' events.
12872 */
12873class ValueEventRegistration {
12874 constructor(callbackContext) {
12875 this.callbackContext = callbackContext;
12876 }
12877 respondsTo(eventType) {
12878 return eventType === 'value';
12879 }
12880 createEvent(change, query) {
12881 const index = query._queryParams.getIndex();
12882 return new DataEvent('value', this, new DataSnapshot(change.snapshotNode, new ReferenceImpl(query._repo, query._path), index));
12883 }
12884 getEventRunner(eventData) {
12885 if (eventData.getEventType() === 'cancel') {
12886 return () => this.callbackContext.onCancel(eventData.error);
12887 }
12888 else {
12889 return () => this.callbackContext.onValue(eventData.snapshot, null);
12890 }
12891 }
12892 createCancelEvent(error, path) {
12893 if (this.callbackContext.hasCancelCallback) {
12894 return new CancelEvent(this, error, path);
12895 }
12896 else {
12897 return null;
12898 }
12899 }
12900 matches(other) {
12901 if (!(other instanceof ValueEventRegistration)) {
12902 return false;
12903 }
12904 else if (!other.callbackContext || !this.callbackContext) {
12905 // If no callback specified, we consider it to match any callback.
12906 return true;
12907 }
12908 else {
12909 return other.callbackContext.matches(this.callbackContext);
12910 }
12911 }
12912 hasAnyCallback() {
12913 return this.callbackContext !== null;
12914 }
12915}
12916/**
12917 * Represents the registration of a child_x event.
12918 */
12919class ChildEventRegistration {
12920 constructor(eventType, callbackContext) {
12921 this.eventType = eventType;
12922 this.callbackContext = callbackContext;
12923 }
12924 respondsTo(eventType) {
12925 let eventToCheck = eventType === 'children_added' ? 'child_added' : eventType;
12926 eventToCheck =
12927 eventToCheck === 'children_removed' ? 'child_removed' : eventToCheck;
12928 return this.eventType === eventToCheck;
12929 }
12930 createCancelEvent(error, path) {
12931 if (this.callbackContext.hasCancelCallback) {
12932 return new CancelEvent(this, error, path);
12933 }
12934 else {
12935 return null;
12936 }
12937 }
12938 createEvent(change, query) {
12939 assert(change.childName != null, 'Child events should have a childName.');
12940 const childRef = child(new ReferenceImpl(query._repo, query._path), change.childName);
12941 const index = query._queryParams.getIndex();
12942 return new DataEvent(change.type, this, new DataSnapshot(change.snapshotNode, childRef, index), change.prevName);
12943 }
12944 getEventRunner(eventData) {
12945 if (eventData.getEventType() === 'cancel') {
12946 return () => this.callbackContext.onCancel(eventData.error);
12947 }
12948 else {
12949 return () => this.callbackContext.onValue(eventData.snapshot, eventData.prevName);
12950 }
12951 }
12952 matches(other) {
12953 if (other instanceof ChildEventRegistration) {
12954 return (this.eventType === other.eventType &&
12955 (!this.callbackContext ||
12956 !other.callbackContext ||
12957 this.callbackContext.matches(other.callbackContext)));
12958 }
12959 return false;
12960 }
12961 hasAnyCallback() {
12962 return !!this.callbackContext;
12963 }
12964}
12965function addEventListener(query, eventType, callback, cancelCallbackOrListenOptions, options) {
12966 let cancelCallback;
12967 if (typeof cancelCallbackOrListenOptions === 'object') {
12968 cancelCallback = undefined;
12969 options = cancelCallbackOrListenOptions;
12970 }
12971 if (typeof cancelCallbackOrListenOptions === 'function') {
12972 cancelCallback = cancelCallbackOrListenOptions;
12973 }
12974 if (options && options.onlyOnce) {
12975 const userCallback = callback;
12976 const onceCallback = (dataSnapshot, previousChildName) => {
12977 repoRemoveEventCallbackForQuery(query._repo, query, container);
12978 userCallback(dataSnapshot, previousChildName);
12979 };
12980 onceCallback.userCallback = callback.userCallback;
12981 onceCallback.context = callback.context;
12982 callback = onceCallback;
12983 }
12984 const callbackContext = new CallbackContext(callback, cancelCallback || undefined);
12985 const container = eventType === 'value'
12986 ? new ValueEventRegistration(callbackContext)
12987 : new ChildEventRegistration(eventType, callbackContext);
12988 repoAddEventCallbackForQuery(query._repo, query, container);
12989 return () => repoRemoveEventCallbackForQuery(query._repo, query, container);
12990}
12991function onValue(query, callback, cancelCallbackOrListenOptions, options) {
12992 return addEventListener(query, 'value', callback, cancelCallbackOrListenOptions, options);
12993}
12994function onChildAdded(query, callback, cancelCallbackOrListenOptions, options) {
12995 return addEventListener(query, 'child_added', callback, cancelCallbackOrListenOptions, options);
12996}
12997function onChildChanged(query, callback, cancelCallbackOrListenOptions, options) {
12998 return addEventListener(query, 'child_changed', callback, cancelCallbackOrListenOptions, options);
12999}
13000function onChildMoved(query, callback, cancelCallbackOrListenOptions, options) {
13001 return addEventListener(query, 'child_moved', callback, cancelCallbackOrListenOptions, options);
13002}
13003function onChildRemoved(query, callback, cancelCallbackOrListenOptions, options) {
13004 return addEventListener(query, 'child_removed', callback, cancelCallbackOrListenOptions, options);
13005}
13006/**
13007 * Detaches a callback previously attached with `on()`.
13008 *
13009 * Detach a callback previously attached with `on()`. Note that if `on()` was
13010 * called multiple times with the same eventType and callback, the callback
13011 * will be called multiple times for each event, and `off()` must be called
13012 * multiple times to remove the callback. Calling `off()` on a parent listener
13013 * will not automatically remove listeners registered on child nodes, `off()`
13014 * must also be called on any child listeners to remove the callback.
13015 *
13016 * If a callback is not specified, all callbacks for the specified eventType
13017 * will be removed. Similarly, if no eventType is specified, all callbacks
13018 * for the `Reference` will be removed.
13019 *
13020 * Individual listeners can also be removed by invoking their unsubscribe
13021 * callbacks.
13022 *
13023 * @param query - The query that the listener was registered with.
13024 * @param eventType - One of the following strings: "value", "child_added",
13025 * "child_changed", "child_removed", or "child_moved." If omitted, all callbacks
13026 * for the `Reference` will be removed.
13027 * @param callback - The callback function that was passed to `on()` or
13028 * `undefined` to remove all callbacks.
13029 */
13030function off(query, eventType, callback) {
13031 let container = null;
13032 const expCallback = callback ? new CallbackContext(callback) : null;
13033 if (eventType === 'value') {
13034 container = new ValueEventRegistration(expCallback);
13035 }
13036 else if (eventType) {
13037 container = new ChildEventRegistration(eventType, expCallback);
13038 }
13039 repoRemoveEventCallbackForQuery(query._repo, query, container);
13040}
13041/**
13042 * A `QueryConstraint` is used to narrow the set of documents returned by a
13043 * Database query. `QueryConstraint`s are created by invoking {@link endAt},
13044 * {@link endBefore}, {@link startAt}, {@link startAfter}, {@link
13045 * limitToFirst}, {@link limitToLast}, {@link orderByChild},
13046 * {@link orderByChild}, {@link orderByKey} , {@link orderByPriority} ,
13047 * {@link orderByValue} or {@link equalTo} and
13048 * can then be passed to {@link query} to create a new query instance that
13049 * also contains this `QueryConstraint`.
13050 */
13051class QueryConstraint {
13052}
13053class QueryEndAtConstraint extends QueryConstraint {
13054 constructor(_value, _key) {
13055 super();
13056 this._value = _value;
13057 this._key = _key;
13058 }
13059 _apply(query) {
13060 validateFirebaseDataArg('endAt', this._value, query._path, true);
13061 const newParams = queryParamsEndAt(query._queryParams, this._value, this._key);
13062 validateLimit(newParams);
13063 validateQueryEndpoints(newParams);
13064 if (query._queryParams.hasEnd()) {
13065 throw new Error('endAt: Starting point was already set (by another call to endAt, ' +
13066 'endBefore or equalTo).');
13067 }
13068 return new QueryImpl(query._repo, query._path, newParams, query._orderByCalled);
13069 }
13070}
13071/**
13072 * Creates a `QueryConstraint` with the specified ending point.
13073 *
13074 * Using `startAt()`, `startAfter()`, `endBefore()`, `endAt()` and `equalTo()`
13075 * allows you to choose arbitrary starting and ending points for your queries.
13076 *
13077 * The ending point is inclusive, so children with exactly the specified value
13078 * will be included in the query. The optional key argument can be used to
13079 * further limit the range of the query. If it is specified, then children that
13080 * have exactly the specified value must also have a key name less than or equal
13081 * to the specified key.
13082 *
13083 * You can read more about `endAt()` in
13084 * {@link https://firebase.google.com/docs/database/web/lists-of-data#filtering_data | Filtering data}.
13085 *
13086 * @param value - The value to end at. The argument type depends on which
13087 * `orderBy*()` function was used in this query. Specify a value that matches
13088 * the `orderBy*()` type. When used in combination with `orderByKey()`, the
13089 * value must be a string.
13090 * @param key - The child key to end at, among the children with the previously
13091 * specified priority. This argument is only allowed if ordering by child,
13092 * value, or priority.
13093 */
13094function endAt(value, key) {
13095 validateKey('endAt', 'key', key, true);
13096 return new QueryEndAtConstraint(value, key);
13097}
13098class QueryEndBeforeConstraint extends QueryConstraint {
13099 constructor(_value, _key) {
13100 super();
13101 this._value = _value;
13102 this._key = _key;
13103 }
13104 _apply(query) {
13105 validateFirebaseDataArg('endBefore', this._value, query._path, false);
13106 const newParams = queryParamsEndBefore(query._queryParams, this._value, this._key);
13107 validateLimit(newParams);
13108 validateQueryEndpoints(newParams);
13109 if (query._queryParams.hasEnd()) {
13110 throw new Error('endBefore: Starting point was already set (by another call to endAt, ' +
13111 'endBefore or equalTo).');
13112 }
13113 return new QueryImpl(query._repo, query._path, newParams, query._orderByCalled);
13114 }
13115}
13116/**
13117 * Creates a `QueryConstraint` with the specified ending point (exclusive).
13118 *
13119 * Using `startAt()`, `startAfter()`, `endBefore()`, `endAt()` and `equalTo()`
13120 * allows you to choose arbitrary starting and ending points for your queries.
13121 *
13122 * The ending point is exclusive. If only a value is provided, children
13123 * with a value less than the specified value will be included in the query.
13124 * If a key is specified, then children must have a value lesss than or equal
13125 * to the specified value and a a key name less than the specified key.
13126 *
13127 * @param value - The value to end before. 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 before, among the children with the
13132 * previously specified priority. This argument is only allowed if ordering by
13133 * child, value, or priority.
13134 */
13135function endBefore(value, key) {
13136 validateKey('endBefore', 'key', key, true);
13137 return new QueryEndBeforeConstraint(value, key);
13138}
13139class QueryStartAtConstraint extends QueryConstraint {
13140 constructor(_value, _key) {
13141 super();
13142 this._value = _value;
13143 this._key = _key;
13144 }
13145 _apply(query) {
13146 validateFirebaseDataArg('startAt', this._value, query._path, true);
13147 const newParams = queryParamsStartAt(query._queryParams, this._value, this._key);
13148 validateLimit(newParams);
13149 validateQueryEndpoints(newParams);
13150 if (query._queryParams.hasStart()) {
13151 throw new Error('startAt: Starting point was already set (by another call to startAt, ' +
13152 'startBefore or equalTo).');
13153 }
13154 return new QueryImpl(query._repo, query._path, newParams, query._orderByCalled);
13155 }
13156}
13157/**
13158 * Creates a `QueryConstraint` with the specified starting point.
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 starting point is inclusive, so children with exactly the specified value
13164 * will be included in the query. The optional key argument can be used to
13165 * further limit the range of the query. If it is specified, then children that
13166 * have exactly the specified value must also have a key name greater than or
13167 * equal to the specified key.
13168 *
13169 * You can read more about `startAt()` in
13170 * {@link https://firebase.google.com/docs/database/web/lists-of-data#filtering_data | Filtering data}.
13171 *
13172 * @param value - The value to start at. The argument type depends on which
13173 * `orderBy*()` function was used in this query. Specify a value that matches
13174 * the `orderBy*()` type. When used in combination with `orderByKey()`, the
13175 * value must be a string.
13176 * @param key - The child key to start at. This argument is only allowed if
13177 * ordering by child, value, or priority.
13178 */
13179function startAt(value = null, key) {
13180 validateKey('startAt', 'key', key, true);
13181 return new QueryStartAtConstraint(value, key);
13182}
13183class QueryStartAfterConstraint extends QueryConstraint {
13184 constructor(_value, _key) {
13185 super();
13186 this._value = _value;
13187 this._key = _key;
13188 }
13189 _apply(query) {
13190 validateFirebaseDataArg('startAfter', this._value, query._path, false);
13191 const newParams = queryParamsStartAfter(query._queryParams, this._value, this._key);
13192 validateLimit(newParams);
13193 validateQueryEndpoints(newParams);
13194 if (query._queryParams.hasStart()) {
13195 throw new Error('startAfter: Starting point was already set (by another call to startAt, ' +
13196 'startAfter, or equalTo).');
13197 }
13198 return new QueryImpl(query._repo, query._path, newParams, query._orderByCalled);
13199 }
13200}
13201/**
13202 * Creates a `QueryConstraint` with the specified starting point (exclusive).
13203 *
13204 * Using `startAt()`, `startAfter()`, `endBefore()`, `endAt()` and `equalTo()`
13205 * allows you to choose arbitrary starting and ending points for your queries.
13206 *
13207 * The starting point is exclusive. If only a value is provided, children
13208 * with a value greater than the specified value will be included in the query.
13209 * If a key is specified, then children must have a value greater than or equal
13210 * to the specified value and a a key name greater than the specified key.
13211 *
13212 * @param value - The value to start after. The argument type depends on which
13213 * `orderBy*()` function was used in this query. Specify a value that matches
13214 * the `orderBy*()` type. When used in combination with `orderByKey()`, the
13215 * value must be a string.
13216 * @param key - The child key to start after. This argument is only allowed if
13217 * ordering by child, value, or priority.
13218 */
13219function startAfter(value, key) {
13220 validateKey('startAfter', 'key', key, true);
13221 return new QueryStartAfterConstraint(value, key);
13222}
13223class QueryLimitToFirstConstraint extends QueryConstraint {
13224 constructor(_limit) {
13225 super();
13226 this._limit = _limit;
13227 }
13228 _apply(query) {
13229 if (query._queryParams.hasLimit()) {
13230 throw new Error('limitToFirst: Limit was already set (by another call to limitToFirst ' +
13231 'or limitToLast).');
13232 }
13233 return new QueryImpl(query._repo, query._path, queryParamsLimitToFirst(query._queryParams, this._limit), query._orderByCalled);
13234 }
13235}
13236/**
13237 * Creates a new `QueryConstraint` that if limited to the first specific number
13238 * of children.
13239 *
13240 * The `limitToFirst()` method is used to set a maximum number of children to be
13241 * synced for a given callback. If we set a limit of 100, we will initially only
13242 * receive up to 100 `child_added` events. If we have fewer than 100 messages
13243 * stored in our Database, a `child_added` event will fire for each message.
13244 * However, if we have over 100 messages, we will only receive a `child_added`
13245 * event for the first 100 ordered messages. As items change, we will receive
13246 * `child_removed` events for each item that drops out of the active list so
13247 * that the total number stays at 100.
13248 *
13249 * You can read more about `limitToFirst()` in
13250 * {@link https://firebase.google.com/docs/database/web/lists-of-data#filtering_data | Filtering data}.
13251 *
13252 * @param limit - The maximum number of nodes to include in this query.
13253 */
13254function limitToFirst(limit) {
13255 if (typeof limit !== 'number' || Math.floor(limit) !== limit || limit <= 0) {
13256 throw new Error('limitToFirst: First argument must be a positive integer.');
13257 }
13258 return new QueryLimitToFirstConstraint(limit);
13259}
13260class QueryLimitToLastConstraint extends QueryConstraint {
13261 constructor(_limit) {
13262 super();
13263 this._limit = _limit;
13264 }
13265 _apply(query) {
13266 if (query._queryParams.hasLimit()) {
13267 throw new Error('limitToLast: Limit was already set (by another call to limitToFirst ' +
13268 'or limitToLast).');
13269 }
13270 return new QueryImpl(query._repo, query._path, queryParamsLimitToLast(query._queryParams, this._limit), query._orderByCalled);
13271 }
13272}
13273/**
13274 * Creates a new `QueryConstraint` that is limited to return only the last
13275 * specified number of children.
13276 *
13277 * The `limitToLast()` method is used to set a maximum number of children to be
13278 * synced for a given callback. If we set a limit of 100, we will initially only
13279 * receive up to 100 `child_added` events. If we have fewer than 100 messages
13280 * stored in our Database, a `child_added` event will fire for each message.
13281 * However, if we have over 100 messages, we will only receive a `child_added`
13282 * event for the last 100 ordered messages. As items change, we will receive
13283 * `child_removed` events for each item that drops out of the active list so
13284 * that the total number stays at 100.
13285 *
13286 * You can read more about `limitToLast()` in
13287 * {@link https://firebase.google.com/docs/database/web/lists-of-data#filtering_data | Filtering data}.
13288 *
13289 * @param limit - The maximum number of nodes to include in this query.
13290 */
13291function limitToLast(limit) {
13292 if (typeof limit !== 'number' || Math.floor(limit) !== limit || limit <= 0) {
13293 throw new Error('limitToLast: First argument must be a positive integer.');
13294 }
13295 return new QueryLimitToLastConstraint(limit);
13296}
13297class QueryOrderByChildConstraint extends QueryConstraint {
13298 constructor(_path) {
13299 super();
13300 this._path = _path;
13301 }
13302 _apply(query) {
13303 validateNoPreviousOrderByCall(query, 'orderByChild');
13304 const parsedPath = new Path(this._path);
13305 if (pathIsEmpty(parsedPath)) {
13306 throw new Error('orderByChild: cannot pass in empty path. Use orderByValue() instead.');
13307 }
13308 const index = new PathIndex(parsedPath);
13309 const newParams = queryParamsOrderBy(query._queryParams, index);
13310 validateQueryEndpoints(newParams);
13311 return new QueryImpl(query._repo, query._path, newParams,
13312 /*orderByCalled=*/ true);
13313 }
13314}
13315/**
13316 * Creates a new `QueryConstraint` that orders by the specified child key.
13317 *
13318 * Queries can only order by one key at a time. Calling `orderByChild()`
13319 * multiple times on the same query is an error.
13320 *
13321 * Firebase queries allow you to order your data by any child key on the fly.
13322 * However, if you know in advance what your indexes will be, you can define
13323 * them via the .indexOn rule in your Security Rules for better performance. See
13324 * the{@link https://firebase.google.com/docs/database/security/indexing-data}
13325 * rule for more information.
13326 *
13327 * You can read more about `orderByChild()` in
13328 * {@link https://firebase.google.com/docs/database/web/lists-of-data#sort_data | Sort data}.
13329 *
13330 * @param path - The path to order by.
13331 */
13332function orderByChild(path) {
13333 if (path === '$key') {
13334 throw new Error('orderByChild: "$key" is invalid. Use orderByKey() instead.');
13335 }
13336 else if (path === '$priority') {
13337 throw new Error('orderByChild: "$priority" is invalid. Use orderByPriority() instead.');
13338 }
13339 else if (path === '$value') {
13340 throw new Error('orderByChild: "$value" is invalid. Use orderByValue() instead.');
13341 }
13342 validatePathString('orderByChild', 'path', path, false);
13343 return new QueryOrderByChildConstraint(path);
13344}
13345class QueryOrderByKeyConstraint extends QueryConstraint {
13346 _apply(query) {
13347 validateNoPreviousOrderByCall(query, 'orderByKey');
13348 const newParams = queryParamsOrderBy(query._queryParams, KEY_INDEX);
13349 validateQueryEndpoints(newParams);
13350 return new QueryImpl(query._repo, query._path, newParams,
13351 /*orderByCalled=*/ true);
13352 }
13353}
13354/**
13355 * Creates a new `QueryConstraint` that orders by the key.
13356 *
13357 * Sorts the results of a query by their (ascending) key values.
13358 *
13359 * You can read more about `orderByKey()` in
13360 * {@link https://firebase.google.com/docs/database/web/lists-of-data#sort_data | Sort data}.
13361 */
13362function orderByKey() {
13363 return new QueryOrderByKeyConstraint();
13364}
13365class QueryOrderByPriorityConstraint extends QueryConstraint {
13366 _apply(query) {
13367 validateNoPreviousOrderByCall(query, 'orderByPriority');
13368 const newParams = queryParamsOrderBy(query._queryParams, PRIORITY_INDEX);
13369 validateQueryEndpoints(newParams);
13370 return new QueryImpl(query._repo, query._path, newParams,
13371 /*orderByCalled=*/ true);
13372 }
13373}
13374/**
13375 * Creates a new `QueryConstraint` that orders by priority.
13376 *
13377 * Applications need not use priority but can order collections by
13378 * ordinary properties (see
13379 * {@link https://firebase.google.com/docs/database/web/lists-of-data#sort_data | Sort data}
13380 * for alternatives to priority.
13381 */
13382function orderByPriority() {
13383 return new QueryOrderByPriorityConstraint();
13384}
13385class QueryOrderByValueConstraint extends QueryConstraint {
13386 _apply(query) {
13387 validateNoPreviousOrderByCall(query, 'orderByValue');
13388 const newParams = queryParamsOrderBy(query._queryParams, VALUE_INDEX);
13389 validateQueryEndpoints(newParams);
13390 return new QueryImpl(query._repo, query._path, newParams,
13391 /*orderByCalled=*/ true);
13392 }
13393}
13394/**
13395 * Creates a new `QueryConstraint` that orders by value.
13396 *
13397 * If the children of a query are all scalar values (string, number, or
13398 * boolean), you can order the results by their (ascending) values.
13399 *
13400 * You can read more about `orderByValue()` in
13401 * {@link https://firebase.google.com/docs/database/web/lists-of-data#sort_data | Sort data}.
13402 */
13403function orderByValue() {
13404 return new QueryOrderByValueConstraint();
13405}
13406class QueryEqualToValueConstraint extends QueryConstraint {
13407 constructor(_value, _key) {
13408 super();
13409 this._value = _value;
13410 this._key = _key;
13411 }
13412 _apply(query) {
13413 validateFirebaseDataArg('equalTo', this._value, query._path, false);
13414 if (query._queryParams.hasStart()) {
13415 throw new Error('equalTo: Starting point was already set (by another call to startAt/startAfter or ' +
13416 'equalTo).');
13417 }
13418 if (query._queryParams.hasEnd()) {
13419 throw new Error('equalTo: Ending point was already set (by another call to endAt/endBefore or ' +
13420 'equalTo).');
13421 }
13422 return new QueryEndAtConstraint(this._value, this._key)._apply(new QueryStartAtConstraint(this._value, this._key)._apply(query));
13423 }
13424}
13425/**
13426 * Creates a `QueryConstraint` that includes children that match the specified
13427 * value.
13428 *
13429 * Using `startAt()`, `startAfter()`, `endBefore()`, `endAt()` and `equalTo()`
13430 * allows you to choose arbitrary starting and ending points for your queries.
13431 *
13432 * The optional key argument can be used to further limit the range of the
13433 * query. If it is specified, then children that have exactly the specified
13434 * value must also have exactly the specified key as their key name. This can be
13435 * used to filter result sets with many matches for the same value.
13436 *
13437 * You can read more about `equalTo()` in
13438 * {@link https://firebase.google.com/docs/database/web/lists-of-data#filtering_data | Filtering data}.
13439 *
13440 * @param value - The value to match for. The argument type depends on which
13441 * `orderBy*()` function was used in this query. Specify a value that matches
13442 * the `orderBy*()` type. When used in combination with `orderByKey()`, the
13443 * value must be a string.
13444 * @param key - The child key to start at, among the children with the
13445 * previously specified priority. This argument is only allowed if ordering by
13446 * child, value, or priority.
13447 */
13448function equalTo(value, key) {
13449 validateKey('equalTo', 'key', key, true);
13450 return new QueryEqualToValueConstraint(value, key);
13451}
13452/**
13453 * Creates a new immutable instance of `Query` that is extended to also include
13454 * additional query constraints.
13455 *
13456 * @param query - The Query instance to use as a base for the new constraints.
13457 * @param queryConstraints - The list of `QueryConstraint`s to apply.
13458 * @throws if any of the provided query constraints cannot be combined with the
13459 * existing or new constraints.
13460 */
13461function query(query, ...queryConstraints) {
13462 let queryImpl = getModularInstance(query);
13463 for (const constraint of queryConstraints) {
13464 queryImpl = constraint._apply(queryImpl);
13465 }
13466 return queryImpl;
13467}
13468/**
13469 * Define reference constructor in various modules
13470 *
13471 * We are doing this here to avoid several circular
13472 * dependency issues
13473 */
13474syncPointSetReferenceConstructor(ReferenceImpl);
13475syncTreeSetReferenceConstructor(ReferenceImpl);
13476
13477/**
13478 * @license
13479 * Copyright 2020 Google LLC
13480 *
13481 * Licensed under the Apache License, Version 2.0 (the "License");
13482 * you may not use this file except in compliance with the License.
13483 * You may obtain a copy of the License at
13484 *
13485 * http://www.apache.org/licenses/LICENSE-2.0
13486 *
13487 * Unless required by applicable law or agreed to in writing, software
13488 * distributed under the License is distributed on an "AS IS" BASIS,
13489 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13490 * See the License for the specific language governing permissions and
13491 * limitations under the License.
13492 */
13493/**
13494 * This variable is also defined in the firebase Node.js Admin SDK. Before
13495 * modifying this definition, consult the definition in:
13496 *
13497 * https://github.com/firebase/firebase-admin-node
13498 *
13499 * and make sure the two are consistent.
13500 */
13501const FIREBASE_DATABASE_EMULATOR_HOST_VAR = 'FIREBASE_DATABASE_EMULATOR_HOST';
13502/**
13503 * Creates and caches `Repo` instances.
13504 */
13505const repos = {};
13506/**
13507 * If true, any new `Repo` will be created to use `ReadonlyRestClient` (for testing purposes).
13508 */
13509let useRestClient = false;
13510/**
13511 * Update an existing `Repo` in place to point to a new host/port.
13512 */
13513function repoManagerApplyEmulatorSettings(repo, host, port, tokenProvider) {
13514 repo.repoInfo_ = new RepoInfo(`${host}:${port}`,
13515 /* secure= */ false, repo.repoInfo_.namespace, repo.repoInfo_.webSocketOnly, repo.repoInfo_.nodeAdmin, repo.repoInfo_.persistenceKey, repo.repoInfo_.includeNamespaceInQueryParams);
13516 if (tokenProvider) {
13517 repo.authTokenProvider_ = tokenProvider;
13518 }
13519}
13520/**
13521 * This function should only ever be called to CREATE a new database instance.
13522 * @internal
13523 */
13524function repoManagerDatabaseFromApp(app, authProvider, appCheckProvider, url, nodeAdmin) {
13525 let dbUrl = url || app.options.databaseURL;
13526 if (dbUrl === undefined) {
13527 if (!app.options.projectId) {
13528 fatal("Can't determine Firebase Database URL. Be sure to include " +
13529 ' a Project ID when calling firebase.initializeApp().');
13530 }
13531 log('Using default host for project ', app.options.projectId);
13532 dbUrl = `${app.options.projectId}-default-rtdb.firebaseio.com`;
13533 }
13534 let parsedUrl = parseRepoInfo(dbUrl, nodeAdmin);
13535 let repoInfo = parsedUrl.repoInfo;
13536 let isEmulator;
13537 let dbEmulatorHost = undefined;
13538 if (typeof process !== 'undefined') {
13539 dbEmulatorHost = process.env[FIREBASE_DATABASE_EMULATOR_HOST_VAR];
13540 }
13541 if (dbEmulatorHost) {
13542 isEmulator = true;
13543 dbUrl = `http://${dbEmulatorHost}?ns=${repoInfo.namespace}`;
13544 parsedUrl = parseRepoInfo(dbUrl, nodeAdmin);
13545 repoInfo = parsedUrl.repoInfo;
13546 }
13547 else {
13548 isEmulator = !parsedUrl.repoInfo.secure;
13549 }
13550 const authTokenProvider = nodeAdmin && isEmulator
13551 ? new EmulatorTokenProvider(EmulatorTokenProvider.OWNER)
13552 : new FirebaseAuthTokenProvider(app.name, app.options, authProvider);
13553 validateUrl('Invalid Firebase Database URL', parsedUrl);
13554 if (!pathIsEmpty(parsedUrl.path)) {
13555 fatal('Database URL must point to the root of a Firebase Database ' +
13556 '(not including a child path).');
13557 }
13558 const repo = repoManagerCreateRepo(repoInfo, app, authTokenProvider, new AppCheckTokenProvider(app.name, appCheckProvider));
13559 return new Database(repo, app);
13560}
13561/**
13562 * Remove the repo and make sure it is disconnected.
13563 *
13564 */
13565function repoManagerDeleteRepo(repo, appName) {
13566 const appRepos = repos[appName];
13567 // This should never happen...
13568 if (!appRepos || appRepos[repo.key] !== repo) {
13569 fatal(`Database ${appName}(${repo.repoInfo_}) has already been deleted.`);
13570 }
13571 repoInterrupt(repo);
13572 delete appRepos[repo.key];
13573}
13574/**
13575 * Ensures a repo doesn't already exist and then creates one using the
13576 * provided app.
13577 *
13578 * @param repoInfo - The metadata about the Repo
13579 * @returns The Repo object for the specified server / repoName.
13580 */
13581function repoManagerCreateRepo(repoInfo, app, authTokenProvider, appCheckProvider) {
13582 let appRepos = repos[app.name];
13583 if (!appRepos) {
13584 appRepos = {};
13585 repos[app.name] = appRepos;
13586 }
13587 let repo = appRepos[repoInfo.toURLString()];
13588 if (repo) {
13589 fatal('Database initialized multiple times. Please make sure the format of the database URL matches with each database() call.');
13590 }
13591 repo = new Repo(repoInfo, useRestClient, authTokenProvider, appCheckProvider);
13592 appRepos[repoInfo.toURLString()] = repo;
13593 return repo;
13594}
13595/**
13596 * Forces us to use ReadonlyRestClient instead of PersistentConnection for new Repos.
13597 */
13598function repoManagerForceRestClient(forceRestClient) {
13599 useRestClient = forceRestClient;
13600}
13601/**
13602 * Class representing a Firebase Realtime Database.
13603 */
13604class Database {
13605 /** @hideconstructor */
13606 constructor(_repoInternal,
13607 /** The {@link @firebase/app#FirebaseApp} associated with this Realtime Database instance. */
13608 app) {
13609 this._repoInternal = _repoInternal;
13610 this.app = app;
13611 /** Represents a `Database` instance. */
13612 this['type'] = 'database';
13613 /** Track if the instance has been used (root or repo accessed) */
13614 this._instanceStarted = false;
13615 }
13616 get _repo() {
13617 if (!this._instanceStarted) {
13618 repoStart(this._repoInternal, this.app.options.appId, this.app.options['databaseAuthVariableOverride']);
13619 this._instanceStarted = true;
13620 }
13621 return this._repoInternal;
13622 }
13623 get _root() {
13624 if (!this._rootInternal) {
13625 this._rootInternal = new ReferenceImpl(this._repo, newEmptyPath());
13626 }
13627 return this._rootInternal;
13628 }
13629 _delete() {
13630 if (this._rootInternal !== null) {
13631 repoManagerDeleteRepo(this._repo, this.app.name);
13632 this._repoInternal = null;
13633 this._rootInternal = null;
13634 }
13635 return Promise.resolve();
13636 }
13637 _checkNotDeleted(apiName) {
13638 if (this._rootInternal === null) {
13639 fatal('Cannot call ' + apiName + ' on a deleted database.');
13640 }
13641 }
13642}
13643/**
13644 * Returns the instance of the Realtime Database SDK that is associated
13645 * with the provided {@link @firebase/app#FirebaseApp}. Initializes a new instance with
13646 * with default settings if no instance exists or if the existing instance uses
13647 * a custom database URL.
13648 *
13649 * @param app - The {@link @firebase/app#FirebaseApp} instance that the returned Realtime
13650 * Database instance is associated with.
13651 * @param url - The URL of the Realtime Database instance to connect to. If not
13652 * provided, the SDK connects to the default instance of the Firebase App.
13653 * @returns The `Database` instance of the provided app.
13654 */
13655function getDatabase(app = getApp(), url) {
13656 return _getProvider(app, 'database').getImmediate({
13657 identifier: url
13658 });
13659}
13660/**
13661 * Modify the provided instance to communicate with the Realtime Database
13662 * emulator.
13663 *
13664 * <p>Note: This method must be called before performing any other operation.
13665 *
13666 * @param db - The instance to modify.
13667 * @param host - The emulator host (ex: localhost)
13668 * @param port - The emulator port (ex: 8080)
13669 * @param options.mockUserToken - the mock auth token to use for unit testing Security Rules
13670 */
13671function connectDatabaseEmulator(db, host, port, options = {}) {
13672 db = getModularInstance(db);
13673 db._checkNotDeleted('useEmulator');
13674 if (db._instanceStarted) {
13675 fatal('Cannot call useEmulator() after instance has already been initialized.');
13676 }
13677 const repo = db._repoInternal;
13678 let tokenProvider = undefined;
13679 if (repo.repoInfo_.nodeAdmin) {
13680 if (options.mockUserToken) {
13681 fatal('mockUserToken is not supported by the Admin SDK. For client access with mock users, please use the "firebase" package instead of "firebase-admin".');
13682 }
13683 tokenProvider = new EmulatorTokenProvider(EmulatorTokenProvider.OWNER);
13684 }
13685 else if (options.mockUserToken) {
13686 const token = typeof options.mockUserToken === 'string'
13687 ? options.mockUserToken
13688 : createMockUserToken(options.mockUserToken, db.app.options.projectId);
13689 tokenProvider = new EmulatorTokenProvider(token);
13690 }
13691 // Modify the repo to apply emulator settings
13692 repoManagerApplyEmulatorSettings(repo, host, port, tokenProvider);
13693}
13694/**
13695 * Disconnects from the server (all Database operations will be completed
13696 * offline).
13697 *
13698 * The client automatically maintains a persistent connection to the Database
13699 * server, which will remain active indefinitely and reconnect when
13700 * disconnected. However, the `goOffline()` and `goOnline()` methods may be used
13701 * to control the client connection in cases where a persistent connection is
13702 * undesirable.
13703 *
13704 * While offline, the client will no longer receive data updates from the
13705 * Database. However, all Database operations performed locally will continue to
13706 * immediately fire events, allowing your application to continue behaving
13707 * normally. Additionally, each operation performed locally will automatically
13708 * be queued and retried upon reconnection to the Database server.
13709 *
13710 * To reconnect to the Database and begin receiving remote events, see
13711 * `goOnline()`.
13712 *
13713 * @param db - The instance to disconnect.
13714 */
13715function goOffline(db) {
13716 db = getModularInstance(db);
13717 db._checkNotDeleted('goOffline');
13718 repoInterrupt(db._repo);
13719}
13720/**
13721 * Reconnects to the server and synchronizes the offline Database state
13722 * with the server state.
13723 *
13724 * This method should be used after disabling the active connection with
13725 * `goOffline()`. Once reconnected, the client will transmit the proper data
13726 * and fire the appropriate events so that your client "catches up"
13727 * automatically.
13728 *
13729 * @param db - The instance to reconnect.
13730 */
13731function goOnline(db) {
13732 db = getModularInstance(db);
13733 db._checkNotDeleted('goOnline');
13734 repoResume(db._repo);
13735}
13736function enableLogging(logger, persistent) {
13737 enableLogging$1(logger, persistent);
13738}
13739
13740/**
13741 * @license
13742 * Copyright 2021 Google LLC
13743 *
13744 * Licensed under the Apache License, Version 2.0 (the "License");
13745 * you may not use this file except in compliance with the License.
13746 * You may obtain a copy of the License at
13747 *
13748 * http://www.apache.org/licenses/LICENSE-2.0
13749 *
13750 * Unless required by applicable law or agreed to in writing, software
13751 * distributed under the License is distributed on an "AS IS" BASIS,
13752 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13753 * See the License for the specific language governing permissions and
13754 * limitations under the License.
13755 */
13756function registerDatabase(variant) {
13757 setSDKVersion(SDK_VERSION$1);
13758 _registerComponent(new Component('database', (container, { instanceIdentifier: url }) => {
13759 const app = container.getProvider('app').getImmediate();
13760 const authProvider = container.getProvider('auth-internal');
13761 const appCheckProvider = container.getProvider('app-check-internal');
13762 return repoManagerDatabaseFromApp(app, authProvider, appCheckProvider, url);
13763 }, "PUBLIC" /* PUBLIC */).setMultipleInstances(true));
13764 registerVersion(name, version, variant);
13765}
13766
13767/**
13768 * @license
13769 * Copyright 2020 Google LLC
13770 *
13771 * Licensed under the Apache License, Version 2.0 (the "License");
13772 * you may not use this file except in compliance with the License.
13773 * You may obtain a copy of the License at
13774 *
13775 * http://www.apache.org/licenses/LICENSE-2.0
13776 *
13777 * Unless required by applicable law or agreed to in writing, software
13778 * distributed under the License is distributed on an "AS IS" BASIS,
13779 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13780 * See the License for the specific language governing permissions and
13781 * limitations under the License.
13782 */
13783const SERVER_TIMESTAMP = {
13784 '.sv': 'timestamp'
13785};
13786/**
13787 * Returns a placeholder value for auto-populating the current timestamp (time
13788 * since the Unix epoch, in milliseconds) as determined by the Firebase
13789 * servers.
13790 */
13791function serverTimestamp() {
13792 return SERVER_TIMESTAMP;
13793}
13794/**
13795 * Returns a placeholder value that can be used to atomically increment the
13796 * current database value by the provided delta.
13797 *
13798 * @param delta - the amount to modify the current value atomically.
13799 * @returns A placeholder value for modifying data atomically server-side.
13800 */
13801function increment(delta) {
13802 return {
13803 '.sv': {
13804 'increment': delta
13805 }
13806 };
13807}
13808
13809/**
13810 * @license
13811 * Copyright 2020 Google LLC
13812 *
13813 * Licensed under the Apache License, Version 2.0 (the "License");
13814 * you may not use this file except in compliance with the License.
13815 * You may obtain a copy of the License at
13816 *
13817 * http://www.apache.org/licenses/LICENSE-2.0
13818 *
13819 * Unless required by applicable law or agreed to in writing, software
13820 * distributed under the License is distributed on an "AS IS" BASIS,
13821 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13822 * See the License for the specific language governing permissions and
13823 * limitations under the License.
13824 */
13825/**
13826 * A type for the resolve value of {@link runTransaction}.
13827 */
13828class TransactionResult {
13829 /** @hideconstructor */
13830 constructor(
13831 /** Whether the transaction was successfully committed. */
13832 committed,
13833 /** The resulting data snapshot. */
13834 snapshot) {
13835 this.committed = committed;
13836 this.snapshot = snapshot;
13837 }
13838 /** Returns a JSON-serializable representation of this object. */
13839 toJSON() {
13840 return { committed: this.committed, snapshot: this.snapshot.toJSON() };
13841 }
13842}
13843/**
13844 * Atomically modifies the data at this location.
13845 *
13846 * Atomically modify the data at this location. Unlike a normal `set()`, which
13847 * just overwrites the data regardless of its previous value, `runTransaction()` is
13848 * used to modify the existing value to a new value, ensuring there are no
13849 * conflicts with other clients writing to the same location at the same time.
13850 *
13851 * To accomplish this, you pass `runTransaction()` an update function which is
13852 * used to transform the current value into a new value. If another client
13853 * writes to the location before your new value is successfully written, your
13854 * update function will be called again with the new current value, and the
13855 * write will be retried. This will happen repeatedly until your write succeeds
13856 * without conflict or you abort the transaction by not returning a value from
13857 * your update function.
13858 *
13859 * Note: Modifying data with `set()` will cancel any pending transactions at
13860 * that location, so extreme care should be taken if mixing `set()` and
13861 * `runTransaction()` to update the same data.
13862 *
13863 * Note: When using transactions with Security and Firebase Rules in place, be
13864 * aware that a client needs `.read` access in addition to `.write` access in
13865 * order to perform a transaction. This is because the client-side nature of
13866 * transactions requires the client to read the data in order to transactionally
13867 * update it.
13868 *
13869 * @param ref - The location to atomically modify.
13870 * @param transactionUpdate - A developer-supplied function which will be passed
13871 * the current data stored at this location (as a JavaScript object). The
13872 * function should return the new value it would like written (as a JavaScript
13873 * object). If `undefined` is returned (i.e. you return with no arguments) the
13874 * transaction will be aborted and the data at this location will not be
13875 * modified.
13876 * @param options - An options object to configure transactions.
13877 * @returns A `Promise` that can optionally be used instead of the `onComplete`
13878 * callback to handle success and failure.
13879 */
13880function runTransaction(ref,
13881// eslint-disable-next-line @typescript-eslint/no-explicit-any
13882transactionUpdate, options) {
13883 var _a;
13884 ref = getModularInstance(ref);
13885 validateWritablePath('Reference.transaction', ref._path);
13886 if (ref.key === '.length' || ref.key === '.keys') {
13887 throw ('Reference.transaction failed: ' + ref.key + ' is a read-only object.');
13888 }
13889 const applyLocally = (_a = options === null || options === void 0 ? void 0 : options.applyLocally) !== null && _a !== void 0 ? _a : true;
13890 const deferred = new Deferred();
13891 const promiseComplete = (error, committed, node) => {
13892 let dataSnapshot = null;
13893 if (error) {
13894 deferred.reject(error);
13895 }
13896 else {
13897 dataSnapshot = new DataSnapshot(node, new ReferenceImpl(ref._repo, ref._path), PRIORITY_INDEX);
13898 deferred.resolve(new TransactionResult(committed, dataSnapshot));
13899 }
13900 };
13901 // Add a watch to make sure we get server updates.
13902 const unwatcher = onValue(ref, () => { });
13903 repoStartTransaction(ref._repo, ref._path, transactionUpdate, promiseComplete, unwatcher, applyLocally);
13904 return deferred.promise;
13905}
13906
13907/**
13908 * @license
13909 * Copyright 2017 Google LLC
13910 *
13911 * Licensed under the Apache License, Version 2.0 (the "License");
13912 * you may not use this file except in compliance with the License.
13913 * You may obtain a copy of the License at
13914 *
13915 * http://www.apache.org/licenses/LICENSE-2.0
13916 *
13917 * Unless required by applicable law or agreed to in writing, software
13918 * distributed under the License is distributed on an "AS IS" BASIS,
13919 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13920 * See the License for the specific language governing permissions and
13921 * limitations under the License.
13922 */
13923// eslint-disable-next-line @typescript-eslint/no-explicit-any
13924PersistentConnection.prototype.simpleListen = function (pathString, onComplete) {
13925 this.sendRequest('q', { p: pathString }, onComplete);
13926};
13927// eslint-disable-next-line @typescript-eslint/no-explicit-any
13928PersistentConnection.prototype.echo = function (data, onEcho) {
13929 this.sendRequest('echo', { d: data }, onEcho);
13930};
13931/**
13932 * @internal
13933 */
13934const hijackHash = function (newHash) {
13935 const oldPut = PersistentConnection.prototype.put;
13936 PersistentConnection.prototype.put = function (pathString, data, onComplete, hash) {
13937 if (hash !== undefined) {
13938 hash = newHash();
13939 }
13940 oldPut.call(this, pathString, data, onComplete, hash);
13941 };
13942 return function () {
13943 PersistentConnection.prototype.put = oldPut;
13944 };
13945};
13946/**
13947 * Forces the RepoManager to create Repos that use ReadonlyRestClient instead of PersistentConnection.
13948 * @internal
13949 */
13950const forceRestClient = function (forceRestClient) {
13951 repoManagerForceRestClient(forceRestClient);
13952};
13953
13954/**
13955 * Firebase Realtime Database
13956 *
13957 * @packageDocumentation
13958 */
13959registerDatabase();
13960
13961export { 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, 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 };
13962//# sourceMappingURL=index.esm2017.js.map