Skip to content

Commit

Permalink
Implement useEmulator for Database (#3904)
Browse files Browse the repository at this point in the history
  • Loading branch information
samtstern authored Oct 20, 2020
1 parent 4b540f9 commit ef33328
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 44 deletions.
7 changes: 7 additions & 0 deletions .changeset/bright-ducks-jump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'firebase': minor
'@firebase/database': minor
'@firebase/database-types': minor
---

Add a useEmulator(host, port) method to Realtime Database
2 changes: 2 additions & 0 deletions packages/database-types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export interface DataSnapshot {

export interface Database {
app: FirebaseApp;
useEmulator(host: string, port: number): void;
goOffline(): void;
goOnline(): void;
ref(path?: string | Reference): Reference;
Expand All @@ -43,6 +44,7 @@ export interface Database {
export class FirebaseDatabase implements Database {
private constructor();
app: FirebaseApp;
useEmulator(host: string, port: number): void;
goOffline(): void;
goOnline(): void;
ref(path?: string | Reference): Reference;
Expand Down
89 changes: 59 additions & 30 deletions packages/database/src/api/Database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,11 @@ import { FirebaseDatabase } from '@firebase/database-types';
* @implements {FirebaseService}
*/
export class Database implements FirebaseService {
INTERNAL: DatabaseInternals;
private root_: Reference;
/** Track if the instance has been used (root or repo accessed) */
private instanceStarted_: boolean = false;

/** Backing state for root_ */
private rootInternal_?: Reference;

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

/**
* The constructor should not be called by users of our public API.
* @param {!Repo} repo_
* @param {!Repo} repoInternal_
*/
constructor(private repo_: Repo) {
if (!(repo_ instanceof Repo)) {
constructor(private repoInternal_: Repo) {
if (!(repoInternal_ instanceof Repo)) {
fatal(
"Don't call new Database() directly - please use firebase.database()."
);
}
}

/** @type {Reference} */
this.root_ = new Reference(repo_, Path.Empty);
INTERNAL = {
delete: async () => {
this.checkDeleted_('delete');
RepoManager.getInstance().deleteRepo(this.repo_);
this.repoInternal_ = null;
this.rootInternal_ = null;
}
};

this.INTERNAL = new DatabaseInternals(this);
private get repo_(): Repo {
if (!this.instanceStarted_) {
this.repoInternal_.start();
this.instanceStarted_ = true;
}
return this.repoInternal_;
}

get root_(): Reference {
if (!this.rootInternal_) {
this.rootInternal_ = new Reference(this.repo_, Path.Empty);
}

return this.rootInternal_;
}

get app(): FirebaseApp {
return this.repo_.app;
}

/**
* Modify this instance to communicate with the Realtime Database emulator.
*
* <p>Note: This method must be called before performing any other operation.
*
* @param host the emulator host (ex: localhost)
* @param port the emulator port (ex: 8080)
*/
useEmulator(host: string, port: number): void {
this.checkDeleted_('useEmulator');
if (this.instanceStarted_) {
fatal(
'Cannot call useEmulator() after instance has already been initialized.'
);
return;
}

// Modify the repo to apply emulator settings
RepoManager.getInstance().applyEmulatorSettings(
this.repoInternal_,
host,
port
);
}

/**
* Returns a reference to the root or to the path specified in the provided
* argument.
Expand Down Expand Up @@ -109,14 +157,14 @@ export class Database implements FirebaseService {
validateUrl(apiName, 1, parsedURL);

const repoInfo = parsedURL.repoInfo;
if (repoInfo.host !== this.repo_.repoInfo_.host) {
if (!repoInfo.isCustomHost() && repoInfo.host !== this.repo_.repoInfo_.host) {
fatal(
apiName +
': Host name does not match the current database: ' +
'(found ' +
repoInfo.host +
' but expected ' +
(this.repo_.repoInfo_ as RepoInfo).host +
this.repo_.repoInfo_.host+
')'
);
}
Expand All @@ -128,7 +176,7 @@ export class Database implements FirebaseService {
* @param {string} apiName
*/
private checkDeleted_(apiName: string) {
if (this.repo_ === null) {
if (this.repoInternal_ === null) {
fatal('Cannot call ' + apiName + ' on a deleted database.');
}
}
Expand All @@ -146,22 +194,3 @@ export class Database implements FirebaseService {
this.repo_.resume();
}
}

export class DatabaseInternals {
/** @param {!Database} database */
constructor(public database: Database) {}

/** @return {Promise<void>} */
async delete(): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this.database as any).checkDeleted_('delete');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
RepoManager.getInstance().deleteRepo((this.database as any).repo_ as Repo);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this.database as any).repo_ = null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this.database as any).root_ = null;
this.database.INTERNAL = null;
this.database = null;
}
}
28 changes: 18 additions & 10 deletions packages/database/src/core/Repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ const INTERRUPT_REASON = 'repo_interrupt';
* A connection to a single data repository.
*/
export class Repo {
/** Key for uniquely identifying this repo, used in RepoManager */
readonly key: string;

dataUpdateCount = 0;
private infoSyncTree_: SyncTree;
private serverSyncTree_: SyncTree;
Expand Down Expand Up @@ -81,23 +84,28 @@ export class Repo {

constructor(
public repoInfo_: RepoInfo,
forceRestClient: boolean,
private forceRestClient_: boolean,
public app: FirebaseApp,
authTokenProvider: AuthTokenProvider
public authTokenProvider_: AuthTokenProvider
) {
this.stats_ = StatsManager.getCollection(repoInfo_);
// This key is intentionally not updated if RepoInfo is later changed or replaced
this.key = this.repoInfo_.toURLString();
}

start(): void {
this.stats_ = StatsManager.getCollection(this.repoInfo_);

if (forceRestClient || beingCrawled()) {
if (this.forceRestClient_ || beingCrawled()) {
this.server_ = new ReadonlyRestClient(
this.repoInfo_,
this.onDataUpdate_.bind(this),
authTokenProvider
this.authTokenProvider_
);

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

this.persistentConnection_ = new PersistentConnection(
this.repoInfo_,
app.options.appId,
this.app.options.appId,
this.onDataUpdate_.bind(this),
this.onConnectStatus_.bind(this),
this.onServerInfoUpdate_.bind(this),
authTokenProvider,
this.authTokenProvider_,
authOverride
);

this.server_ = this.persistentConnection_;
}

authTokenProvider.addTokenChangeListener(token => {
this.authTokenProvider_.addTokenChangeListener(token => {
this.server_.refreshAuthToken(token);
});

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

Expand Down
23 changes: 21 additions & 2 deletions packages/database/src/core/RepoManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,25 @@ export class RepoManager {
}
}

/**
* Update an existing repo in place to point to a new host/port.
*/
applyEmulatorSettings(repo: Repo, host: string, port: number): void {
repo.repoInfo_ = new RepoInfo(
`${host}:${port}`,
/* secure= */ false,
repo.repoInfo_.namespace,
repo.repoInfo_.webSocketOnly,
repo.repoInfo_.nodeAdmin,
repo.repoInfo_.persistenceKey,
repo.repoInfo_.includeNamespaceInQueryParams
);

if (repo.repoInfo_.nodeAdmin) {
repo.authTokenProvider_ = new EmulatorAdminTokenProvider();
}
}

/**
* This function should only ever be called to CREATE a new database instance.
*
Expand Down Expand Up @@ -157,13 +176,13 @@ export class RepoManager {
deleteRepo(repo: Repo) {
const appRepos = safeGet(this.repos_, repo.app.name);
// This should never happen...
if (!appRepos || safeGet(appRepos, repo.repoInfo_.toURLString()) !== repo) {
if (!appRepos || safeGet(appRepos, repo.key) !== repo) {
fatal(
`Database ${repo.app.name}(${repo.repoInfo_}) has already been deleted.`
);
}
repo.interrupt();
delete appRepos[repo.repoInfo_.toURLString()];
delete appRepos[repo.key];
}

/**
Expand Down
30 changes: 28 additions & 2 deletions packages/database/test/database.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,8 +229,8 @@ describe('Database Tests', () => {
});

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

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

Expand Down Expand Up @@ -260,4 +260,30 @@ describe('Database Tests', () => {
const ref = (db as any).refFromURL();
}).to.throw(/Expects at least 1/);
});

it('can call useEmulator before use', () => {
const db = (firebase as any).database();
db.useEmulator('localhost', 1234);
expect(db.ref().toString()).to.equal('http://localhost:1234/');
});

it('cannot call useEmulator after use', () => {
const db = (firebase as any).database();

db.ref().set({
hello: 'world'
});

expect(() => {
db.useEmulator('localhost', 1234);
}).to.throw(/Cannot call useEmulator/);
});

it('refFromURL returns an emulated ref with useEmulator', () => {
const db = (firebase as any).database();
db.useEmulator('localhost', 1234);

const ref = db.refFromURL(DATABASE_ADDRESS + '/path/to/data');
expect(ref.toString()).to.equal(`http://localhost:1234/path/to/data`);
});
});
9 changes: 9 additions & 0 deletions packages/firebase/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5667,6 +5667,15 @@ declare namespace firebase.database {
* ```
*/
app: firebase.app.App;
/**
* Modify this instance to communicate with the Realtime Database emulator.
*
* <p>Note: This method must be called before performing any other operation.
*
* @param host the emulator host (ex: localhost)
* @param port the emulator port (ex: 8080)
*/
useEmulator(host: string, port: number): void;
/**
* Disconnects from the server (all Database operations will be completed
* offline).
Expand Down

0 comments on commit ef33328

Please sign in to comment.