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