Skip to content

Commit ef33328

Browse files
authored
Implement useEmulator for Database (#3904)
1 parent 4b540f9 commit ef33328

File tree

7 files changed

+144
-44
lines changed

7 files changed

+144
-44
lines changed

.changeset/bright-ducks-jump.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'firebase': minor
3+
'@firebase/database': minor
4+
'@firebase/database-types': minor
5+
---
6+
7+
Add a useEmulator(host, port) method to Realtime Database

packages/database-types/index.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export interface DataSnapshot {
3434

3535
export interface Database {
3636
app: FirebaseApp;
37+
useEmulator(host: string, port: number): void;
3738
goOffline(): void;
3839
goOnline(): void;
3940
ref(path?: string | Reference): Reference;
@@ -43,6 +44,7 @@ export interface Database {
4344
export class FirebaseDatabase implements Database {
4445
private constructor();
4546
app: FirebaseApp;
47+
useEmulator(host: string, port: number): void;
4648
goOffline(): void;
4749
goOnline(): void;
4850
ref(path?: string | Reference): Reference;

packages/database/src/api/Database.ts

+59-30
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,11 @@ import { FirebaseDatabase } from '@firebase/database-types';
3333
* @implements {FirebaseService}
3434
*/
3535
export class Database implements FirebaseService {
36-
INTERNAL: DatabaseInternals;
37-
private root_: Reference;
36+
/** Track if the instance has been used (root or repo accessed) */
37+
private instanceStarted_: boolean = false;
38+
39+
/** Backing state for root_ */
40+
private rootInternal_?: Reference;
3841

3942
static readonly ServerValue = {
4043
TIMESTAMP: {
@@ -51,25 +54,70 @@ export class Database implements FirebaseService {
5154

5255
/**
5356
* The constructor should not be called by users of our public API.
54-
* @param {!Repo} repo_
57+
* @param {!Repo} repoInternal_
5558
*/
56-
constructor(private repo_: Repo) {
57-
if (!(repo_ instanceof Repo)) {
59+
constructor(private repoInternal_: Repo) {
60+
if (!(repoInternal_ instanceof Repo)) {
5861
fatal(
5962
"Don't call new Database() directly - please use firebase.database()."
6063
);
6164
}
65+
}
6266

63-
/** @type {Reference} */
64-
this.root_ = new Reference(repo_, Path.Empty);
67+
INTERNAL = {
68+
delete: async () => {
69+
this.checkDeleted_('delete');
70+
RepoManager.getInstance().deleteRepo(this.repo_);
71+
this.repoInternal_ = null;
72+
this.rootInternal_ = null;
73+
}
74+
};
6575

66-
this.INTERNAL = new DatabaseInternals(this);
76+
private get repo_(): Repo {
77+
if (!this.instanceStarted_) {
78+
this.repoInternal_.start();
79+
this.instanceStarted_ = true;
80+
}
81+
return this.repoInternal_;
82+
}
83+
84+
get root_(): Reference {
85+
if (!this.rootInternal_) {
86+
this.rootInternal_ = new Reference(this.repo_, Path.Empty);
87+
}
88+
89+
return this.rootInternal_;
6790
}
6891

6992
get app(): FirebaseApp {
7093
return this.repo_.app;
7194
}
7295

96+
/**
97+
* Modify this instance to communicate with the Realtime Database emulator.
98+
*
99+
* <p>Note: This method must be called before performing any other operation.
100+
*
101+
* @param host the emulator host (ex: localhost)
102+
* @param port the emulator port (ex: 8080)
103+
*/
104+
useEmulator(host: string, port: number): void {
105+
this.checkDeleted_('useEmulator');
106+
if (this.instanceStarted_) {
107+
fatal(
108+
'Cannot call useEmulator() after instance has already been initialized.'
109+
);
110+
return;
111+
}
112+
113+
// Modify the repo to apply emulator settings
114+
RepoManager.getInstance().applyEmulatorSettings(
115+
this.repoInternal_,
116+
host,
117+
port
118+
);
119+
}
120+
73121
/**
74122
* Returns a reference to the root or to the path specified in the provided
75123
* argument.
@@ -109,14 +157,14 @@ export class Database implements FirebaseService {
109157
validateUrl(apiName, 1, parsedURL);
110158

111159
const repoInfo = parsedURL.repoInfo;
112-
if (repoInfo.host !== this.repo_.repoInfo_.host) {
160+
if (!repoInfo.isCustomHost() && repoInfo.host !== this.repo_.repoInfo_.host) {
113161
fatal(
114162
apiName +
115163
': Host name does not match the current database: ' +
116164
'(found ' +
117165
repoInfo.host +
118166
' but expected ' +
119-
(this.repo_.repoInfo_ as RepoInfo).host +
167+
this.repo_.repoInfo_.host+
120168
')'
121169
);
122170
}
@@ -128,7 +176,7 @@ export class Database implements FirebaseService {
128176
* @param {string} apiName
129177
*/
130178
private checkDeleted_(apiName: string) {
131-
if (this.repo_ === null) {
179+
if (this.repoInternal_ === null) {
132180
fatal('Cannot call ' + apiName + ' on a deleted database.');
133181
}
134182
}
@@ -146,22 +194,3 @@ export class Database implements FirebaseService {
146194
this.repo_.resume();
147195
}
148196
}
149-
150-
export class DatabaseInternals {
151-
/** @param {!Database} database */
152-
constructor(public database: Database) {}
153-
154-
/** @return {Promise<void>} */
155-
async delete(): Promise<void> {
156-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
157-
(this.database as any).checkDeleted_('delete');
158-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
159-
RepoManager.getInstance().deleteRepo((this.database as any).repo_ as Repo);
160-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
161-
(this.database as any).repo_ = null;
162-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
163-
(this.database as any).root_ = null;
164-
this.database.INTERNAL = null;
165-
this.database = null;
166-
}
167-
}

packages/database/src/core/Repo.ts

+18-10
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ const INTERRUPT_REASON = 'repo_interrupt';
5454
* A connection to a single data repository.
5555
*/
5656
export class Repo {
57+
/** Key for uniquely identifying this repo, used in RepoManager */
58+
readonly key: string;
59+
5760
dataUpdateCount = 0;
5861
private infoSyncTree_: SyncTree;
5962
private serverSyncTree_: SyncTree;
@@ -81,23 +84,28 @@ export class Repo {
8184

8285
constructor(
8386
public repoInfo_: RepoInfo,
84-
forceRestClient: boolean,
87+
private forceRestClient_: boolean,
8588
public app: FirebaseApp,
86-
authTokenProvider: AuthTokenProvider
89+
public authTokenProvider_: AuthTokenProvider
8790
) {
88-
this.stats_ = StatsManager.getCollection(repoInfo_);
91+
// This key is intentionally not updated if RepoInfo is later changed or replaced
92+
this.key = this.repoInfo_.toURLString();
93+
}
94+
95+
start(): void {
96+
this.stats_ = StatsManager.getCollection(this.repoInfo_);
8997

90-
if (forceRestClient || beingCrawled()) {
98+
if (this.forceRestClient_ || beingCrawled()) {
9199
this.server_ = new ReadonlyRestClient(
92100
this.repoInfo_,
93101
this.onDataUpdate_.bind(this),
94-
authTokenProvider
102+
this.authTokenProvider_
95103
);
96104

97105
// Minor hack: Fire onConnect immediately, since there's no actual connection.
98106
setTimeout(this.onConnectStatus_.bind(this, true), 0);
99107
} else {
100-
const authOverride = app.options['databaseAuthVariableOverride'];
108+
const authOverride = this.app.options['databaseAuthVariableOverride'];
101109
// Validate authOverride
102110
if (typeof authOverride !== 'undefined' && authOverride !== null) {
103111
if (typeof authOverride !== 'object') {
@@ -114,25 +122,25 @@ export class Repo {
114122

115123
this.persistentConnection_ = new PersistentConnection(
116124
this.repoInfo_,
117-
app.options.appId,
125+
this.app.options.appId,
118126
this.onDataUpdate_.bind(this),
119127
this.onConnectStatus_.bind(this),
120128
this.onServerInfoUpdate_.bind(this),
121-
authTokenProvider,
129+
this.authTokenProvider_,
122130
authOverride
123131
);
124132

125133
this.server_ = this.persistentConnection_;
126134
}
127135

128-
authTokenProvider.addTokenChangeListener(token => {
136+
this.authTokenProvider_.addTokenChangeListener(token => {
129137
this.server_.refreshAuthToken(token);
130138
});
131139

132140
// In the case of multiple Repos for the same repoInfo (i.e. there are multiple Firebase.Contexts being used),
133141
// we only want to create one StatsReporter. As such, we'll report stats over the first Repo created.
134142
this.statsReporter_ = StatsManager.getOrCreateReporter(
135-
repoInfo_,
143+
this.repoInfo_,
136144
() => new StatsReporter(this.stats_, this.server_)
137145
);
138146

packages/database/src/core/RepoManager.ts

+21-2
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,25 @@ export class RepoManager {
8787
}
8888
}
8989

90+
/**
91+
* Update an existing repo in place to point to a new host/port.
92+
*/
93+
applyEmulatorSettings(repo: Repo, host: string, port: number): void {
94+
repo.repoInfo_ = new RepoInfo(
95+
`${host}:${port}`,
96+
/* secure= */ false,
97+
repo.repoInfo_.namespace,
98+
repo.repoInfo_.webSocketOnly,
99+
repo.repoInfo_.nodeAdmin,
100+
repo.repoInfo_.persistenceKey,
101+
repo.repoInfo_.includeNamespaceInQueryParams
102+
);
103+
104+
if (repo.repoInfo_.nodeAdmin) {
105+
repo.authTokenProvider_ = new EmulatorAdminTokenProvider();
106+
}
107+
}
108+
90109
/**
91110
* This function should only ever be called to CREATE a new database instance.
92111
*
@@ -157,13 +176,13 @@ export class RepoManager {
157176
deleteRepo(repo: Repo) {
158177
const appRepos = safeGet(this.repos_, repo.app.name);
159178
// This should never happen...
160-
if (!appRepos || safeGet(appRepos, repo.repoInfo_.toURLString()) !== repo) {
179+
if (!appRepos || safeGet(appRepos, repo.key) !== repo) {
161180
fatal(
162181
`Database ${repo.app.name}(${repo.repoInfo_}) has already been deleted.`
163182
);
164183
}
165184
repo.interrupt();
166-
delete appRepos[repo.repoInfo_.toURLString()];
185+
delete appRepos[repo.key];
167186
}
168187

169188
/**

packages/database/test/database.test.ts

+28-2
Original file line numberDiff line numberDiff line change
@@ -229,8 +229,8 @@ describe('Database Tests', () => {
229229
});
230230

231231
it('ref() validates project', () => {
232-
const db1 = defaultApp.database('http://bar.foo.com');
233-
const db2 = defaultApp.database('http://foo.bar.com');
232+
const db1 = defaultApp.database('http://bar.firebaseio.com');
233+
const db2 = defaultApp.database('http://foo.firebaseio.com');
234234

235235
const ref1 = db1.ref('child');
236236

@@ -260,4 +260,30 @@ describe('Database Tests', () => {
260260
const ref = (db as any).refFromURL();
261261
}).to.throw(/Expects at least 1/);
262262
});
263+
264+
it('can call useEmulator before use', () => {
265+
const db = (firebase as any).database();
266+
db.useEmulator('localhost', 1234);
267+
expect(db.ref().toString()).to.equal('http://localhost:1234/');
268+
});
269+
270+
it('cannot call useEmulator after use', () => {
271+
const db = (firebase as any).database();
272+
273+
db.ref().set({
274+
hello: 'world'
275+
});
276+
277+
expect(() => {
278+
db.useEmulator('localhost', 1234);
279+
}).to.throw(/Cannot call useEmulator/);
280+
});
281+
282+
it('refFromURL returns an emulated ref with useEmulator', () => {
283+
const db = (firebase as any).database();
284+
db.useEmulator('localhost', 1234);
285+
286+
const ref = db.refFromURL(DATABASE_ADDRESS + '/path/to/data');
287+
expect(ref.toString()).to.equal(`http://localhost:1234/path/to/data`);
288+
});
263289
});

packages/firebase/index.d.ts

+9
Original file line numberDiff line numberDiff line change
@@ -5667,6 +5667,15 @@ declare namespace firebase.database {
56675667
* ```
56685668
*/
56695669
app: firebase.app.App;
5670+
/**
5671+
* Modify this instance to communicate with the Realtime Database emulator.
5672+
*
5673+
* <p>Note: This method must be called before performing any other operation.
5674+
*
5675+
* @param host the emulator host (ex: localhost)
5676+
* @param port the emulator port (ex: 8080)
5677+
*/
5678+
useEmulator(host: string, port: number): void;
56705679
/**
56715680
* Disconnects from the server (all Database operations will be completed
56725681
* offline).

0 commit comments

Comments
 (0)