[js][android] database sync tree implementation with adjusted tests

This commit is contained in:
Salakar
2017-08-15 21:29:50 +01:00
parent 67985f8e90
commit e4d27029b9
14 changed files with 647 additions and 233 deletions

View File

@@ -5,8 +5,8 @@
import Query from './query.js';
import Snapshot from './snapshot';
import Disconnect from './disconnect';
import INTERNALS from './../../internals';
import ReferenceBase from './../../utils/ReferenceBase';
import {
promiseOrCallback,
isFunction,
@@ -17,8 +17,9 @@ import {
generatePushID,
} from './../../utils';
// Unique Reference ID for native events
let refId = 1;
import INTERNALS from './../../internals';
// track all event registrations by path
/**
* Enum for event types
@@ -62,7 +63,6 @@ const ReferenceEventTypes = {
*/
export default class Reference extends ReferenceBase {
_refId: number;
_refListeners: { [listenerId: number]: DatabaseListener };
_database: Object;
_query: Query;
@@ -70,13 +70,12 @@ export default class Reference extends ReferenceBase {
constructor(database: Object, path: string, existingModifiers?: Array<DatabaseModifier>) {
super(path, database);
this._promise = null;
this._refId = refId++;
this._listeners = 0;
this._refListeners = {};
this._database = database;
this._query = new Query(this, path, existingModifiers);
this.log = this._database.log;
this.log.debug('Created new Reference', this._refId, this.path);
this.log.debug('Created new Reference', this._getRefKey());
}
/**
@@ -90,7 +89,7 @@ export default class Reference extends ReferenceBase {
* @returns {*}
*/
keepSynced(bool: boolean) {
return this._database._native.keepSynced(this._refId, this.path, this._query.getModifiers(), bool);
return this._database._native.keepSynced(this._getRefKey(), this.path, this._query.getModifiers(), bool);
}
/**
@@ -225,7 +224,7 @@ export default class Reference extends ReferenceBase {
cancelOrContext: (error: FirebaseError) => void,
context?: Object,
) {
return this._database._native.once(this._refId, this.path, this._query.getModifiers(), eventName)
return this._database._native.once(this._getRefKey(), this.path, this._query.getModifiers(), eventName)
.then(({ snapshot }) => {
const _snapshot = new Snapshot(this, snapshot);
@@ -513,11 +512,23 @@ export default class Reference extends ReferenceBase {
*/
/**
* Generate a unique key based on this refs path and query modifiers
* Generate a unique registration key.
*
* @return {string}
*/
makeQueryKey() {
return `$${this.path}$${this._query.queryIdentifier()}$${this._refId}$${this._listeners}`;
_getRegistrationKey(eventType) {
return `$${this._database._appName}$${this.path}$${this._query.queryIdentifier()}$${this._listeners}$${eventType}`;
}
/**
* Generate a string that uniquely identifies this
* combination of path and query modifiers
*
* @return {string}
* @private
*/
_getRefKey() {
return `$${this._database._appName}$${this.path}$${this._query.queryIdentifier()}`;
}
/**
@@ -571,17 +582,32 @@ export default class Reference extends ReferenceBase {
};
}
/**
* Register a listener for data changes at the current ref's location.
* The primary method of reading data from a Database.
*
* @param eventType
* @param callback
* @param cancelCallbackOrContext
* @param context
* @return {*}
* Listeners can be unbound using {@link off}.
*
* Event Types:
*
* - value: {@link callback}.
* - child_added: {@link callback}
* - child_removed: {@link callback}
* - child_changed: {@link callback}
* - child_moved: {@link callback}
*
* @param {ReferenceEventType} eventType - Type of event to attach a callback for.
* @param {ReferenceEventCallback} callback - Function that will be called
* when the event occurs with the new data.
* @param {cancelCallbackOrContext=} cancelCallbackOrContext - Optional callback that is called
* if the event subscription fails. {@link cancelCallbackOrContext}
* @param {*=} context - Optional object to bind the callbacks to when calling them.
* @returns {ReferenceEventCallback} callback function, unmodified (unbound), for
* convenience if you want to pass an inline function to on() and store it later for
* removing using off().
*
* {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#on}
*/
// todo:context shouldn't be needed - confirm
// todo refId should no longer be required - update native to work without it then remove from js internals
on(eventType: string, callback: () => any, cancelCallbackOrContext?: () => any, context?: Object): Function {
if (!eventType) {
throw new Error('Query.on failed: Function called with 0 arguments. Expects at least 2.');
@@ -607,38 +633,88 @@ export default class Reference extends ReferenceBase {
throw new Error('Query.on failed: Function called with 4 arguments, but third optional argument `cancelCallbackOrContext` was not a function.');
}
const eventQueryKey = `${this.makeQueryKey()}$${eventType}`;
const eventRegistrationKey = this._getRegistrationKey(eventType);
const registrationCancellationKey = `${eventRegistrationKey}$cancelled`;
const _context = (cancelCallbackOrContext && !isFunction(cancelCallbackOrContext)) ? cancelCallbackOrContext : context;
INTERNALS.SharedEventEmitter.addListener(eventQueryKey, _context ? callback.bind(_context) : callback);
this._syncTree.addRegistration(
{
eventType,
ref: this,
path: this.path,
key: this._getRefKey(),
appName: this._database._appName,
registration: eventRegistrationKey,
},
_context ? callback.bind(_context) : callback,
);
if (isFunction(cancelCallbackOrContext)) {
INTERNALS.SharedEventEmitter.once(`${eventQueryKey}:cancelled`, _context ? cancelCallbackOrContext.bind(_context) : cancelCallbackOrContext);
// cancellations have their own separate registration
// as these are one off events, and they're not guaranteed
// to occur either, only happens on failure to register on native
this._syncTree.addRegistration(
{
ref: this,
once: true,
path: this.path,
key: this._getRefKey(),
appName: this._database._appName,
eventType: `${eventType}$cancelled`,
registration: registrationCancellationKey,
},
_context ? cancelCallbackOrContext.bind(_context) : cancelCallbackOrContext,
);
}
// initialise the native listener if not already listening
this._database._native.on({
eventType,
eventQueryKey,
id: this._refId, // todo remove
path: this.path,
key: this._getRefKey(),
appName: this._database._appName,
modifiers: this._query.getModifiers(),
registration: {
eventRegistrationKey,
registrationCancellationKey,
hasCancellationCallback: isFunction(cancelCallbackOrContext),
},
});
if (!this._database._references[this._refId]) {
this._database._references[this._refId] = this;
}
// increment number of listeners - just s short way of making
// every registration unique per .on() call
this._listeners = this._listeners + 1;
// return original unbound successCallback for
// the purposes of calling .off(eventType, callback) at a later date
return callback;
}
/**
* Detaches a callback previously attached with on().
*
* Detach a callback previously attached with on(). Note that if on() was called
* multiple times with the same eventType and callback, the callback will be called
* multiple times for each event, and off() must be called multiple times to
* remove the callback. Calling off() on a parent listener will not automatically
* remove listeners registered on child nodes, off() must also be called on any
* child listeners to remove the callback.
*
* If a callback is not specified, all callbacks for the specified eventType will be removed.
* Similarly, if no eventType or callback is specified, all callbacks for the Reference will be removed.
* @param eventType
* @param originalCallback
*/
off(eventType?: string = '', originalCallback?: () => any) {
if (!arguments.length) {
// Firebase Docs:
// if no eventType or callback is specified, all callbacks for the Reference will be removed.
return this._syncTree.removeListenersForRegistrations(this._syncTree.getRegistrationsByPath(this.path));
}
/*
* VALIDATE ARGS
*/
if (eventType && (!isString(eventType) || !ReferenceEventTypes[eventType])) {
throw new Error(`Query.off failed: First argument must be a valid string event type: "${Object.keys(ReferenceEventTypes).join(', ')}"`);
}
@@ -647,34 +723,36 @@ export default class Reference extends ReferenceBase {
throw new Error('Query.off failed: Function called with 2 arguments, but second optional argument was not a function.');
}
if (eventType) {
const eventQueryKey = `${this.makeQueryKey()}$${eventType}`;
// Firebase Docs:
// Note that if on() was called
// multiple times with the same eventType and callback, the callback will be called
// multiple times for each event, and off() must be called multiple times to
// remove the callback.
// Remove only a single registration
if (eventType && originalCallback) {
const registrations = this._syncTree.getRegistrationsByPathEvent(this.path, eventType);
if (originalCallback) {
INTERNALS.SharedEventEmitter.removeListener(eventQueryKey, originalCallback);
} else {
INTERNALS.SharedEventEmitter.removeAllListeners(eventQueryKey);
INTERNALS.SharedEventEmitter.removeAllListeners(`${this.makeQueryKey()}:cancelled`);
}
// remove the paired cancellation registration if any exist
this._syncTree.removeListenersForRegistrations([`${registrations[0]}$cancelled`]);
// check if there's any listeners remaining in the js thread
// if there's isn't then call the native .off method which
// will unsubscribe from the native firebase listeners
const remainingListeners = INTERNALS.SharedEventEmitter.listeners(eventQueryKey);
if (!remainingListeners || !remainingListeners.length) {
this._database._native.off(
this._refId, // todo remove
eventQueryKey,
);
// remove straggling cancellation listeners
INTERNALS.SharedEventEmitter.removeAllListeners(`${this.makeQueryKey()}:cancelled`);
}
} else {
// todo remove all associated event subs if no event type && no orignalCb
// remove only the first registration to match firebase web sdk
// call multiple times to remove multiple registrations
return this._syncTree.removeListenerRegistrations(originalCallback, [registrations[0]]);
}
// Firebase Docs:
// If a callback is not specified, all callbacks for the specified eventType will be removed.
const registrations = this._syncTree.getRegistrationsByPathEvent(this.path, eventType);
this._syncTree.removeListenersForRegistrations(
this._syncTree.getRegistrationsByPathEvent(this.path, `${eventType}$cancelled`),
);
return this._syncTree.removeListenersForRegistrations(registrations);
}
get _syncTree() {
return INTERNALS.SyncTree;
}
}