UNPKG

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