Skip to content

Commit b3e68ca

Browse files
authored
Emulator Idempotency: Database (#8769)
Update the `connectDatabaseEmulator` function to support its invocation more than once. If the Database instance is already in use, and `connectDatabaseEmulator` is invoked with the same configuration, then the invocation will now succeed instead of assert. This unlocks support for web frameworks which may render the page numerous times with the same instances of RTDB. Before this PR customers needed to add extra code to guard against calling `connectDatabaseEmulator` in their SSR logic. Now we do that guarding logic on their behalf which should simplify our customer's apps. Fixes #6824.
1 parent 69c3326 commit b3e68ca

File tree

6 files changed

+73
-13
lines changed

6 files changed

+73
-13
lines changed

.changeset/forty-bags-arrive.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@firebase/database-compat': patch
3+
'@firebase/database': patch
4+
'firebase': patch
5+
---
6+
7+
Fixed: invoking `connectDatabaseEmulator` multiple times with the same parameters will no longer
8+
cause an error. Fixes [GitHub Issue #6824](https://github.com/firebase/firebase-js-sdk/issues/6824).

packages/database-compat/test/database.test.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,9 @@ describe('Database Tests', () => {
301301

302302
expect(() => {
303303
db.useEmulator('localhost', 1234);
304-
}).to.throw(/Cannot call useEmulator/);
304+
}).to.throw(
305+
'FIREBASE FATAL ERROR: connectDatabaseEmulator() cannot initialize or alter the emulator configuration after the database instance has started.'
306+
);
305307
});
306308

