UNPKG

voluptasmollitia

Version:
228 lines (206 loc) 7.16 kB
/** * @license * Copyright 2019 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { createInstallationRequest } from '../api/create-installation-request'; import { AppConfig } from '../interfaces/app-config'; import { InProgressInstallationEntry, InstallationEntry, RegisteredInstallationEntry, RequestStatus } from '../interfaces/installation-entry'; import { PENDING_TIMEOUT_MS } from '../util/constants'; import { ERROR_FACTORY, ErrorCode, isServerError } from '../util/errors'; import { sleep } from '../util/sleep'; import { generateFid, INVALID_FID } from './generate-fid'; import { remove, set, update } from './idb-manager'; export interface InstallationEntryWithRegistrationPromise { installationEntry: InstallationEntry; /** Exist iff the installationEntry is not registered. */ registrationPromise?: Promise<RegisteredInstallationEntry>; } /** * Updates and returns the InstallationEntry from the database. * Also triggers a registration request if it is necessary and possible. */ export async function getInstallationEntry( appConfig: AppConfig ): Promise<InstallationEntryWithRegistrationPromise> { let registrationPromise: Promise<RegisteredInstallationEntry> | undefined; const installationEntry = await update(appConfig, oldEntry => { const installationEntry = updateOrCreateInstallationEntry(oldEntry); const entryWithPromise = triggerRegistrationIfNecessary( appConfig, installationEntry ); registrationPromise = entryWithPromise.registrationPromise; return entryWithPromise.installationEntry; }); if (installationEntry.fid === INVALID_FID) { // FID generation failed. Waiting for the FID from the server. return { installationEntry: await registrationPromise! }; } return { installationEntry, registrationPromise }; } /** * Creates a new Installation Entry if one does not exist. * Also clears timed out pending requests. */ function updateOrCreateInstallationEntry( oldEntry: InstallationEntry | undefined ): InstallationEntry { const entry: InstallationEntry = oldEntry || { fid: generateFid(), registrationStatus: RequestStatus.NOT_STARTED }; return clearTimedOutRequest(entry); } /** * If the Firebase Installation is not registered yet, this will trigger the * registration and return an InProgressInstallationEntry. * * If registrationPromise does not exist, the installationEntry is guaranteed * to be registered. */ function triggerRegistrationIfNecessary( appConfig: AppConfig, installationEntry: InstallationEntry ): InstallationEntryWithRegistrationPromise { if (installationEntry.registrationStatus === RequestStatus.NOT_STARTED) { if (!navigator.onLine) { // Registration required but app is offline. const registrationPromiseWithError = Promise.reject( ERROR_FACTORY.create(ErrorCode.APP_OFFLINE) ); return { installationEntry, registrationPromise: registrationPromiseWithError }; } // Try registering. Change status to IN_PROGRESS. const inProgressEntry: InProgressInstallationEntry = { fid: installationEntry.fid, registrationStatus: RequestStatus.IN_PROGRESS, registrationTime: Date.now() }; const registrationPromise = registerInstallation( appConfig, inProgressEntry ); return { installationEntry: inProgressEntry, registrationPromise }; } else if ( installationEntry.registrationStatus === RequestStatus.IN_PROGRESS ) { return { installationEntry, registrationPromise: waitUntilFidRegistration(appConfig) }; } else { return { installationEntry }; } } /** This will be executed only once for each new Firebase Installation. */ async function registerInstallation( appConfig: AppConfig, installationEntry: InProgressInstallationEntry ): Promise<RegisteredInstallationEntry> { try { const registeredInstallationEntry = await createInstallationRequest( appConfig, installationEntry ); return set(appConfig, registeredInstallationEntry); } catch (e) { if (isServerError(e) && e.customData.serverCode === 409) { // Server returned a "FID can not be used" error. // Generate a new ID next time. await remove(appConfig); } else { // Registration failed. Set FID as not registered. await set(appConfig, { fid: installationEntry.fid, registrationStatus: RequestStatus.NOT_STARTED }); } throw e; } } /** Call if FID registration is pending in another request. */ async function waitUntilFidRegistration( appConfig: AppConfig ): Promise<RegisteredInstallationEntry> { // Unfortunately, there is no way of reliably observing when a value in // IndexedDB changes (yet, see https://github.com/WICG/indexed-db-observers), // so we need to poll. let entry: InstallationEntry = await updateInstallationRequest(appConfig); while (entry.registrationStatus === RequestStatus.IN_PROGRESS) { // createInstallation request still in progress. await sleep(100); entry = await updateInstallationRequest(appConfig); } if (entry.registrationStatus === RequestStatus.NOT_STARTED) { // The request timed out or failed in a different call. Try again. const { installationEntry, registrationPromise } = await getInstallationEntry(appConfig); if (registrationPromise) { return registrationPromise; } else { // if there is no registrationPromise, entry is registered. return installationEntry as RegisteredInstallationEntry; } } return entry; } /** * Called only if there is a CreateInstallation request in progress. * * Updates the InstallationEntry in the DB based on the status of the * CreateInstallation request. * * Returns the updated InstallationEntry. */ function updateInstallationRequest( appConfig: AppConfig ): Promise<InstallationEntry> { return update(appConfig, oldEntry => { if (!oldEntry) { throw ERROR_FACTORY.create(ErrorCode.INSTALLATION_NOT_FOUND); } return clearTimedOutRequest(oldEntry); }); } function clearTimedOutRequest(entry: InstallationEntry): InstallationEntry { if (hasInstallationRequestTimedOut(entry)) { return { fid: entry.fid, registrationStatus: RequestStatus.NOT_STARTED }; } return entry; } function hasInstallationRequestTimedOut( installationEntry: InstallationEntry ): boolean { return ( installationEntry.registrationStatus === RequestStatus.IN_PROGRESS && installationEntry.registrationTime + PENDING_TIMEOUT_MS < Date.now() ); }