Skip to content
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

Implement useEmulator for Database #3904

Merged
merged 17 commits into from
Oct 20, 2020
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
82 changes: 55 additions & 27 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,69 @@ 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;
}
};
samtstern marked this conversation as resolved.
Show resolved Hide resolved

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 {
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 @@ -146,22 +193,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}`,
yuchenshi marked this conversation as resolved.
Show resolved Hide resolved
/* 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
18 changes: 18 additions & 0 deletions packages/database/test/database.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,4 +260,22 @@ 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/);
});
});
9 changes: 9 additions & 0 deletions packages/firebase/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5647,6 +5647,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