[js][android] Support multiple listeners on a single ref

This commit is contained in:
Chris Bianca
2017-04-26 12:21:53 +01:00
parent e8854f1a2e
commit ef306162b4
9 changed files with 395 additions and 360 deletions

View File

@@ -19,9 +19,8 @@ const FirebaseDatabaseEvt = new NativeEventEmitter(FirebaseDatabase);
export default class Database extends Base {
constructor(firebase: Object, options: Object = {}) {
super(firebase, options);
this.subscriptions = {};
this.references = {};
this.serverTimeOffset = 0;
this.errorSubscriptions = {};
this.persistenceEnabled = false;
this.namespace = 'firebase:database';
this.transaction = new TransactionHandler(firebase, this, FirebaseDatabaseEvt);
@@ -68,20 +67,12 @@ export default class Database extends Base {
* @param errorCb
* @returns {*}
*/
on(path: string, modifiersString: string, modifiers: Array<string>, eventName: string, cb: () => void, errorCb: () => void) {
const handle = this._handle(path, modifiersString);
this.log.debug('adding on listener', handle);
if (!this.subscriptions[handle]) this.subscriptions[handle] = {};
if (!this.subscriptions[handle][eventName]) this.subscriptions[handle][eventName] = [];
this.subscriptions[handle][eventName].push(cb);
if (errorCb) {
if (!this.errorSubscriptions[handle]) this.errorSubscriptions[handle] = [];
this.errorSubscriptions[handle].push(errorCb);
}
return promisify('on', FirebaseDatabase)(path, modifiersString, modifiers, eventName);
on(ref: Reference, listener: DatabaseListener) {
const { refId, path, query } = ref;
const { listenerId, eventName } = listener;
this.log.debug('on() : ', ref.refId, listenerId, eventName);
this.references[refId] = ref;
return promisify('on', FirebaseDatabase)(refId, path, query.getModifiers(), listenerId, eventName);
}
/**
@@ -92,49 +83,30 @@ export default class Database extends Base {
* @param origCB
* @returns {*}
*/
off(path: string, modifiersString: string, eventName?: string, origCB?: () => void) {
const handle = this._handle(path, modifiersString);
this.log.debug('off() : ', handle, eventName);
off(refId: number, listeners: Array<DatabaseListener>, remainingListenersCount: number) {
this.log.debug('off() : ', refId, listeners);
if (!this.subscriptions[handle] || (eventName && !this.subscriptions[handle][eventName])) {
this.log.warn('off() called, but not currently listening at that location (bad path)', handle, eventName);
return Promise.resolve();
}
// Delete the reference if there are no more listeners
if (remainingListenersCount === 0) delete this.references[refId];
if (eventName && origCB) {
const i = this.subscriptions[handle][eventName].indexOf(origCB);
if (listeners.length === 0) return Promise.resolve();
if (i === -1) {
this.log.warn('off() called, but the callback specified is not listening at that location (bad path)', handle, eventName);
return Promise.resolve();
}
this.subscriptions[handle][eventName].splice(i, 1);
if (this.subscriptions[handle][eventName].length > 0) return Promise.resolve();
} else if (eventName) {
this.subscriptions[handle][eventName] = [];
} else {
this.subscriptions[handle] = {};
}
this.errorSubscriptions[handle] = [];
return promisify('off', FirebaseDatabase)(path, modifiersString, eventName);
return promisify('off', FirebaseDatabase)(refId, listeners.map(listener => ({
listenerId: listener.listenerId,
eventName: listener.eventName,
})));
}
/**
* Removes all event handlers and their native subscriptions
* Removes all references and their native listeners
* @returns {Promise.<*>}
*/
cleanup() {
const promises = [];
Object.keys(this.subscriptions).forEach((handle) => {
Object.keys(this.subscriptions[handle]).forEach((eventName) => {
const separator = handle.indexOf('|');
const path = handle.substring(0, separator);
const modifiersString = handle.substring(separator + 1);
promises.push(this.off(path, modifiersString, eventName));
});
Object.keys(this.references).forEach((refId) => {
const ref = this.references[refId];
promises.push(this.off(refId, ref.listeners, 0));
});
return Promise.all(promises);
}
@@ -169,18 +141,6 @@ export default class Database extends Base {
return Promise.reject({ status: 'Already enabled' });
}
/**
*
* @param path
* @param modifiersString
* @returns {string}
* @private
*/
_handle(path: string = '', modifiersString: string = '') {
return `${path}|${modifiersString}`;
}
/**
*
* @param event
@@ -188,18 +148,14 @@ export default class Database extends Base {
*/
_handleDatabaseEvent(event: Object) {
const body = event.body || {};
const { path, modifiersString, eventName, snapshot } = body;
const handle = this._handle(path, modifiersString);
this.log.debug('_handleDatabaseEvent: ', handle, eventName, snapshot && snapshot.key);
if (this.subscriptions[handle] && this.subscriptions[handle][eventName]) {
this.subscriptions[handle][eventName].forEach((cb) => {
cb(new Snapshot(new Reference(this, path, modifiersString.split('|')), snapshot), body);
});
const { refId, listenerId, path, eventName, snapshot } = body;
this.log.debug('_handleDatabaseEvent: ', refId, listenerId, path, eventName, snapshot && snapshot.key);
if (this.references[refId] && this.references[refId].listeners[listenerId]) {
const cb = this.references[refId].listeners[listenerId].successCallback;
cb(new Snapshot(this.references[refId], snapshot));
} else {
FirebaseDatabase.off(path, modifiersString, eventName, () => {
this.log.debug('_handleDatabaseEvent: No JS listener registered, removed native listener', handle, eventName);
FirebaseDatabase.off(refId, [{ listenerId, eventName }], () => {
this.log.debug('_handleDatabaseEvent: No JS listener registered, removed native listener', refId, listenerId, eventName);
});
}
}
@@ -235,13 +191,15 @@ export default class Database extends Base {
* @private
*/
_handleDatabaseError(error: Object = {}) {
const { path, modifiers } = error;
const handle = this._handle(path, modifiers);
const { refId, listenerId, path } = error;
const firebaseError = this._toFirebaseError(error);
this.log.debug('_handleDatabaseError ->', handle, 'database_error', error);
this.log.debug('_handleDatabaseError ->', refId, listenerId, path, 'database_error', error);
if (this.errorSubscriptions[handle]) this.errorSubscriptions[handle].forEach(listener => listener(firebaseError));
if (this.references[refId] && this.references[refId].listeners[listenerId]) {
const failureCb = this.references[refId].listeners[listenerId].failureCallback;
if (failureCb) failureCb(firebaseError);
}
}
}
@@ -250,4 +208,3 @@ export const statics = {
TIMESTAMP: FirebaseDatabase.serverValueTimestamp || { '.sv': 'timestamp' },
},
};

View File

@@ -9,47 +9,41 @@ import Reference from './reference.js';
* @class Query
*/
export default class Query extends ReferenceBase {
static ref: Reference;
modifiers: Array<DatabaseModifier>;
static modifiers: Array<string>;
ref: Reference;
constructor(ref: Reference, path: string, existingModifiers?: Array<string>) {
constructor(ref: Reference, path: string, existingModifiers?: Array<DatabaseModifier>) {
super(ref.database, path);
this.log.debug('creating Query ', path, existingModifiers);
this.ref = ref;
this.modifiers = existingModifiers ? [...existingModifiers] : [];
}
setOrderBy(name: string, key?: string) {
if (key) {
this.modifiers.push(`${name}:${key}`);
} else {
this.modifiers.push(name);
}
orderBy(name: string, key?: string) {
this.modifiers.push({
type: 'orderBy',
name,
key,
});
}
setLimit(name: string, limit: number) {
this.modifiers.push(`${name}:${limit}`);
limit(name: string, limit: number) {
this.modifiers.push({
type: 'limit',
name,
limit,
});
}
setFilter(name: string, value: any, key?:string) {
if (key) {
this.modifiers.push(`${name}:${value}:${typeof value}:${key}`);
} else {
this.modifiers.push(`${name}:${value}:${typeof value}`);
}
filter(name: string, value: any, key?:string) {
this.modifiers.push({
type: 'filter',
name,
value,
valueType: typeof value,
key,
});
}
getModifiers(): Array<string> {
getModifiers(): Array<DatabaseModifier> {
return [...this.modifiers];
}
getModifiersString(): string {
if (!this.modifiers || !Array.isArray(this.modifiers)) {
return '';
}
return this.modifiers.join('|');
}
}

View File

@@ -10,6 +10,8 @@ import { ReferenceBase } from './../base';
import { promisify, isFunction, isObject, tryJSONParse, tryJSONStringify, generatePushID } from './../../utils';
const FirebaseDatabase = NativeModules.RNFirebaseDatabase;
// Unique Reference ID for native events
let refId = 1;
/**
* @link https://firebase.google.com/docs/reference/js/firebase.database.Reference
@@ -17,15 +19,19 @@ const FirebaseDatabase = NativeModules.RNFirebaseDatabase;
*/
export default class Reference extends ReferenceBase {
refId: number;
listeners: { [listenerId: number]: DatabaseListener };
database: FirebaseDatabase;
query: Query;
constructor(database: FirebaseDatabase, path: string, existingModifiers?: Array<string>) {
constructor(database: FirebaseDatabase, path: string, existingModifiers?: Array<DatabaseModifier>) {
super(database.firebase, path);
this.refId = refId++;
this.listeners = {};
this.database = database;
this.namespace = 'firebase:db:ref';
this.query = new Query(this, path, existingModifiers);
this.log.debug('Created new Reference', this.database._handle(path, existingModifiers));
this.log.debug('Created new Reference', this.refId, this.path);
}
/**
@@ -81,7 +87,6 @@ export default class Reference extends ReferenceBase {
const path = this.path;
const _value = this._serializeAnyType(value);
return promisify('push', FirebaseDatabase)(path, _value)
.then(({ ref }) => {
const newRef = new Reference(this.database, ref);
@@ -95,36 +100,37 @@ export default class Reference extends ReferenceBase {
/**
*
* @param eventType
* @param eventName
* @param successCallback
* @param failureCallback
* @param context TODO
* @returns {*}
*/
on(eventType: string, successCallback: () => any, failureCallback: () => any) {
on(eventName: string, successCallback: () => any, failureCallback: () => any) {
if (!isFunction(successCallback)) throw new Error('The specified callback must be a function');
if (failureCallback && !isFunction(failureCallback)) throw new Error('The specified error callback must be a function');
const path = this.path;
const modifiers = this.query.getModifiers();
const modifiersString = this.query.getModifiersString();
this.log.debug('adding reference.on', path, modifiersString, eventType);
this.database.on(path, modifiersString, modifiers, eventType, successCallback, failureCallback);
this.log.debug('adding reference.on', this.refId, eventName);
const listener = {
listenerId: Object.keys(this.listeners).length + 1,
eventName,
successCallback,
failureCallback,
};
this.listeners[listener.listenerId] = listener;
this.database.on(this, listener);
return successCallback;
}
/**
*
* @param eventType
* @param eventName
* @param successCallback
* @param failureCallback
* @param context TODO
* @returns {Promise.<TResult>}
*/
once(eventType: string = 'value', successCallback: (snapshot: Object) => void, failureCallback: (error: Error) => void) {
const path = this.path;
const modifiers = this.query.getModifiers();
const modifiersString = this.query.getModifiersString();
return promisify('once', FirebaseDatabase)(path, modifiersString, modifiers, eventType)
once(eventName: string = 'value', successCallback: (snapshot: Object) => void, failureCallback: (error: FirebaseError) => void) {
return promisify('once', FirebaseDatabase)(this.refId, this.path, this.query.getModifiers(), eventName)
.then(({ snapshot }) => new Snapshot(this, snapshot))
.then((snapshot) => {
if (isFunction(successCallback)) successCallback(snapshot);
@@ -139,15 +145,35 @@ export default class Reference extends ReferenceBase {
/**
*
* @param eventType
* @param eventName
* @param origCB
* @returns {*}
*/
off(eventType?: string = '', origCB?: () => any) {
const path = this.path;
const modifiersString = this.query.getModifiersString();
this.log.debug('ref.off(): ', path, modifiersString, eventType);
return this.database.off(path, modifiersString, eventType, origCB);
off(eventName?: string = '', origCB?: () => any) {
this.log.debug('ref.off(): ', this.refId, eventName);
let listenersToRemove;
if (eventName && origCB) {
listenersToRemove = Object.values(this.listeners).filter((listener) => {
return listener.eventName === eventName && listener.successCallback === origCB;
});
// Only remove a single listener as per the web spec
if (listenersToRemove.length > 1) listenersToRemove = [listenersToRemove[0]];
} else if (eventName) {
listenersToRemove = Object.values(this.listeners).filter((listener) => {
return listener.eventName === eventName;
});
} else if (origCB) {
listenersToRemove = Object.values(this.listeners).filter((listener) => {
return listener.successCallback === origCB;
});
} else {
listenersToRemove = Object.values(this.listeners);
}
// Remove the listeners from the reference to prevent memory leaks
listenersToRemove.forEach((listener) => {
delete this.listeners[listener.listenerId];
});
return this.database.off(this.refId, listenersToRemove, Object.keys(this.listeners).length);
}
/**
@@ -227,7 +253,7 @@ export default class Reference extends ReferenceBase {
*/
orderBy(name: string, key?: string): Reference {
const newRef = new Reference(this.database, this.path, this.query.getModifiers());
newRef.query.setOrderBy(name, key);
newRef.query.orderBy(name, key);
return newRef;
}
@@ -261,7 +287,7 @@ export default class Reference extends ReferenceBase {
*/
limit(name: string, limit: number): Reference {
const newRef = new Reference(this.database, this.path, this.query.getModifiers());
newRef.query.setLimit(name, limit);
newRef.query.limit(name, limit);
return newRef;
}
@@ -308,7 +334,7 @@ export default class Reference extends ReferenceBase {
*/
filter(name: string, value: any, key?: string): Reference {
const newRef = new Reference(this.database, this.path, this.query.getModifiers());
newRef.query.setFilter(name, value, key);
newRef.query.filter(name, value, key);
return newRef;
}