diff --git a/.changeset/four-steaks-relate.md b/.changeset/four-steaks-relate.md new file mode 100644 index 00000000000..642691b32dc --- /dev/null +++ b/.changeset/four-steaks-relate.md @@ -0,0 +1,8 @@ +--- +'@firebase/util': minor +'@firebase/app': patch +'@firebase/installations': patch +'@firebase/messaging': patch +--- + +Replace stopgap firebase/util IndexedDB methods with `idb` library. diff --git a/common/api-review/util.api.md b/common/api-review/util.api.md index 21bcea8b894..adfb36a4b09 100644 --- a/common/api-review/util.api.md +++ b/common/api-review/util.api.md @@ -87,25 +87,6 @@ export function createMockUserToken(token: EmulatorMockTokenOptions, projectId?: // @public export function createSubscribe(executor: Executor, onNoObservers?: Executor): Subscribe; -// Warning: (ae-internal-missing-underscore) The name "DBWrapper" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export class DBWrapper { - constructor(_db: IDBDatabase); - // (undocumented) - close(): void; - // Warning: (ae-forgotten-export) The symbol "ObjectStoreWrapper" needs to be exported by the entry point index.d.ts - // - // (undocumented) - createObjectStore(storeName: string, options?: IDBObjectStoreParameters): ObjectStoreWrapper; - // (undocumented) - objectStoreNames: DOMStringList; - // Warning: (ae-forgotten-export) The symbol "TransactionWrapper" needs to be exported by the entry point index.d.ts - // - // (undocumented) - transaction(storeNames: string[] | string, mode?: IDBTransactionMode): TransactionWrapper; -} - // Warning: (ae-forgotten-export) The symbol "DecodedToken" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "decode" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -141,11 +122,6 @@ export class Deferred { wrapCallback(callback?: (error?: unknown, value?: unknown) => void): (error: unknown, value?: unknown) => void; } -// Warning: (ae-internal-missing-underscore) The name "deleteDB" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export function deleteDB(dbName: string): Promise; - // Warning: (ae-forgotten-export) The symbol "FirebaseIdToken" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "EmulatorMockTokenOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -360,11 +336,6 @@ export interface Observer { next: NextFn; } -// Warning: (ae-internal-missing-underscore) The name "openDB" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export function openDB(dbName: string, dbVersion: number, upgradeCallback: (db: DBWrapper, oldVersion: number, newVersion: number | null, transaction: TransactionWrapper) => void): Promise; - // Warning: (ae-missing-release-tag) "ordinal" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public diff --git a/packages/app/package.json b/packages/app/package.json index a7a1d188a68..d47a75c65ee 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -40,12 +40,13 @@ "@firebase/util": "1.5.2", "@firebase/logger": "0.3.2", "@firebase/component": "0.5.13", + "idb": "7.0.1", "tslib": "^2.1.0" }, "license": "Apache-2.0", "devDependencies": { - "rollup": "2.57.0", "@rollup/plugin-json": "4.1.0", + "rollup": "2.57.0", "rollup-plugin-replace": "2.2.0", "rollup-plugin-typescript2": "0.30.0", "typescript": "4.2.2" @@ -65,4 +66,4 @@ ], "reportDir": "./coverage/node" } -} \ No newline at end of file +} diff --git a/packages/app/src/indexeddb.ts b/packages/app/src/indexeddb.ts index e136d895f1f..bf49522faab 100644 --- a/packages/app/src/indexeddb.ts +++ b/packages/app/src/indexeddb.ts @@ -15,26 +15,36 @@ * limitations under the License. */ -import { DBWrapper, openDB } from '@firebase/util'; +import { DBSchema, openDB, IDBPDatabase } from 'idb'; import { AppError, ERROR_FACTORY } from './errors'; import { FirebaseApp } from './public-types'; import { HeartbeatsInIndexedDB } from './types'; + const DB_NAME = 'firebase-heartbeat-database'; const DB_VERSION = 1; const STORE_NAME = 'firebase-heartbeat-store'; -let dbPromise: Promise | null = null; -function getDbPromise(): Promise { +interface AppDB extends DBSchema { + 'firebase-heartbeat-store': { + key: string; + value: HeartbeatsInIndexedDB; + }; +} + +let dbPromise: Promise> | null = null; +function getDbPromise(): Promise> { if (!dbPromise) { - dbPromise = openDB(DB_NAME, DB_VERSION, (db, oldVersion) => { - // We don't use 'break' in this switch statement, the fall-through - // behavior is what we want, because if there are multiple versions between - // the old version and the current version, we want ALL the migrations - // that correspond to those versions to run, not only the last one. - // eslint-disable-next-line default-case - switch (oldVersion) { - case 0: - db.createObjectStore(STORE_NAME); + dbPromise = openDB(DB_NAME, DB_VERSION, { + upgrade: (db, oldVersion) => { + // We don't use 'break' in this switch statement, the fall-through + // behavior is what we want, because if there are multiple versions between + // the old version and the current version, we want ALL the migrations + // that correspond to those versions to run, not only the last one. + // eslint-disable-next-line default-case + switch (oldVersion) { + case 0: + db.createObjectStore(STORE_NAME); + } } }).catch(e => { throw ERROR_FACTORY.create(AppError.STORAGE_OPEN, { @@ -70,7 +80,7 @@ export async function writeHeartbeatsToIndexedDB( const tx = db.transaction(STORE_NAME, 'readwrite'); const objectStore = tx.objectStore(STORE_NAME); await objectStore.put(heartbeatObject, computeKey(app)); - return tx.complete; + return tx.done; } catch (e) { throw ERROR_FACTORY.create(AppError.STORAGE_WRITE, { originalErrorMessage: e.message diff --git a/packages/installations/package.json b/packages/installations/package.json index 66aa65c82b8..64f67caed04 100644 --- a/packages/installations/package.json +++ b/packages/installations/package.json @@ -64,6 +64,7 @@ "dependencies": { "@firebase/util": "1.5.2", "@firebase/component": "0.5.13", + "idb": "7.0.1", "tslib": "^2.1.0" } } \ No newline at end of file diff --git a/packages/installations/src/helpers/idb-manager.ts b/packages/installations/src/helpers/idb-manager.ts index 6c502ae4bb9..32a42d6fe73 100644 --- a/packages/installations/src/helpers/idb-manager.ts +++ b/packages/installations/src/helpers/idb-manager.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { DBWrapper, openDB } from '@firebase/util'; +import { DBSchema, IDBPDatabase, openDB } from 'idb'; import { AppConfig } from '../interfaces/installation-impl'; import { InstallationEntry } from '../interfaces/installation-entry'; import { getKey } from '../util/get-key'; @@ -25,18 +25,27 @@ const DATABASE_NAME = 'firebase-installations-database'; const DATABASE_VERSION = 1; const OBJECT_STORE_NAME = 'firebase-installations-store'; -let dbPromise: Promise | null = null; -function getDbPromise(): Promise { +interface InstallationsDB extends DBSchema { + 'firebase-installations-store': { + key: string; + value: InstallationEntry | undefined; + }; +} + +let dbPromise: Promise> | null = null; +function getDbPromise(): Promise> { if (!dbPromise) { - dbPromise = openDB(DATABASE_NAME, DATABASE_VERSION, (db, oldVersion) => { - // We don't use 'break' in this switch statement, the fall-through - // behavior is what we want, because if there are multiple versions between - // the old version and the current version, we want ALL the migrations - // that correspond to those versions to run, not only the last one. - // eslint-disable-next-line default-case - switch (oldVersion) { - case 0: - db.createObjectStore(OBJECT_STORE_NAME); + dbPromise = openDB(DATABASE_NAME, DATABASE_VERSION, { + upgrade: (db, oldVersion) => { + // We don't use 'break' in this switch statement, the fall-through + // behavior is what we want, because if there are multiple versions between + // the old version and the current version, we want ALL the migrations + // that correspond to those versions to run, not only the last one. + // eslint-disable-next-line default-case + switch (oldVersion) { + case 0: + db.createObjectStore(OBJECT_STORE_NAME); + } } }); } @@ -66,7 +75,7 @@ export async function set( const objectStore = tx.objectStore(OBJECT_STORE_NAME); const oldValue = (await objectStore.get(key)) as InstallationEntry; await objectStore.put(value, key); - await tx.complete; + await tx.done; if (!oldValue || oldValue.fid !== value.fid) { fidChanged(appConfig, value.fid); @@ -81,7 +90,7 @@ export async function remove(appConfig: AppConfig): Promise { const db = await getDbPromise(); const tx = db.transaction(OBJECT_STORE_NAME, 'readwrite'); await tx.objectStore(OBJECT_STORE_NAME).delete(key); - await tx.complete; + await tx.done; } /** @@ -108,7 +117,7 @@ export async function update( } else { await store.put(newValue, key); } - await tx.complete; + await tx.done; if (newValue && (!oldValue || oldValue.fid !== newValue.fid)) { fidChanged(appConfig, newValue.fid); @@ -121,5 +130,5 @@ export async function clear(): Promise { const db = await getDbPromise(); const tx = db.transaction(OBJECT_STORE_NAME, 'readwrite'); await tx.objectStore(OBJECT_STORE_NAME).clear(); - await tx.complete; + await tx.done; } diff --git a/packages/messaging/package.json b/packages/messaging/package.json index dcbe9490faf..643b95fa140 100644 --- a/packages/messaging/package.json +++ b/packages/messaging/package.json @@ -56,6 +56,7 @@ "@firebase/messaging-interop-types": "0.1.0", "@firebase/util": "1.5.2", "@firebase/component": "0.5.13", + "idb": "7.0.1", "tslib": "^2.1.0" }, "devDependencies": { diff --git a/packages/messaging/src/helpers/migrate-old-database.test.ts b/packages/messaging/src/helpers/migrate-old-database.test.ts index 2ae03c7545d..11d61121ebb 100644 --- a/packages/messaging/src/helpers/migrate-old-database.test.ts +++ b/packages/messaging/src/helpers/migrate-old-database.test.ts @@ -28,7 +28,7 @@ import { FakePushSubscription } from '../testing/fakes/service-worker'; import { base64ToArray } from './array-base64-translator'; import { expect } from 'chai'; import { getFakeTokenDetails } from '../testing/fakes/token-details'; -import { openDB } from '@firebase/util'; +import { openDB } from 'idb'; describe('migrateOldDb', () => { it("does nothing if old DB didn't exist", async () => { @@ -179,22 +179,24 @@ describe('migrateOldDb', () => { }); async function put(version: number, value: object): Promise { - const db = await openDB('fcm_token_details_db', version, (db, oldVersion) => { - if (oldVersion === 0) { - const objectStore = db.createObjectStore('fcm_token_object_Store', { - keyPath: 'swScope' - }); - objectStore.createIndex('fcmSenderId', 'fcmSenderId', { - unique: false - }); - objectStore.createIndex('fcmToken', 'fcmToken', { unique: true }); + const db = await openDB('fcm_token_details_db', version, { + upgrade: (db, oldVersion) => { + if (oldVersion === 0) { + const objectStore = db.createObjectStore('fcm_token_object_Store', { + keyPath: 'swScope' + }); + objectStore.createIndex('fcmSenderId', 'fcmSenderId', { + unique: false + }); + objectStore.createIndex('fcmToken', 'fcmToken', { unique: true }); + } } }); try { const tx = db.transaction('fcm_token_object_Store', 'readwrite'); await tx.objectStore('fcm_token_object_Store').put(value); - await tx.complete; + await tx.done; } finally { db.close(); } diff --git a/packages/messaging/src/helpers/migrate-old-database.ts b/packages/messaging/src/helpers/migrate-old-database.ts index 0117faca131..a0fce5b3531 100644 --- a/packages/messaging/src/helpers/migrate-old-database.ts +++ b/packages/messaging/src/helpers/migrate-old-database.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { deleteDB, openDB } from '@firebase/util'; +import { deleteDB, openDB } from 'idb'; import { TokenDetails } from '../interfaces/token-details'; import { arrayToBase64 } from './array-base64-translator'; @@ -88,10 +88,8 @@ export async function migrateOldDatabase( let tokenDetails: TokenDetails | null = null; - const db = await openDB( - OLD_DB_NAME, - OLD_DB_VERSION, - async (db, oldVersion, newVersion, upgradeTransaction) => { + const db = await openDB(OLD_DB_NAME, OLD_DB_VERSION, { + upgrade: async (db, oldVersion, newVersion, upgradeTransaction) => { if (oldVersion < 2) { // Database too old, skip migration. return; @@ -162,7 +160,7 @@ export async function migrateOldDatabase( }; } } - ); + }); db.close(); // Delete all old databases. diff --git a/packages/messaging/src/internals/idb-manager.ts b/packages/messaging/src/internals/idb-manager.ts index 1aa82734757..de471066afc 100644 --- a/packages/messaging/src/internals/idb-manager.ts +++ b/packages/messaging/src/internals/idb-manager.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { DBWrapper, deleteDB, openDB } from '@firebase/util'; +import { DBSchema, IDBPDatabase, deleteDB, openDB } from 'idb'; import { FirebaseInternalDependencies } from '../interfaces/internal-dependencies'; import { TokenDetails } from '../interfaces/token-details'; @@ -26,13 +26,18 @@ export const DATABASE_NAME = 'firebase-messaging-database'; const DATABASE_VERSION = 1; const OBJECT_STORE_NAME = 'firebase-messaging-store'; -let dbPromise: Promise | null = null; -function getDbPromise(): Promise { +interface MessagingDB extends DBSchema { + 'firebase-messaging-store': { + key: string; + value: TokenDetails; + }; +} + +let dbPromise: Promise> | null = null; +function getDbPromise(): Promise> { if (!dbPromise) { - dbPromise = openDB( - DATABASE_NAME, - DATABASE_VERSION, - (upgradeDb, oldVersion) => { + dbPromise = openDB(DATABASE_NAME, DATABASE_VERSION, { + upgrade: (upgradeDb, oldVersion) => { // We don't use 'break' in this switch statement, the fall-through behavior is what we want, // because if there are multiple versions between the old version and the current version, we // want ALL the migrations that correspond to those versions to run, not only the last one. @@ -42,7 +47,7 @@ function getDbPromise(): Promise { upgradeDb.createObjectStore(OBJECT_STORE_NAME); } } - ); + }); } return dbPromise; } @@ -81,7 +86,7 @@ export async function dbSet( const db = await getDbPromise(); const tx = db.transaction(OBJECT_STORE_NAME, 'readwrite'); await tx.objectStore(OBJECT_STORE_NAME).put(tokenDetails, key); - await tx.complete; + await tx.done; return tokenDetails; } @@ -93,7 +98,7 @@ export async function dbRemove( const db = await getDbPromise(); const tx = db.transaction(OBJECT_STORE_NAME, 'readwrite'); await tx.objectStore(OBJECT_STORE_NAME).delete(key); - await tx.complete; + await tx.done; } /** Deletes the DB. Useful for tests. */ diff --git a/packages/messaging/src/listeners/sw-listeners.test.ts b/packages/messaging/src/listeners/sw-listeners.test.ts index 5c1b85e6dd9..4019c2d5288 100644 --- a/packages/messaging/src/listeners/sw-listeners.test.ts +++ b/packages/messaging/src/listeners/sw-listeners.test.ts @@ -39,6 +39,7 @@ import { } from '../interfaces/internal-message-payload'; import { NotificationEvent, + PushSubscriptionChangeEvent, ServiceWorkerGlobalScope, ServiceWorkerGlobalScopeEventMap, WindowClient @@ -427,9 +428,9 @@ describe('SwController', () => { describe('onSubChange', () => { it('calls deleteToken if there is no new subscription', async () => { - const event = makeEvent('pushsubscriptionchange', { + const event = makeFakePushSubscriptionChangeEvent({ oldSubscription: new FakePushSubscription(), - newSubscription: undefined + newSubscription: null }); await callEventListener(event); @@ -439,7 +440,7 @@ describe('SwController', () => { }); it('calls deleteToken and getToken if subscription changed', async () => { - const event = makeEvent('pushsubscriptionchange', { + const event = makeFakePushSubscriptionChangeEvent({ oldSubscription: new FakePushSubscription(), newSubscription: new FakePushSubscription() }); @@ -474,3 +475,12 @@ function makeEvent( Object.assign(event, data); return event as unknown as ServiceWorkerGlobalScopeEventMap[K]; } + +function makeFakePushSubscriptionChangeEvent(data: { + newSubscription: PushSubscription | null; + oldSubscription: PushSubscription | null; +}): PushSubscriptionChangeEvent { + const event = new FakeEvent('pushsubscriptionchange'); + Object.assign(event, data); + return event as unknown as PushSubscriptionChangeEvent; +} diff --git a/packages/messaging/src/testing/setup.ts b/packages/messaging/src/testing/setup.ts index 7aa969119c6..69b47f3de81 100644 --- a/packages/messaging/src/testing/setup.ts +++ b/packages/messaging/src/testing/setup.ts @@ -19,7 +19,7 @@ import chaiAsPromised from 'chai-as-promised'; import sinonChai from 'sinon-chai'; import { dbDelete } from '../internals/idb-manager'; -import { deleteDB } from '@firebase/util'; +import { deleteDB } from 'idb'; import { restore } from 'sinon'; import { use } from 'chai'; diff --git a/packages/util/index.node.ts b/packages/util/index.node.ts index e27c304145c..8dace3b8e1e 100644 --- a/packages/util/index.node.ts +++ b/packages/util/index.node.ts @@ -39,6 +39,3 @@ export * from './src/utf8'; export * from './src/exponential_backoff'; export * from './src/formatters'; export * from './src/compat'; -// This can't be used in Node but it will cause errors if libraries import -// these methods and they aren't here. -export * from './src/indexeddb'; diff --git a/packages/util/index.ts b/packages/util/index.ts index 0cf518fbd81..00d661734b8 100644 --- a/packages/util/index.ts +++ b/packages/util/index.ts @@ -34,4 +34,3 @@ export * from './src/utf8'; export * from './src/exponential_backoff'; export * from './src/formatters'; export * from './src/compat'; -export * from './src/indexeddb'; diff --git a/packages/util/src/indexeddb.ts b/packages/util/src/indexeddb.ts deleted file mode 100644 index 5c459c44f26..00000000000 --- a/packages/util/src/indexeddb.ts +++ /dev/null @@ -1,195 +0,0 @@ -/** - * @license - * Copyright 2022 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. - */ - -/** - * @internal - */ -function promisifyRequest( - request: IDBRequest, - errorMessage: string -): Promise { - return new Promise((resolve, reject) => { - request.onsuccess = event => { - resolve((event.target as IDBRequest).result); - }; - request.onerror = event => { - reject(`${errorMessage}: ${(event.target as IDBRequest).error?.message}`); - }; - }); -} - -/** - * @internal - */ -export class DBWrapper { - objectStoreNames: DOMStringList; - constructor(private _db: IDBDatabase) { - this.objectStoreNames = this._db.objectStoreNames; - } - transaction( - storeNames: string[] | string, - mode: IDBTransactionMode = 'readonly' - ): TransactionWrapper { - return new TransactionWrapper( - this._db.transaction.call(this._db, storeNames, mode) - ); - } - createObjectStore( - storeName: string, - options?: IDBObjectStoreParameters - ): ObjectStoreWrapper { - return new ObjectStoreWrapper( - this._db.createObjectStore(storeName, options) - ); - } - close(): void { - this._db.close(); - } -} - -/** - * @internal - */ -class TransactionWrapper { - complete: Promise; - constructor(private _transaction: IDBTransaction) { - this.complete = new Promise((resolve, reject) => { - this._transaction.oncomplete = function () { - resolve(); - }; - this._transaction.onerror = () => { - reject(this._transaction.error); - }; - this._transaction.onabort = () => { - reject(this._transaction.error); - }; - }); - } - objectStore(storeName: string): ObjectStoreWrapper { - return new ObjectStoreWrapper(this._transaction.objectStore(storeName)); - } -} - -/** - * @internal - */ -class ObjectStoreWrapper { - constructor(private _store: IDBObjectStore) {} - index(name: string): IndexWrapper { - return new IndexWrapper(this._store.index(name)); - } - createIndex( - name: string, - keypath: string, - options: IDBIndexParameters - ): IndexWrapper { - return new IndexWrapper(this._store.createIndex(name, keypath, options)); - } - get(key: string): Promise { - const request = this._store.get(key); - return promisifyRequest(request, 'Error reading from IndexedDB'); - } - put(value: unknown, key?: string): Promise { - const request = this._store.put(value, key); - return promisifyRequest(request, 'Error writing to IndexedDB'); - } - delete(key: string): Promise { - const request = this._store.delete(key); - return promisifyRequest(request, 'Error deleting from IndexedDB'); - } - clear(): Promise { - const request = this._store.clear(); - return promisifyRequest(request, 'Error clearing IndexedDB object store'); - } -} - -/** - * @internal - */ -class IndexWrapper { - constructor(private _index: IDBIndex) {} - get(key: string): Promise { - const request = this._index.get(key); - return promisifyRequest(request, 'Error reading from IndexedDB'); - } -} - -/** - * @internal - */ -export function openDB( - dbName: string, - dbVersion: number, - upgradeCallback: ( - db: DBWrapper, - oldVersion: number, - newVersion: number | null, - transaction: TransactionWrapper - ) => void -): Promise { - return new Promise((resolve, reject) => { - try { - const request = indexedDB.open(dbName, dbVersion); - - request.onsuccess = event => { - resolve(new DBWrapper((event.target as IDBOpenDBRequest).result)); - }; - - request.onerror = event => { - reject( - `Error opening indexedDB: ${ - (event.target as IDBRequest).error?.message - }` - ); - }; - - request.onupgradeneeded = event => { - upgradeCallback( - new DBWrapper(request.result), - event.oldVersion, - event.newVersion, - new TransactionWrapper(request.transaction!) - ); - }; - } catch (e) { - reject(`Error opening indexedDB: ${e.message}`); - } - }); -} - -/** - * @internal - */ -export async function deleteDB(dbName: string): Promise { - return new Promise((resolve, reject) => { - try { - const request = indexedDB.deleteDatabase(dbName); - request.onsuccess = () => { - resolve(); - }; - request.onerror = event => { - reject( - `Error deleting indexedDB database "${dbName}": ${ - (event.target as IDBRequest).error?.message - }` - ); - }; - } catch (e) { - reject(`Error deleting indexedDB database "${dbName}": ${e.message}`); - } - }); -} diff --git a/yarn.lock b/yarn.lock index 1d60467a41b..383b0da3564 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9068,6 +9068,11 @@ iconv-lite@0.6.3, iconv-lite@^0.6.2: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" +idb@7.0.1: + version "7.0.1" + resolved "https://registry.npmjs.org/idb/-/idb-7.0.1.tgz#d2875b3a2f205d854ee307f6d196f246fea590a7" + integrity sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg== + ieee754@^1.1.13, ieee754@^1.1.4: version "1.2.1" resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"