307309
it('refFromURL returns an emulated ref with useEmulator', () => {

packages/database/src/api/Database.ts

+19-8
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { Provider } from '@firebase/component';
2727
import {
2828
getModularInstance,
2929
createMockUserToken,
30+
deepEqual,
3031
EmulatorMockTokenOptions,
3132
getDefaultEmulatorHostnameAndPort
3233
} from '@firebase/util';
@@ -38,7 +39,7 @@ import {
3839
FirebaseAuthTokenProvider
3940
} from '../core/AuthTokenProvider';
4041
import { Repo, repoInterrupt, repoResume, repoStart } from '../core/Repo';
41-
import { RepoInfo } from '../core/RepoInfo';
42+
import { RepoInfo, RepoInfoEmulatorOptions } from '../core/RepoInfo';
4243
import { parseRepoInfo } from '../core/util/libs/parser';
4344
import { newEmptyPath, pathIsEmpty } from '../core/util/Path';
4445
import {
@@ -84,19 +85,20 @@ let useRestClient = false;
8485
*/
8586
function repoManagerApplyEmulatorSettings(
8687
repo: Repo,
87-
host: string,
88-
port: number,
88+
hostAndPort: string,
89+
emulatorOptions: RepoInfoEmulatorOptions,
8990
tokenProvider?: AuthTokenProvider
9091
): void {
9192
repo.repoInfo_ = new RepoInfo(
92-
`${host}:${port}`,
93+
hostAndPort,
9394
/* secure= */ false,
9495
repo.repoInfo_.namespace,
9596
repo.repoInfo_.webSocketOnly,
9697
repo.repoInfo_.nodeAdmin,
9798
repo.repoInfo_.persistenceKey,
9899
repo.repoInfo_.includeNamespaceInQueryParams,
99-
/*isUsingEmulator=*/ true
100+
/*isUsingEmulator=*/ true,
101+
emulatorOptions
100102
);
101103

102104
if (tokenProvider) {
@@ -350,13 +352,22 @@ export function connectDatabaseEmulator(
350352
): void {
351353
db = getModularInstance(db);
352354
db._checkNotDeleted('useEmulator');
355+
const hostAndPort = `${host}:${port}`;
356+
const repo = db._repoInternal;
353357
if (db._instanceStarted) {
358+
// If the instance has already been started, then silenty fail if this function is called again
359+
// with the same parameters. If the parameters differ then assert.
360+
if (
361+
hostAndPort === db._repoInternal.repoInfo_.host &&
362+
deepEqual(options, repo.repoInfo_.emulatorOptions)
363+
) {
364+
return;
365+
}
354366
fatal(
355-
'Cannot call useEmulator() after instance has already been initialized.'
367+
'connectDatabaseEmulator() cannot initialize or alter the emulator configuration after the database instance has started.'
356368
);
357369
}
358370

359-
const repo = db._repoInternal;
360371
let tokenProvider: EmulatorTokenProvider | undefined = undefined;
361372
if (repo.repoInfo_.nodeAdmin) {
362373
if (options.mockUserToken) {
@@ -374,7 +385,7 @@ export function connectDatabaseEmulator(
374385
}
375386

376387
// Modify the repo to apply emulator settings
377-
repoManagerApplyEmulatorSettings(repo, host, port, tokenProvider);
388+
repoManagerApplyEmulatorSettings(repo, hostAndPort, options, tokenProvider);
378389
}
379390

380391
/**

packages/database/src/core/RepoInfo.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,17 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { assert } from '@firebase/util';
18+
import { assert, EmulatorMockTokenOptions } from '@firebase/util';
1919

2020
import { LONG_POLLING, WEBSOCKET } from '../realtime/Constants';
2121

2222
import { PersistentStorage } from './storage/storage';
2323
import { each } from './util/util';
2424

25+
export interface RepoInfoEmulatorOptions {
26+
mockUserToken?: string | EmulatorMockTokenOptions;
27+
}
28+
2529
/**
2630
* A class that holds metadata about a Repo object
2731
*/
@@ -46,7 +50,8 @@ export class RepoInfo {
4650
public readonly nodeAdmin: boolean = false,
4751
public readonly persistenceKey: string = '',
4852
public readonly includeNamespaceInQueryParams: boolean = false,
49-
public readonly isUsingEmulator: boolean = false
53+
public readonly isUsingEmulator: boolean = false,
54+
public readonly emulatorOptions: RepoInfoEmulatorOptions | null = null
5055
) {
5156
this._host = host.toLowerCase();
5257
this._domain = this._host.substr(this._host.indexOf('.') + 1);

packages/database/test/exp/integration.test.ts

+34
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
orderByKey
3636
} from '../../src/api/Reference_impl';
3737
import {
38+
connectDatabaseEmulator,
3839
getDatabase,
3940
goOffline,
4041
goOnline,
@@ -46,8 +47,10 @@ import { EventAccumulatorFactory } from '../helpers/EventAccumulator';
4647
import {
4748
DATABASE_ADDRESS,
4849
DATABASE_URL,
50+
EMULATOR_PORT,
4951
getFreshRepo,
5052
getRWRefs,
53+
USE_EMULATOR,
5154
waitFor,
5255
waitUntil,
5356
writeAndValidate
@@ -138,6 +141,37 @@ describe('Database@exp Tests', () => {
138141
unsubscribe();
139142
});
140143

144+
if (USE_EMULATOR) {
145+
it('can connect to emulator', async () => {
146+
const db = getDatabase(defaultApp);
147+
connectDatabaseEmulator(db, 'localhost', parseInt(EMULATOR_PORT, 10));
148+
await get(refFromURL(db, `${DATABASE_ADDRESS}/foo/bar`));
149+
});
150+
it('can change emulator config before network operations', async () => {
151+
const db = getDatabase(defaultApp);
152+
const port = parseInt(EMULATOR_PORT, 10);
153+
connectDatabaseEmulator(db, 'localhost', port + 1);
154+
connectDatabaseEmulator(db, 'localhost', port);
155+
await get(refFromURL(db, `${DATABASE_ADDRESS}/foo/bar`));
156+
});
157+
it('can connect to emulator after network operations with same parameters', async () => {
158+
const db = getDatabase(defaultApp);
159+
const port = parseInt(EMULATOR_PORT, 10);
160+
connectDatabaseEmulator(db, 'localhost', port);
161+
await get(refFromURL(db, `${DATABASE_ADDRESS}/foo/bar`));
162+
connectDatabaseEmulator(db, 'localhost', port);
163+
});
164+
it('cannot connect to emulator after network operations with different parameters', async () => {
165+
const db = getDatabase(defaultApp);
166+
const port = parseInt(EMULATOR_PORT, 10);
167+
connectDatabaseEmulator(db, 'localhost', port);
168+
await get(refFromURL(db, `${DATABASE_ADDRESS}/foo/bar`));
169+
expect(() => {
170+
connectDatabaseEmulator(db, 'localhost', 9001);
171+
}).to.throw();
172+
});
173+
}
174+
141175
it('can properly handle unknown deep merges', async () => {
142176
// Note: This test requires `testIndex` to be added as an index.
143177
// Please run `yarn test:setup` to ensure that this gets added.

packages/database/test/helpers/util.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ import { EventAccumulator } from './EventAccumulator';
3333

3434
// eslint-disable-next-line @typescript-eslint/no-require-imports
3535
export const TEST_PROJECT = require('../../../../config/project.json');
36-
const EMULATOR_PORT = process.env.RTDB_EMULATOR_PORT;
36+
export const EMULATOR_PORT = process.env.RTDB_EMULATOR_PORT;
3737
const EMULATOR_NAMESPACE = process.env.RTDB_EMULATOR_NAMESPACE;
38-
const USE_EMULATOR = !!EMULATOR_PORT;
38+
export const USE_EMULATOR = !!EMULATOR_PORT;
3939

4040
let freshRepoId = 0;
4141
const activeFreshApps: FirebaseApp[] = [];

0 commit comments

Comments
 (0)