Skip to content

Emulator Idempotency: Database #8769

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Feb 11, 2025
8 changes: 8 additions & 0 deletions .changeset/forty-bags-arrive.md
Original file line number Diff line number Diff line change
@@ -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).
4 changes: 3 additions & 1 deletion packages/database-compat/test/database.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
27 changes: 19 additions & 8 deletions packages/database/src/api/Database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { Provider } from '@firebase/component';
import {
getModularInstance,
createMockUserToken,
deepEqual,
EmulatorMockTokenOptions,
getDefaultEmulatorHostnameAndPort
} from '@firebase/util';
Expand All @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -374,7 +385,7 @@ export function connectDatabaseEmulator(
}

// Modify the repo to apply emulator settings
repoManagerApplyEmulatorSettings(repo, host, port, tokenProvider);
repoManagerApplyEmulatorSettings(repo, hostAndPort, options, tokenProvider);
}

/**
Expand Down
9 changes: 7 additions & 2 deletions packages/database/src/core/RepoInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -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);
Expand Down
34 changes: 34 additions & 0 deletions packages/database/test/exp/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
orderByKey
} from '../../src/api/Reference_impl';
import {
connectDatabaseEmulator,
getDatabase,
goOffline,
goOnline,
Expand All @@ -46,8 +47,10 @@ import { EventAccumulatorFactory } from '../helpers/EventAccumulator';
import {
DATABASE_ADDRESS,
DATABASE_URL,
EMULATOR_PORT,
getFreshRepo,
getRWRefs,
USE_EMULATOR,
waitFor,
waitUntil,
writeAndValidate
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions packages/database/test/helpers/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];
Expand Down
Loading