1 | import { getApp, _getProvider, _registerComponent, registerVersion, SDK_VERSION } from '@firebase/app';
|
2 | import { ErrorFactory, FirebaseError, getModularInstance, calculateBackoffMillis, isIndexedDBAvailable, validateIndexedDBOpenable } from '@firebase/util';
|
3 | import { Component } from '@firebase/component';
|
4 | import { LogLevel, Logger } from '@firebase/logger';
|
5 | import '@firebase/installations';
|
6 |
|
7 | const name = "@firebase/remote-config";
|
8 | const 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 | */
|
34 | class 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 | */
|
62 | const 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 | */
|
80 | const 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 | };
|
101 | const ERROR_FACTORY = new ErrorFactory('remoteconfig' /* service */, 'Remote Config' /* service name */, ERROR_DESCRIPTION_MAP);
|
102 | // Note how this is like typeof/instanceof, but for ErrorCode.
|
103 | function 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 | */
|
123 | const DEFAULT_VALUE_FOR_BOOLEAN = false;
|
124 | const DEFAULT_VALUE_FOR_STRING = '';
|
125 | const DEFAULT_VALUE_FOR_NUMBER = 0;
|
126 | const BOOLEAN_TRUTHY_VALUES = ['1', 'true', 't', 'yes', 'y', 'on'];
|
127 | class 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 | */
|
179 | function 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 | */
|
192 | async 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 | */
|
219 | function 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 | */
|
233 | async 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 | */
|
274 | function 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 | */
|
292 | function 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 | */
|
307 | function 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 | */
|
321 | function 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 | */
|
334 | function 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 | */
|
359 | function 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 | */
|
375 | function 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 | */
|
402 | class 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 | */
|
489 | function 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 | */
|
519 | class 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 | */
|
663 | function 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 | */
|
681 | function 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 | */
|
698 | class 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 | */
|
758 | const DEFAULT_FETCH_TIMEOUT_MILLIS = 60 * 1000; // One minute
|
759 | const 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 | */
|
765 | class 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 | */
|
831 | function 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 | */
|
847 | const APP_NAMESPACE_STORE = 'app_namespace_store';
|
848 | const DB_NAME = 'firebase_remote_config';
|
849 | const DB_VERSION = 1;
|
850 | // Visible for testing.
|
851 | function 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 | */
|
886 | class 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 | */
|
1039 | class 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 | */
|
1114 | function 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 | */
|
1195 | async 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 | */
|
1210 | async 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 */
|
1229 | registerRemoteConfig();
|
1230 |
|
1231 | export { activate, ensureInitialized, fetchAndActivate, fetchConfig, getAll, getBoolean, getNumber, getRemoteConfig, getString, getValue, isSupported, setLogLevel };
|
1232 | //# sourceMappingURL=index.esm2017.js.map
|