UNPKG

42.2 kBJavaScriptView Raw
1import firebase from '@firebase/app';
2import '@firebase/installations';
3import { ErrorFactory, FirebaseError, calculateBackoffMillis } from '@firebase/util';
4import { LogLevel, Logger } from '@firebase/logger';
5import { Component } from '@firebase/component';
6
7/**
8 * @license
9 * Copyright 2019 Google LLC
10 *
11 * Licensed under the Apache License, Version 2.0 (the "License");
12 * you may not use this file except in compliance with the License.
13 * You may obtain a copy of the License at
14 *
15 * http://www.apache.org/licenses/LICENSE-2.0
16 *
17 * Unless required by applicable law or agreed to in writing, software
18 * distributed under the License is distributed on an "AS IS" BASIS,
19 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20 * See the License for the specific language governing permissions and
21 * limitations under the License.
22 */
23/**
24 * Implements the {@link RemoteConfigClient} abstraction with success response caching.
25 *
26 * <p>Comparable to the browser's Cache API for responses, but the Cache API requires a Service
27 * Worker, which requires HTTPS, which would significantly complicate SDK installation. Also, the
28 * Cache API doesn't support matching entries by time.
29 */
30class CachingClient {
31 constructor(client, storage, storageCache, logger) {
32 this.client = client;
33 this.storage = storage;
34 this.storageCache = storageCache;
35 this.logger = logger;
36 }
37 /**
38 * Returns true if the age of the cached fetched configs is less than or equal to
39 * {@link Settings#minimumFetchIntervalInSeconds}.
40 *
41 * <p>This is comparable to passing `headers = { 'Cache-Control': max-age <maxAge> }` to the
42 * native Fetch API.
43 *
44 * <p>Visible for testing.
45 */
46 isCachedDataFresh(cacheMaxAgeMillis, lastSuccessfulFetchTimestampMillis) {
47 // Cache can only be fresh if it's populated.
48 if (!lastSuccessfulFetchTimestampMillis) {
49 this.logger.debug('Config fetch cache check. Cache unpopulated.');
50 return false;
51 }
52 // Calculates age of cache entry.
53 const cacheAgeMillis = Date.now() - lastSuccessfulFetchTimestampMillis;
54 const isCachedDataFresh = cacheAgeMillis <= cacheMaxAgeMillis;
55 this.logger.debug('Config fetch cache check.' +
56 ` Cache age millis: ${cacheAgeMillis}.` +
57 ` Cache max age millis (minimumFetchIntervalMillis setting): ${cacheMaxAgeMillis}.` +
58 ` Is cache hit: ${isCachedDataFresh}.`);
59 return isCachedDataFresh;
60 }
61 async fetch(request) {
62 // Reads from persisted storage to avoid cache miss if callers don't wait on initialization.
63 const [lastSuccessfulFetchTimestampMillis, lastSuccessfulFetchResponse] = await Promise.all([
64 this.storage.getLastSuccessfulFetchTimestampMillis(),
65 this.storage.getLastSuccessfulFetchResponse()
66 ]);
67 // Exits early on cache hit.
68 if (lastSuccessfulFetchResponse &&
69 this.isCachedDataFresh(request.cacheMaxAgeMillis, lastSuccessfulFetchTimestampMillis)) {
70 return lastSuccessfulFetchResponse;
71 }
72 // Deviates from pure decorator by not honoring a passed ETag since we don't have a public API
73 // that allows the caller to pass an ETag.
74 request.eTag =
75 lastSuccessfulFetchResponse && lastSuccessfulFetchResponse.eTag;
76 // Falls back to service on cache miss.
77 const response = await this.client.fetch(request);
78 // Fetch throws for non-success responses, so success is guaranteed here.
79 const storageOperations = [
80 // Uses write-through cache for consistency with synchronous public API.
81 this.storageCache.setLastSuccessfulFetchTimestampMillis(Date.now())
82 ];
83 if (response.status === 200) {
84 // Caches response only if it has changed, ie non-304 responses.
85 storageOperations.push(this.storage.setLastSuccessfulFetchResponse(response));
86 }
87 await Promise.all(storageOperations);
88 return response;
89 }
90}
91
92/**
93 * @license
94 * Copyright 2019 Google LLC
95 *
96 * Licensed under the Apache License, Version 2.0 (the "License");
97 * you may not use this file except in compliance with the License.
98 * You may obtain a copy of the License at
99 *
100 * http://www.apache.org/licenses/LICENSE-2.0
101 *
102 * Unless required by applicable law or agreed to in writing, software
103 * distributed under the License is distributed on an "AS IS" BASIS,
104 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
105 * See the License for the specific language governing permissions and
106 * limitations under the License.
107 */
108const ERROR_DESCRIPTION_MAP = {
109 ["registration-window" /* REGISTRATION_WINDOW */]: 'Undefined window object. This SDK only supports usage in a browser environment.',
110 ["registration-project-id" /* REGISTRATION_PROJECT_ID */]: 'Undefined project identifier. Check Firebase app initialization.',
111 ["registration-api-key" /* REGISTRATION_API_KEY */]: 'Undefined API key. Check Firebase app initialization.',
112 ["registration-app-id" /* REGISTRATION_APP_ID */]: 'Undefined app identifier. Check Firebase app initialization.',
113 ["storage-open" /* STORAGE_OPEN */]: 'Error thrown when opening storage. Original error: {$originalErrorMessage}.',
114 ["storage-get" /* STORAGE_GET */]: 'Error thrown when reading from storage. Original error: {$originalErrorMessage}.',
115 ["storage-set" /* STORAGE_SET */]: 'Error thrown when writing to storage. Original error: {$originalErrorMessage}.',
116 ["storage-delete" /* STORAGE_DELETE */]: 'Error thrown when deleting from storage. Original error: {$originalErrorMessage}.',
117 ["fetch-client-network" /* FETCH_NETWORK */]: 'Fetch client failed to connect to a network. Check Internet connection.' +
118 ' Original error: {$originalErrorMessage}.',
119 ["fetch-timeout" /* FETCH_TIMEOUT */]: 'The config fetch request timed out. ' +
120 ' Configure timeout using "fetchTimeoutMillis" SDK setting.',
121 ["fetch-throttle" /* FETCH_THROTTLE */]: 'The config fetch request timed out while in an exponential backoff state.' +
122 ' Configure timeout using "fetchTimeoutMillis" SDK setting.' +
123 ' Unix timestamp in milliseconds when fetch request throttling ends: {$throttleEndTimeMillis}.',
124 ["fetch-client-parse" /* FETCH_PARSE */]: 'Fetch client could not parse response.' +
125 ' Original error: {$originalErrorMessage}.',
126 ["fetch-status" /* FETCH_STATUS */]: 'Fetch server returned an HTTP error status. HTTP status: {$httpStatus}.'
127};
128const ERROR_FACTORY = new ErrorFactory('remoteconfig' /* service */, 'Remote Config' /* service name */, ERROR_DESCRIPTION_MAP);
129// Note how this is like typeof/instanceof, but for ErrorCode.
130function hasErrorCode(e, errorCode) {
131 return e instanceof FirebaseError && e.code.indexOf(errorCode) !== -1;
132}
133
134/**
135 * @license
136 * Copyright 2019 Google LLC
137 *
138 * Licensed under the Apache License, Version 2.0 (the "License");
139 * you may not use this file except in compliance with the License.
140 * You may obtain a copy of the License at
141 *
142 * http://www.apache.org/licenses/LICENSE-2.0
143 *
144 * Unless required by applicable law or agreed to in writing, software
145 * distributed under the License is distributed on an "AS IS" BASIS,
146 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
147 * See the License for the specific language governing permissions and
148 * limitations under the License.
149 */
150/**
151 * Attempts to get the most accurate browser language setting.
152 *
153 * <p>Adapted from getUserLanguage in packages/auth/src/utils.js for TypeScript.
154 *
155 * <p>Defers default language specification to server logic for consistency.
156 *
157 * @param navigatorLanguage Enables tests to override read-only {@link NavigatorLanguage}.
158 */
159function getUserLanguage(navigatorLanguage = navigator) {
160 return (
161 // Most reliable, but only supported in Chrome/Firefox.
162 (navigatorLanguage.languages && navigatorLanguage.languages[0]) ||
163 // Supported in most browsers, but returns the language of the browser
164 // UI, not the language set in browser settings.
165 navigatorLanguage.language
166 // Polyfill otherwise.
167 );
168}
169
170/**
171 * @license
172 * Copyright 2019 Google LLC
173 *
174 * Licensed under the Apache License, Version 2.0 (the "License");
175 * you may not use this file except in compliance with the License.
176 * You may obtain a copy of the License at
177 *
178 * http://www.apache.org/licenses/LICENSE-2.0
179 *
180 * Unless required by applicable law or agreed to in writing, software
181 * distributed under the License is distributed on an "AS IS" BASIS,
182 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
183 * See the License for the specific language governing permissions and
184 * limitations under the License.
185 */
186/**
187 * Implements the Client abstraction for the Remote Config REST API.
188 */
189class RestClient {
190 constructor(firebaseInstallations, sdkVersion, namespace, projectId, apiKey, appId) {
191 this.firebaseInstallations = firebaseInstallations;
192 this.sdkVersion = sdkVersion;
193 this.namespace = namespace;
194 this.projectId = projectId;
195 this.apiKey = apiKey;
196 this.appId = appId;
197 }
198 /**
199 * Fetches from the Remote Config REST API.
200 *
201 * @throws a {@link ErrorCode.FETCH_NETWORK} error if {@link GlobalFetch#fetch} can't
202 * connect to the network.
203 * @throws a {@link ErrorCode.FETCH_PARSE} error if {@link Response#json} can't parse the
204 * fetch response.
205 * @throws a {@link ErrorCode.FETCH_STATUS} error if the service returns an HTTP error status.
206 */
207 async fetch(request) {
208 const [installationId, installationToken] = await Promise.all([
209 this.firebaseInstallations.getId(),
210 this.firebaseInstallations.getToken()
211 ]);
212 const urlBase = window.FIREBASE_REMOTE_CONFIG_URL_BASE ||
213 'https://firebaseremoteconfig.googleapis.com';
214 const url = `${urlBase}/v1/projects/${this.projectId}/namespaces/${this.namespace}:fetch?key=${this.apiKey}`;
215 const headers = {
216 'Content-Type': 'application/json',
217 'Content-Encoding': 'gzip',
218 // Deviates from pure decorator by not passing max-age header since we don't currently have
219 // service behavior using that header.
220 'If-None-Match': request.eTag || '*'
221 };
222 const requestBody = {
223 /* eslint-disable camelcase */
224 sdk_version: this.sdkVersion,
225 app_instance_id: installationId,
226 app_instance_id_token: installationToken,
227 app_id: this.appId,
228 language_code: getUserLanguage()
229 /* eslint-enable camelcase */
230 };
231 const options = {
232 method: 'POST',
233 headers,
234 body: JSON.stringify(requestBody)
235 };
236 // This logic isn't REST-specific, but shimming abort logic isn't worth another decorator.
237 const fetchPromise = fetch(url, options);
238 const timeoutPromise = new Promise((_resolve, reject) => {
239 // Maps async event listener to Promise API.
240 request.signal.addEventListener(() => {
241 // Emulates https://heycam.github.io/webidl/#aborterror
242 const error = new Error('The operation was aborted.');
243 error.name = 'AbortError';
244 reject(error);
245 });
246 });
247 let response;
248 try {
249 await Promise.race([fetchPromise, timeoutPromise]);
250 response = await fetchPromise;
251 }
252 catch (originalError) {
253 let errorCode = "fetch-client-network" /* FETCH_NETWORK */;
254 if (originalError.name === 'AbortError') {
255 errorCode = "fetch-timeout" /* FETCH_TIMEOUT */;
256 }
257 throw ERROR_FACTORY.create(errorCode, {
258 originalErrorMessage: originalError.message
259 });
260 }
261 let status = response.status;
262 // Normalizes nullable header to optional.
263 const responseEtag = response.headers.get('ETag') || undefined;
264 let config;
265 let state;
266 // JSON parsing throws SyntaxError if the response body isn't a JSON string.
267 // Requesting application/json and checking for a 200 ensures there's JSON data.
268 if (response.status === 200) {
269 let responseBody;
270 try {
271 responseBody = await response.json();
272 }
273 catch (originalError) {
274 throw ERROR_FACTORY.create("fetch-client-parse" /* FETCH_PARSE */, {
275 originalErrorMessage: originalError.message
276 });
277 }
278 config = responseBody['entries'];
279 state = responseBody['state'];
280 }
281 // Normalizes based on legacy state.
282 if (state === 'INSTANCE_STATE_UNSPECIFIED') {
283 status = 500;
284 }
285 else if (state === 'NO_CHANGE') {
286 status = 304;
287 }
288 else if (state === 'NO_TEMPLATE' || state === 'EMPTY_CONFIG') {
289 // These cases can be fixed remotely, so normalize to safe value.
290 config = {};
291 }
292 // Normalize to exception-based control flow for non-success cases.
293 // Encapsulates HTTP specifics in this class as much as possible. Status is still the best for
294 // differentiating success states (200 from 304; the state body param is undefined in a
295 // standard 304).
296 if (status !== 304 && status !== 200) {
297 throw ERROR_FACTORY.create("fetch-status" /* FETCH_STATUS */, {
298 httpStatus: status
299 });
300 }
301 return { status, eTag: responseEtag, config };
302 }
303}
304
305/**
306 * @license
307 * Copyright 2019 Google LLC
308 *
309 * Licensed under the Apache License, Version 2.0 (the "License");
310 * you may not use this file except in compliance with the License.
311 * You may obtain a copy of the License at
312 *
313 * http://www.apache.org/licenses/LICENSE-2.0
314 *
315 * Unless required by applicable law or agreed to in writing, software
316 * distributed under the License is distributed on an "AS IS" BASIS,
317 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
318 * See the License for the specific language governing permissions and
319 * limitations under the License.
320 */
321/**
322 * Shims a minimal AbortSignal.
323 *
324 * <p>AbortController's AbortSignal conveniently decouples fetch timeout logic from other aspects
325 * of networking, such as retries. Firebase doesn't use AbortController enough to justify a
326 * polyfill recommendation, like we do with the Fetch API, but this minimal shim can easily be
327 * swapped out if/when we do.
328 */
329class RemoteConfigAbortSignal {
330 constructor() {
331 this.listeners = [];
332 }
333 addEventListener(listener) {
334 this.listeners.push(listener);
335 }
336 abort() {
337 this.listeners.forEach(listener => listener());
338 }
339}
340
341/**
342 * @license
343 * Copyright 2019 Google LLC
344 *
345 * Licensed under the Apache License, Version 2.0 (the "License");
346 * you may not use this file except in compliance with the License.
347 * You may obtain a copy of the License at
348 *
349 * http://www.apache.org/licenses/LICENSE-2.0
350 *
351 * Unless required by applicable law or agreed to in writing, software
352 * distributed under the License is distributed on an "AS IS" BASIS,
353 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
354 * See the License for the specific language governing permissions and
355 * limitations under the License.
356 */
357const DEFAULT_VALUE_FOR_BOOLEAN = false;
358const DEFAULT_VALUE_FOR_STRING = '';
359const DEFAULT_VALUE_FOR_NUMBER = 0;
360const BOOLEAN_TRUTHY_VALUES = ['1', 'true', 't', 'yes', 'y', 'on'];
361class Value {
362 constructor(_source, _value = DEFAULT_VALUE_FOR_STRING) {
363 this._source = _source;
364 this._value = _value;
365 }
366 asString() {
367 return this._value;
368 }
369 asBoolean() {
370 if (this._source === 'static') {
371 return DEFAULT_VALUE_FOR_BOOLEAN;
372 }
373 return BOOLEAN_TRUTHY_VALUES.indexOf(this._value.toLowerCase()) >= 0;
374 }
375 asNumber() {
376 if (this._source === 'static') {
377 return DEFAULT_VALUE_FOR_NUMBER;
378 }
379 let num = Number(this._value);
380 if (isNaN(num)) {
381 num = DEFAULT_VALUE_FOR_NUMBER;
382 }
383 return num;
384 }
385 getSource() {
386 return this._source;
387 }
388}
389
390/**
391 * @license
392 * Copyright 2019 Google LLC
393 *
394 * Licensed under the Apache License, Version 2.0 (the "License");
395 * you may not use this file except in compliance with the License.
396 * You may obtain a copy of the License at
397 *
398 * http://www.apache.org/licenses/LICENSE-2.0
399 *
400 * Unless required by applicable law or agreed to in writing, software
401 * distributed under the License is distributed on an "AS IS" BASIS,
402 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
403 * See the License for the specific language governing permissions and
404 * limitations under the License.
405 */
406const DEFAULT_FETCH_TIMEOUT_MILLIS = 60 * 1000; // One minute
407const DEFAULT_CACHE_MAX_AGE_MILLIS = 12 * 60 * 60 * 1000; // Twelve hours.
408/**
409 * Encapsulates business logic mapping network and storage dependencies to the public SDK API.
410 *
411 * See {@link https://github.com/FirebasePrivate/firebase-js-sdk/blob/master/packages/firebase/index.d.ts|interface documentation} for method descriptions.
412 */
413class RemoteConfig {
414 constructor(
415 // Required by FirebaseServiceFactory interface.
416 app,
417 // JS doesn't support private yet
418 // (https://github.com/tc39/proposal-class-fields#private-fields), so we hint using an
419 // underscore prefix.
420 _client, _storageCache, _storage, _logger) {
421 this.app = app;
422 this._client = _client;
423 this._storageCache = _storageCache;
424 this._storage = _storage;
425 this._logger = _logger;
426 // Tracks completion of initialization promise.
427 this._isInitializationComplete = false;
428 this.settings = {
429 fetchTimeoutMillis: DEFAULT_FETCH_TIMEOUT_MILLIS,
430 minimumFetchIntervalMillis: DEFAULT_CACHE_MAX_AGE_MILLIS
431 };
432 this.defaultConfig = {};
433 }
434 // Based on packages/firestore/src/util/log.ts but not static because we need per-instance levels
435 // to differentiate 2p and 3p use-cases.
436 setLogLevel(logLevel) {
437 switch (logLevel) {
438 case 'debug':
439 this._logger.logLevel = LogLevel.DEBUG;
440 break;
441 case 'silent':
442 this._logger.logLevel = LogLevel.SILENT;
443 break;
444 default:
445 this._logger.logLevel = LogLevel.ERROR;
446 }
447 }
448 get fetchTimeMillis() {
449 return this._storageCache.getLastSuccessfulFetchTimestampMillis() || -1;
450 }
451 get lastFetchStatus() {
452 return this._storageCache.getLastFetchStatus() || 'no-fetch-yet';
453 }
454 async activate() {
455 const [lastSuccessfulFetchResponse, activeConfigEtag] = await Promise.all([
456 this._storage.getLastSuccessfulFetchResponse(),
457 this._storage.getActiveConfigEtag()
458 ]);
459 if (!lastSuccessfulFetchResponse ||
460 !lastSuccessfulFetchResponse.config ||
461 !lastSuccessfulFetchResponse.eTag ||
462 lastSuccessfulFetchResponse.eTag === activeConfigEtag) {
463 // Either there is no successful fetched config, or is the same as current active
464 // config.
465 return false;
466 }
467 await Promise.all([
468 this._storageCache.setActiveConfig(lastSuccessfulFetchResponse.config),
469 this._storage.setActiveConfigEtag(lastSuccessfulFetchResponse.eTag)
470 ]);
471 return true;
472 }
473 ensureInitialized() {
474 if (!this._initializePromise) {
475 this._initializePromise = this._storageCache
476 .loadFromStorage()
477 .then(() => {
478 this._isInitializationComplete = true;
479 });
480 }
481 return this._initializePromise;
482 }
483 /**
484 * @throws a {@link ErrorCode.FETCH_CLIENT_TIMEOUT} if the request takes longer than
485 * {@link Settings.fetchTimeoutInSeconds} or
486 * {@link DEFAULT_FETCH_TIMEOUT_SECONDS}.
487 */
488 async fetch() {
489 // Aborts the request after the given timeout, causing the fetch call to
490 // reject with an AbortError.
491 //
492 // <p>Aborting after the request completes is a no-op, so we don't need a
493 // corresponding clearTimeout.
494 //
495 // Locating abort logic here because:
496 // * it uses a developer setting (timeout)
497 // * it applies to all retries (like curl's max-time arg)
498 // * it is consistent with the Fetch API's signal input
499 const abortSignal = new RemoteConfigAbortSignal();
500 setTimeout(async () => {
501 // Note a very low delay, eg < 10ms, can elapse before listeners are initialized.
502 abortSignal.abort();
503 }, this.settings.fetchTimeoutMillis);
504 // Catches *all* errors thrown by client so status can be set consistently.
505 try {
506 await this._client.fetch({
507 cacheMaxAgeMillis: this.settings.minimumFetchIntervalMillis,
508 signal: abortSignal
509 });
510 await this._storageCache.setLastFetchStatus('success');
511 }
512 catch (e) {
513 const lastFetchStatus = hasErrorCode(e, "fetch-throttle" /* FETCH_THROTTLE */)
514 ? 'throttle'
515 : 'failure';
516 await this._storageCache.setLastFetchStatus(lastFetchStatus);
517 throw e;
518 }
519 }
520 async fetchAndActivate() {
521 await this.fetch();
522 return this.activate();
523 }
524 getAll() {
525 return getAllKeys(this._storageCache.getActiveConfig(), this.defaultConfig).reduce((allConfigs, key) => {
526 allConfigs[key] = this.getValue(key);
527 return allConfigs;
528 }, {});
529 }
530 getBoolean(key) {
531 return this.getValue(key).asBoolean();
532 }
533 getNumber(key) {
534 return this.getValue(key).asNumber();
535 }
536 getString(key) {
537 return this.getValue(key).asString();
538 }
539 getValue(key) {
540 if (!this._isInitializationComplete) {
541 this._logger.debug(`A value was requested for key "${key}" before SDK initialization completed.` +
542 ' Await on ensureInitialized if the intent was to get a previously activated value.');
543 }
544 const activeConfig = this._storageCache.getActiveConfig();
545 if (activeConfig && activeConfig[key] !== undefined) {
546 return new Value('remote', activeConfig[key]);
547 }
548 else if (this.defaultConfig && this.defaultConfig[key] !== undefined) {
549 return new Value('default', String(this.defaultConfig[key]));
550 }
551 this._logger.debug(`Returning static value for key "${key}".` +
552 ' Define a default or remote value if this is unintentional.');
553 return new Value('static');
554 }
555}
556/**
557 * Dedupes and returns an array of all the keys of the received objects.
558 */
559function getAllKeys(obj1 = {}, obj2 = {}) {
560 return Object.keys(Object.assign(Object.assign({}, obj1), obj2));
561}
562
563/**
564 * @license
565 * Copyright 2019 Google LLC
566 *
567 * Licensed under the Apache License, Version 2.0 (the "License");
568 * you may not use this file except in compliance with the License.
569 * You may obtain a copy of the License at
570 *
571 * http://www.apache.org/licenses/LICENSE-2.0
572 *
573 * Unless required by applicable law or agreed to in writing, software
574 * distributed under the License is distributed on an "AS IS" BASIS,
575 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
576 * See the License for the specific language governing permissions and
577 * limitations under the License.
578 */
579/**
580 * Converts an error event associated with a {@link IDBRequest} to a {@link FirebaseError}.
581 */
582function toFirebaseError(event, errorCode) {
583 const originalError = event.target.error || undefined;
584 return ERROR_FACTORY.create(errorCode, {
585 originalErrorMessage: originalError && originalError.message
586 });
587}
588/**
589 * A general-purpose store keyed by app + namespace + {@link
590 * ProjectNamespaceKeyFieldValue}.
591 *
592 * <p>The Remote Config SDK can be used with multiple app installations, and each app can interact
593 * with multiple namespaces, so this store uses app (ID + name) and namespace as common parent keys
594 * for a set of key-value pairs. See {@link Storage#createCompositeKey}.
595 *
596 * <p>Visible for testing.
597 */
598const APP_NAMESPACE_STORE = 'app_namespace_store';
599const DB_NAME = 'firebase_remote_config';
600const DB_VERSION = 1;
601// Visible for testing.
602function openDatabase() {
603 return new Promise((resolve, reject) => {
604 const request = indexedDB.open(DB_NAME, DB_VERSION);
605 request.onerror = event => {
606 reject(toFirebaseError(event, "storage-open" /* STORAGE_OPEN */));
607 };
608 request.onsuccess = event => {
609 resolve(event.target.result);
610 };
611 request.onupgradeneeded = event => {
612 const db = event.target.result;
613 // We don't use 'break' in this switch statement, the fall-through
614 // behavior is what we want, because if there are multiple versions between
615 // the old version and the current version, we want ALL the migrations
616 // that correspond to those versions to run, not only the last one.
617 // eslint-disable-next-line default-case
618 switch (event.oldVersion) {
619 case 0:
620 db.createObjectStore(APP_NAMESPACE_STORE, {
621 keyPath: 'compositeKey'
622 });
623 }
624 };
625 });
626}
627/**
628 * Abstracts data persistence.
629 */
630class Storage {
631 /**
632 * @param appId enables storage segmentation by app (ID + name).
633 * @param appName enables storage segmentation by app (ID + name).
634 * @param namespace enables storage segmentation by namespace.
635 */
636 constructor(appId, appName, namespace, openDbPromise = openDatabase()) {
637 this.appId = appId;
638 this.appName = appName;
639 this.namespace = namespace;
640 this.openDbPromise = openDbPromise;
641 }
642 getLastFetchStatus() {
643 return this.get('last_fetch_status');
644 }
645 setLastFetchStatus(status) {
646 return this.set('last_fetch_status', status);
647 }
648 // This is comparable to a cache entry timestamp. If we need to expire other data, we could
649 // consider adding timestamp to all storage records and an optional max age arg to getters.
650 getLastSuccessfulFetchTimestampMillis() {
651 return this.get('last_successful_fetch_timestamp_millis');
652 }
653 setLastSuccessfulFetchTimestampMillis(timestamp) {
654 return this.set('last_successful_fetch_timestamp_millis', timestamp);
655 }
656 getLastSuccessfulFetchResponse() {
657 return this.get('last_successful_fetch_response');
658 }
659 setLastSuccessfulFetchResponse(response) {
660 return this.set('last_successful_fetch_response', response);
661 }
662 getActiveConfig() {
663 return this.get('active_config');
664 }
665 setActiveConfig(config) {
666 return this.set('active_config', config);
667 }
668 getActiveConfigEtag() {
669 return this.get('active_config_etag');
670 }
671 setActiveConfigEtag(etag) {
672 return this.set('active_config_etag', etag);
673 }
674 getThrottleMetadata() {
675 return this.get('throttle_metadata');
676 }
677 setThrottleMetadata(metadata) {
678 return this.set('throttle_metadata', metadata);
679 }
680 deleteThrottleMetadata() {
681 return this.delete('throttle_metadata');
682 }
683 async get(key) {
684 const db = await this.openDbPromise;
685 return new Promise((resolve, reject) => {
686 const transaction = db.transaction([APP_NAMESPACE_STORE], 'readonly');
687 const objectStore = transaction.objectStore(APP_NAMESPACE_STORE);
688 const compositeKey = this.createCompositeKey(key);
689 try {
690 const request = objectStore.get(compositeKey);
691 request.onerror = event => {
692 reject(toFirebaseError(event, "storage-get" /* STORAGE_GET */));
693 };
694 request.onsuccess = event => {
695 const result = event.target.result;
696 if (result) {
697 resolve(result.value);
698 }
699 else {
700 resolve(undefined);
701 }
702 };
703 }
704 catch (e) {
705 reject(ERROR_FACTORY.create("storage-get" /* STORAGE_GET */, {
706 originalErrorMessage: e && e.message
707 }));
708 }
709 });
710 }
711 async set(key, value) {
712 const db = await this.openDbPromise;
713 return new Promise((resolve, reject) => {
714 const transaction = db.transaction([APP_NAMESPACE_STORE], 'readwrite');
715 const objectStore = transaction.objectStore(APP_NAMESPACE_STORE);
716 const compositeKey = this.createCompositeKey(key);
717 try {
718 const request = objectStore.put({
719 compositeKey,
720 value
721 });
722 request.onerror = (event) => {
723 reject(toFirebaseError(event, "storage-set" /* STORAGE_SET */));
724 };
725 request.onsuccess = () => {
726 resolve();
727 };
728 }
729 catch (e) {
730 reject(ERROR_FACTORY.create("storage-set" /* STORAGE_SET */, {
731 originalErrorMessage: e && e.message
732 }));
733 }
734 });
735 }
736 async delete(key) {
737 const db = await this.openDbPromise;
738 return new Promise((resolve, reject) => {
739 const transaction = db.transaction([APP_NAMESPACE_STORE], 'readwrite');
740 const objectStore = transaction.objectStore(APP_NAMESPACE_STORE);
741 const compositeKey = this.createCompositeKey(key);
742 try {
743 const request = objectStore.delete(compositeKey);
744 request.onerror = (event) => {
745 reject(toFirebaseError(event, "storage-delete" /* STORAGE_DELETE */));
746 };
747 request.onsuccess = () => {
748 resolve();
749 };
750 }
751 catch (e) {
752 reject(ERROR_FACTORY.create("storage-delete" /* STORAGE_DELETE */, {
753 originalErrorMessage: e && e.message
754 }));
755 }
756 });
757 }
758 // Facilitates composite key functionality (which is unsupported in IE).
759 createCompositeKey(key) {
760 return [this.appId, this.appName, this.namespace, key].join();
761 }
762}
763
764/**
765 * @license
766 * Copyright 2019 Google LLC
767 *
768 * Licensed under the Apache License, Version 2.0 (the "License");
769 * you may not use this file except in compliance with the License.
770 * You may obtain a copy of the License at
771 *
772 * http://www.apache.org/licenses/LICENSE-2.0
773 *
774 * Unless required by applicable law or agreed to in writing, software
775 * distributed under the License is distributed on an "AS IS" BASIS,
776 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
777 * See the License for the specific language governing permissions and
778 * limitations under the License.
779 */
780/**
781 * A memory cache layer over storage to support the SDK's synchronous read requirements.
782 */
783class StorageCache {
784 constructor(storage) {
785 this.storage = storage;
786 }
787 /**
788 * Memory-only getters
789 */
790 getLastFetchStatus() {
791 return this.lastFetchStatus;
792 }
793 getLastSuccessfulFetchTimestampMillis() {
794 return this.lastSuccessfulFetchTimestampMillis;
795 }
796 getActiveConfig() {
797 return this.activeConfig;
798 }
799 /**
800 * Read-ahead getter
801 */
802 async loadFromStorage() {
803 const lastFetchStatusPromise = this.storage.getLastFetchStatus();
804 const lastSuccessfulFetchTimestampMillisPromise = this.storage.getLastSuccessfulFetchTimestampMillis();
805 const activeConfigPromise = this.storage.getActiveConfig();
806 // Note:
807 // 1. we consistently check for undefined to avoid clobbering defined values
808 // in memory
809 // 2. we defer awaiting to improve readability, as opposed to destructuring
810 // a Promise.all result, for example
811 const lastFetchStatus = await lastFetchStatusPromise;
812 if (lastFetchStatus) {
813 this.lastFetchStatus = lastFetchStatus;
814 }
815 const lastSuccessfulFetchTimestampMillis = await lastSuccessfulFetchTimestampMillisPromise;
816 if (lastSuccessfulFetchTimestampMillis) {
817 this.lastSuccessfulFetchTimestampMillis = lastSuccessfulFetchTimestampMillis;
818 }
819 const activeConfig = await activeConfigPromise;
820 if (activeConfig) {
821 this.activeConfig = activeConfig;
822 }
823 }
824 /**
825 * Write-through setters
826 */
827 setLastFetchStatus(status) {
828 this.lastFetchStatus = status;
829 return this.storage.setLastFetchStatus(status);
830 }
831 setLastSuccessfulFetchTimestampMillis(timestampMillis) {
832 this.lastSuccessfulFetchTimestampMillis = timestampMillis;
833 return this.storage.setLastSuccessfulFetchTimestampMillis(timestampMillis);
834 }
835 setActiveConfig(activeConfig) {
836 this.activeConfig = activeConfig;
837 return this.storage.setActiveConfig(activeConfig);
838 }
839}
840
841/**
842 * @license
843 * Copyright 2019 Google LLC
844 *
845 * Licensed under the Apache License, Version 2.0 (the "License");
846 * you may not use this file except in compliance with the License.
847 * You may obtain a copy of the License at
848 *
849 * http://www.apache.org/licenses/LICENSE-2.0
850 *
851 * Unless required by applicable law or agreed to in writing, software
852 * distributed under the License is distributed on an "AS IS" BASIS,
853 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
854 * See the License for the specific language governing permissions and
855 * limitations under the License.
856 */
857/**
858 * Supports waiting on a backoff by:
859 *
860 * <ul>
861 * <li>Promisifying setTimeout, so we can set a timeout in our Promise chain</li>
862 * <li>Listening on a signal bus for abort events, just like the Fetch API</li>
863 * <li>Failing in the same way the Fetch API fails, so timing out a live request and a throttled
864 * request appear the same.</li>
865 * </ul>
866 *
867 * <p>Visible for testing.
868 */
869function setAbortableTimeout(signal, throttleEndTimeMillis) {
870 return new Promise((resolve, reject) => {
871 // Derives backoff from given end time, normalizing negative numbers to zero.
872 const backoffMillis = Math.max(throttleEndTimeMillis - Date.now(), 0);
873 const timeout = setTimeout(resolve, backoffMillis);
874 // Adds listener, rather than sets onabort, because signal is a shared object.
875 signal.addEventListener(() => {
876 clearTimeout(timeout);
877 // If the request completes before this timeout, the rejection has no effect.
878 reject(ERROR_FACTORY.create("fetch-throttle" /* FETCH_THROTTLE */, {
879 throttleEndTimeMillis
880 }));
881 });
882 });
883}
884/**
885 * Returns true if the {@link Error} indicates a fetch request may succeed later.
886 */
887function isRetriableError(e) {
888 if (!(e instanceof FirebaseError) || !e.customData) {
889 return false;
890 }
891 // Uses string index defined by ErrorData, which FirebaseError implements.
892 const httpStatus = Number(e.customData['httpStatus']);
893 return (httpStatus === 429 ||
894 httpStatus === 500 ||
895 httpStatus === 503 ||
896 httpStatus === 504);
897}
898/**
899 * Decorates a Client with retry logic.
900 *
901 * <p>Comparable to CachingClient, but uses backoff logic instead of cache max age and doesn't cache
902 * responses (because the SDK has no use for error responses).
903 */
904class RetryingClient {
905 constructor(client, storage) {
906 this.client = client;
907 this.storage = storage;
908 }
909 async fetch(request) {
910 const throttleMetadata = (await this.storage.getThrottleMetadata()) || {
911 backoffCount: 0,
912 throttleEndTimeMillis: Date.now()
913 };
914 return this.attemptFetch(request, throttleMetadata);
915 }
916 /**
917 * A recursive helper for attempting a fetch request repeatedly.
918 *
919 * @throws any non-retriable errors.
920 */
921 async attemptFetch(request, { throttleEndTimeMillis, backoffCount }) {
922 // Starts with a (potentially zero) timeout to support resumption from stored state.
923 // Ensures the throttle end time is honored if the last attempt timed out.
924 // Note the SDK will never make a request if the fetch timeout expires at this point.
925 await setAbortableTimeout(request.signal, throttleEndTimeMillis);
926 try {
927 const response = await this.client.fetch(request);
928 // Note the SDK only clears throttle state if response is success or non-retriable.
929 await this.storage.deleteThrottleMetadata();
930 return response;
931 }
932 catch (e) {
933 if (!isRetriableError(e)) {
934 throw e;
935 }
936 // Increments backoff state.
937 const throttleMetadata = {
938 throttleEndTimeMillis: Date.now() + calculateBackoffMillis(backoffCount),
939 backoffCount: backoffCount + 1
940 };
941 // Persists state.
942 await this.storage.setThrottleMetadata(throttleMetadata);
943 return this.attemptFetch(request, throttleMetadata);
944 }
945 }
946}
947
948const name = "@firebase/remote-config";
949const version = "0.1.38";
950
951/**
952 * @license
953 * Copyright 2019 Google LLC
954 *
955 * Licensed under the Apache License, Version 2.0 (the "License");
956 * you may not use this file except in compliance with the License.
957 * You may obtain a copy of the License at
958 *
959 * http://www.apache.org/licenses/LICENSE-2.0
960 *
961 * Unless required by applicable law or agreed to in writing, software
962 * distributed under the License is distributed on an "AS IS" BASIS,
963 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
964 * See the License for the specific language governing permissions and
965 * limitations under the License.
966 */
967function registerRemoteConfig(firebaseInstance) {
968 firebaseInstance.INTERNAL.registerComponent(new Component('remoteConfig', remoteConfigFactory, "PUBLIC" /* PUBLIC */).setMultipleInstances(true));
969 firebaseInstance.registerVersion(name, version);
970 function remoteConfigFactory(container, { instanceIdentifier: namespace }) {
971 /* Dependencies */
972 // getImmediate for FirebaseApp will always succeed
973 const app = container.getProvider('app').getImmediate();
974 // The following call will always succeed because rc has `import '@firebase/installations'`
975 const installations = container.getProvider('installations').getImmediate();
976 // Guards against the SDK being used in non-browser environments.
977 if (typeof window === 'undefined') {
978 throw ERROR_FACTORY.create("registration-window" /* REGISTRATION_WINDOW */);
979 }
980 // Normalizes optional inputs.
981 const { projectId, apiKey, appId } = app.options;
982 if (!projectId) {
983 throw ERROR_FACTORY.create("registration-project-id" /* REGISTRATION_PROJECT_ID */);
984 }
985 if (!apiKey) {
986 throw ERROR_FACTORY.create("registration-api-key" /* REGISTRATION_API_KEY */);
987 }
988 if (!appId) {
989 throw ERROR_FACTORY.create("registration-app-id" /* REGISTRATION_APP_ID */);
990 }
991 namespace = namespace || 'firebase';
992 const storage = new Storage(appId, app.name, namespace);
993 const storageCache = new StorageCache(storage);
994 const logger = new Logger(name);
995 // Sets ERROR as the default log level.
996 // See RemoteConfig#setLogLevel for corresponding normalization to ERROR log level.
997 logger.logLevel = LogLevel.ERROR;
998 const restClient = new RestClient(installations,
999 // Uses the JS SDK version, by which the RC package version can be deduced, if necessary.
1000 firebaseInstance.SDK_VERSION, namespace, projectId, apiKey, appId);
1001 const retryingClient = new RetryingClient(restClient, storage);
1002 const cachingClient = new CachingClient(retryingClient, storage, storageCache, logger);
1003 const remoteConfigInstance = new RemoteConfig(app, cachingClient, storageCache, storage, logger);
1004 // Starts warming cache.
1005 // eslint-disable-next-line @typescript-eslint/no-floating-promises
1006 remoteConfigInstance.ensureInitialized();
1007 return remoteConfigInstance;
1008 }
1009}
1010registerRemoteConfig(firebase);
1011
1012export { registerRemoteConfig };
1013//# sourceMappingURL=index.esm2017.js.map