diff --git a/.changeset/forty-bags-arrive.md b/.changeset/forty-bags-arrive.md new file mode 100644 index 00000000000..ee68fa5accc --- /dev/null +++ b/.changeset/forty-bags-arrive.md @@ -0,0 +1,8 @@ +--- +'@firebase/database-compat': patch +'@firebase/database': patch +'firebase': patch +--- + +Fixed: invoking `connectDatabaseEmulator` multiple times with the same parameters will no longer +cause an error. Fixes [GitHub Issue #6824](https://github.com/firebase/firebase-js-sdk/issues/6824). \ No newline at end of file diff --git a/packages/database-compat/test/database.test.ts b/packages/database-compat/test/database.test.ts index 8e984e211a6..fa21058591f 100644 --- a/packages/database-compat/test/database.test.ts +++ b/packages/database-compat/test/database.test.ts @@ -301,7 +301,9 @@ describe('Database Tests', () => { expect(() => { db.useEmulator('localhost', 1234); - }).to.throw(/Cannot call useEmulator/); + }).to.throw( + 'FIREBASE FATAL ERROR: connectDatabaseEmulator() cannot initialize or alter the emulator configuration after the database instance has started.' + ); }); it('refFromURL returns an emulated ref with useEmulator', () => { diff --git a/packages/database/src/api/Database.ts b/packages/database/src/api/Database.ts index 72ae85c08a1..32fd4674a44 100644 --- a/packages/database/src/api/Database.ts +++ b/packages/database/src/api/Database.ts @@ -27,6 +27,7 @@ import { Provider } from '@firebase/component'; import { getModularInstance, createMockUserToken, + deepEqual, EmulatorMockTokenOptions, getDefaultEmulatorHostnameAndPort } from '@firebase/util'; @@ -38,7 +39,7 @@ import { FirebaseAuthTokenProvider } from '../core/AuthTokenProvider'; import { Repo, repoInterrupt, repoResume, repoStart } from '../core/Repo'; -import { RepoInfo } from '../core/RepoInfo'; +import { RepoInfo, RepoInfoEmulatorOptions } from '../core/RepoInfo'; import { parseRepoInfo } from '../core/util/libs/parser'; import { newEmptyPath, pathIsEmpty } from '../core/util/Path'; import { @@ -84,19 +85,20 @@ let useRestClient = false; */ function repoManagerApplyEmulatorSettings( repo: Repo, - host: string, - port: number, + hostAndPort: string, + emulatorOptions: RepoInfoEmulatorOptions, tokenProvider?: AuthTokenProvider ): void { repo.repoInfo_ = new RepoInfo( - `${host}:${port}`, + hostAndPort, /* secure= */ false, repo.repoInfo_.namespace, repo.repoInfo_.webSocketOnly, repo.repoInfo_.nodeAdmin, repo.repoInfo_.persistenceKey, repo.repoInfo_.includeNamespaceInQueryParams, - /*isUsingEmulator=*/ true + /*isUsingEmulator=*/ true, + emulatorOptions ); if (tokenProvider) { @@ -350,13 +352,22 @@ export function connectDatabaseEmulator( ): void { db = getModularInstance(db); db._checkNotDeleted('useEmulator'); + const hostAndPort = `${host}:${port}`; + const repo = db._repoInternal; if (db._instanceStarted) { + // If the instance has already been started, then silenty fail if this function is called again + // with the same parameters. If the parameters differ then assert. + if ( + hostAndPort === db._repoInternal.repoInfo_.host && + deepEqual(options, repo.repoInfo_.emulatorOptions) + ) { + return; + } fatal( - 'Cannot call useEmulator() after instance has already been initialized.' + 'connectDatabaseEmulator() cannot initialize or alter the emulator configuration after the database instance has started.' ); } - const repo = db._repoInternal; let tokenProvider: EmulatorTokenProvider | undefined = undefined; if (repo.repoInfo_.nodeAdmin) { if (options.mockUserToken) { @@ -374,7 +385,7 @@ export function connectDatabaseEmulator( } // Modify the repo to apply emulator settings - repoManagerApplyEmulatorSettings(repo, host, port, tokenProvider); + repoManagerApplyEmulatorSettings(repo, hostAndPort, options, tokenProvider); } /** diff --git a/packages/database/src/core/RepoInfo.ts b/packages/database/src/core/RepoInfo.ts index 9d4c1abe36b..1f8934c71d2 100644 --- a/packages/database/src/core/RepoInfo.ts +++ b/packages/database/src/core/RepoInfo.ts @@ -15,13 +15,17 @@ * limitations under the License. */ -import { assert } from '@firebase/util'; +import { assert, EmulatorMockTokenOptions } from '@firebase/util'; import { LONG_POLLING, WEBSOCKET } from '../realtime/Constants'; import { PersistentStorage } from './storage/storage'; import { each } from './util/util'; +export interface RepoInfoEmulatorOptions { + mockUserToken?: string | EmulatorMockTokenOptions; +} + /** * A class that holds metadata about a Repo object */ @@ -46,7 +50,8 @@ export class RepoInfo { public readonly nodeAdmin: boolean = false, public readonly persistenceKey: string = '', public readonly includeNamespaceInQueryParams: boolean = false, - public readonly isUsingEmulator: boolean = false + public readonly isUsingEmulator: boolean = false, + public readonly emulatorOptions: RepoInfoEmulatorOptions | null = null ) { this._host = host.toLowerCase(); this._domain = this._host.substr(this._host.indexOf('.') + 1); diff --git a/packages/database/test/exp/integration.test.ts b/packages/database/test/exp/integration.test.ts index adf5094f222..c27ea9da320 100644 --- a/packages/database/test/exp/integration.test.ts +++ b/packages/database/test/exp/integration.test.ts @@ -35,6 +35,7 @@ import { orderByKey } from '../../src/api/Reference_impl'; import { + connectDatabaseEmulator, getDatabase, goOffline, goOnline, @@ -46,8 +47,10 @@ import { EventAccumulatorFactory } from '../helpers/EventAccumulator'; import { DATABASE_ADDRESS, DATABASE_URL, + EMULATOR_PORT, getFreshRepo, getRWRefs, + USE_EMULATOR, waitFor, waitUntil, writeAndValidate @@ -138,6 +141,37 @@ describe('Database@exp Tests', () => { unsubscribe(); }); + if (USE_EMULATOR) { + it('can connect to emulator', async () => { + const db = getDatabase(defaultApp); + connectDatabaseEmulator(db, 'localhost', parseInt(EMULATOR_PORT, 10)); + await get(refFromURL(db, `${DATABASE_ADDRESS}/foo/bar`)); + }); + it('can change emulator config before network operations', async () => { + const db = getDatabase(defaultApp); + const port = parseInt(EMULATOR_PORT, 10); + connectDatabaseEmulator(db, 'localhost', port + 1); + connectDatabaseEmulator(db, 'localhost', port); + await get(refFromURL(db, `${DATABASE_ADDRESS}/foo/bar`)); + }); + it('can connect to emulator after network operations with same parameters', async () => { + const db = getDatabase(defaultApp); + const port = parseInt(EMULATOR_PORT, 10); + connectDatabaseEmulator(db, 'localhost', port); + await get(refFromURL(db, `${DATABASE_ADDRESS}/foo/bar`)); + connectDatabaseEmulator(db, 'localhost', port); + }); + it('cannot connect to emulator after network operations with different parameters', async () => { + const db = getDatabase(defaultApp); + const port = parseInt(EMULATOR_PORT, 10); + connectDatabaseEmulator(db, 'localhost', port); + await get(refFromURL(db, `${DATABASE_ADDRESS}/foo/bar`)); + expect(() => { + connectDatabaseEmulator(db, 'localhost', 9001); + }).to.throw(); + }); + } + it('can properly handle unknown deep merges', async () => { // Note: This test requires `testIndex` to be added as an index. // Please run `yarn test:setup` to ensure that this gets added. diff --git a/packages/database/test/helpers/util.ts b/packages/database/test/helpers/util.ts index 73eb04a8c5e..1e898d113f4 100644 --- a/packages/database/test/helpers/util.ts +++ b/packages/database/test/helpers/util.ts @@ -33,9 +33,9 @@ import { EventAccumulator } from './EventAccumulator'; // eslint-disable-next-line @typescript-eslint/no-require-imports export const TEST_PROJECT = require('../../../../config/project.json'); -const EMULATOR_PORT = process.env.RTDB_EMULATOR_PORT; +export const EMULATOR_PORT = process.env.RTDB_EMULATOR_PORT; const EMULATOR_NAMESPACE = process.env.RTDB_EMULATOR_NAMESPACE; -const USE_EMULATOR = !!EMULATOR_PORT; +export const USE_EMULATOR = !!EMULATOR_PORT; let freshRepoId = 0; const activeFreshApps: FirebaseApp[] = [];