[database][wip] on/off logic refactor - heavily still wip so things still to change

This commit is contained in:
Salakar
2017-08-14 11:05:49 +01:00
parent 0675aa076d
commit f1709970e9
13 changed files with 574 additions and 459 deletions

View File

@@ -5,6 +5,7 @@
import { NativeModules } from 'react-native';
import Reference from './reference';
import Snapshot from './snapshot';
import TransactionHandler from './transaction';
import ModuleBase from './../../utils/ModuleBase';
@@ -14,6 +15,7 @@ import ModuleBase from './../../utils/ModuleBase';
export default class Database extends ModuleBase {
constructor(firebaseApp: Object, options: Object = {}) {
super(firebaseApp, options, 'Database', true);
this._references = {};
this._serverTimeOffset = 0;
this._transactionHandler = new TransactionHandler(this);
@@ -21,9 +23,49 @@ export default class Database extends ModuleBase {
this._native.setPersistence(this._options.persistence);
}
// todo event & error listeners
// todo serverTimeOffset event/listener - make ref natively and switch to events
// todo use nativeToJSError for on/off error events
this.addListener(
this._getAppEventName('database_cancel_event'),
this._handleCancelEvent.bind(this),
);
this.addListener(
this._getAppEventName('database_on_event'),
this._handleOnEvent.bind(this),
);
}
_handleOnEvent(event) {
console.log('>>>ON-event>>>', event);
const { queryKey, body, refId } = event;
const { snapshot, previousChildName } = body;
const remainingListeners = this.listeners(queryKey);
if (!remainingListeners || !remainingListeners.length) {
this._database._native.off(
_refId,
queryKey,
);
delete this._references[refId];
} else {
const ref = this._references[refId];
if (!ref) {
this._database._native.off(
_refId,
queryKey,
);
} else {
this.emit(queryKey, new Snapshot(ref, snapshot), previousChildName);
}
}
}
_handleCancelEvent(event) {
console.log('>>>CANCEL-event>>>', event);
}
/**

View File

@@ -3,6 +3,9 @@
*/
import Reference from './reference.js';
import { objectToUniqueId } from './../../utils';
// todo doc methods
/**
* @class Query
@@ -15,6 +18,12 @@ export default class Query {
this._reference = ref;
}
/**
*
* @param name
* @param key
* @return {Reference|*}
*/
orderBy(name: string, key?: string) {
this.modifiers.push({
type: 'orderBy',
@@ -25,6 +34,12 @@ export default class Query {
return this._reference;
}
/**
*
* @param name
* @param limit
* @return {Reference|*}
*/
limit(name: string, limit: number) {
this.modifiers.push({
type: 'limit',
@@ -35,6 +50,13 @@ export default class Query {
return this._reference;
}
/**
*
* @param name
* @param value
* @param key
* @return {Reference|*}
*/
filter(name: string, value: any, key?: string) {
this.modifiers.push({
type: 'filter',
@@ -47,7 +69,27 @@ export default class Query {
return this._reference;
}
/**
*
* @return {[*]}
*/
getModifiers(): Array<DatabaseModifier> {
return [...this.modifiers];
}
/**
*
* @return {*}
*/
queryIdentifier() {
// convert query modifiers array into an object for generating a unique key
const object = {};
for (let i = 0, len = this.modifiers.length; i < len; i++) {
const { name, type, value } = this.modifiers[i];
object[`${type}-${name}`] = value;
}
return objectToUniqueId(object);
}
}

View File

@@ -1,11 +1,21 @@
/**
* @flow
*/
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, isObject, tryJSONParse, tryJSONStringify, generatePushID } from './../../utils';
import {
promiseOrCallback,
isFunction,
isObject,
isString,
tryJSONParse,
tryJSONStringify,
generatePushID,
} from './../../utils';
// Unique Reference ID for native events
let refId = 1;
@@ -61,6 +71,7 @@ export default class Reference extends ReferenceBase {
super(path, database);
this._promise = null;
this._refId = refId++;
this._listeners = 0;
this._refListeners = {};
this._database = database;
this._query = new Query(this, path, existingModifiers);
@@ -491,6 +502,17 @@ export default class Reference extends ReferenceBase {
* INTERNALS
*/
/**
* Generate a unique key based on this refs path and query modifiers
* @return {string}
*/
makeQueryKey() {
return `$${this.path}$${this._query.queryIdentifier()}$${this._refId}$${this._listeners}`;
}
/**
* Return instance of db logger
*/
get log() {
return this._database.log;
}
@@ -540,219 +562,103 @@ export default class Reference extends ReferenceBase {
}
// todo below methods need refactoring
// todo below methods need refactoring
// todo below methods need refactoring
// todo below methods need refactoring
// todo below methods need refactoring
// todo below methods need refactoring
// todo below methods need refactoring
// todo below methods need refactoring
/**
* iOS: Called once with the initial data at the specified location and then once each
* time the data changes. It won't trigger until the entire contents have been
* synchronized.
*
* Android: (@link https://github.com/invertase/react-native-firebase/issues/92)
* - Array & number values: Called once with the initial data at the specified
* location and then twice each time the value changes.
* - Other data types: Called once with the initial data at the specified location
* and once each time the data type changes.
*
* @callback onValueCallback
* @param {!DataSnapshot} dataSnapshot - Snapshot representing data at the location
* specified by the current ref. If location has no data, .val() will return null.
* @param eventType
* @param callback
* @param cancelCallback
* @return {*}
*/
/**
* Called once for each initial child at the specified location and then again
* every time a new child is added.
*
* @callback onChildAddedCallback
* @param {!DataSnapshot} dataSnapshot - Snapshot reflecting the data for the
* relevant child.
* @param {?ReferenceKey} previousChildKey - For ordering purposes, the key
* of the previous sibling child by sort order, or null if it is the first child.
*/
/**
* Called once every time a child is removed.
*
* A child will get removed when either:
* - remove() is explicitly called on a child or one of its ancestors
* - set(null) is called on that child or one of its ancestors
* - a child has all of its children removed
* - there is a query in effect which now filters out the child (because it's sort
* order changed or the max limit was hit)
*
* @callback onChildRemovedCallback
* @param {!DataSnapshot} dataSnapshot - Snapshot reflecting the old data for
* the child that was removed.
*/
/**
* Called when a child (or any of its descendants) changes.
*
* A single child_changed event may represent multiple changes to the child.
*
* @callback onChildChangedCallback
* @param {!DataSnapshot} dataSnapshot - Snapshot reflecting new child contents.
* @param {?ReferenceKey} previousChildKey - For ordering purposes, the key
* of the previous sibling child by sort order, or null if it is the first child.
*/
/**
* Called when a child's sort order changes, i.e. its position relative to its
* siblings changes.
*
* @callback onChildMovedCallback
* @param {!DataSnapshot} dataSnapshot - Snapshot reflecting the data of the moved
* child.
* @param {?ReferenceKey} previousChildKey - For ordering purposes, the key
* of the previous sibling child by sort order, or null if it is the first child.
*/
/**
* @typedef (onValueCallback|onChildAddedCallback|onChildRemovedCallback|onChildChangedCallback|onChildMovedCallback) ReferenceEventCallback
*/
/**
* Called if the event subscription is cancelled because the client does
* not have permission to read this data (or has lost the permission to do so).
*
* @callback onFailureCallback
* @param {Error} error - Object indicating why the failure occurred
*/
/**
* Binds callback handlers to when data changes at the current ref's location.
* The primary method of reading data from a Database.
*
* Callbacks can be unbound using {@link off}.
*
* Event Types:
*
* - value: {@link onValueCallback}.
* - child_added: {@link onChildAddedCallback}
* - child_removed: {@link onChildRemovedCallback}
* - child_changed: {@link onChildChangedCallback}
* - child_moved: {@link onChildMovedCallback}
*
* @param {ReferenceEventType} eventType - Type of event to attach a callback for.
* @param {ReferenceEventCallback} successCallback - Function that will be called
* when the event occurs with the new data.
* @param {onFailureCallback=} failureCallbackOrContext - Optional callback that is called
* if the event subscription fails. {@link onFailureCallback}
* @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}
*/
on(eventType: string, successCallback: () => any, failureCallbackOrContext: () => any, context: any) {
if (!eventType) throw new Error('Error: Query on failed: Was called with 0 arguments. Expects at least 2');
if (!ReferenceEventTypes[eventType]) throw new Error('Query.on failed: First argument must be a valid event type: "value", "child_added", "child_removed", "child_changed", or "child_moved".');
if (!successCallback) throw new Error('Query.on failed: Was called with 1 argument. Expects at least 2.');
if (!isFunction(successCallback)) throw new Error('Query.on failed: Second argument must be a valid function.');
if (arguments.length > 2 && !failureCallbackOrContext) throw new Error('Query.on failed: third argument must either be a cancel callback or a context object.');
let _failureCallback;
let _context;
if (context) {
_context = context;
_failureCallback = failureCallbackOrContext;
} else if (isFunction(failureCallbackOrContext)) {
_failureCallback = failureCallbackOrContext;
} else {
_context = failureCallbackOrContext;
// 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, cancelCallback: () => any): Function {
if (!eventType) {
throw new Error('Query.on failed: Function called with 0 arguments. Expects at least 2.');
}
if (_failureCallback) {
_failureCallback = (error) => {
if (error.message.startsWith('FirebaseError: permission_denied')) {
// eslint-disable-next-line
error.message = `permission_denied at /${this.path}: Client doesn't have permission to access the desired data.`
}
failureCallbackOrContext(error);
};
}
// brb, helping someone
let _successCallback;
if (_context) {
_successCallback = successCallback.bind(_context);
} else {
_successCallback = successCallback;
if (!isString(eventType) || !ReferenceEventTypes[eventType]) {
throw new Error(`Query.on failed: First argument must be a valid string event type: "${Object.keys(ReferenceEventTypes).join(', ')}"`);
}
const listener = {
listenerId: Object.keys(this._refListeners).length + 1,
eventName: eventType,
successCallback: _successCallback,
failureCallback: _failureCallback,
};
if (!callback) {
throw new Error('Query.on failed: Function called with 1 argument. Expects at least 2.');
}
this._refListeners[listener.listenerId] = listener;
this._database.on(this, listener);
return successCallback;
if (!isFunction(callback)) {
throw new Error('Query.on failed: Second argument must be a valid function.');
}
if (cancelCallback && !isFunction(cancelCallback)) {
throw new Error('Query.on failed: Function called with 3 arguments, but third optional argument `cancelCallback` was not a function.');
}
const eventQueryKey = `${this.makeQueryKey()}$${eventType}`;
INTERNALS.SharedEventEmitter.addListener(eventQueryKey, callback);
if (isFunction(cancelCallback)) {
INTERNALS.SharedEventEmitter.once(`${this.makeQueryKey()}:cancelled`, cancelCallback);
}
// initialise the native listener if not already listening
this._database._native.on({
eventType,
eventQueryKey,
id: this._refId, // todo remove
path: this.path,
modifiers: this._query.getModifiers(),
});
if (!this._database._references[this._refId]) {
this._database._references[this._refId] = this;
}
this._listeners = this._listeners + 1;
return callback;
}
/**
* Detaches a callback attached with on().
*
* Calling off() on a parent listener will not automatically remove listeners
* registered on child nodes.
*
* If on() was called multiple times with the same eventType off() must be
* called multiple times to completely remove it.
*
* If a callback is not specified, all callbacks for the specified eventType
* will be removed. If no eventType or callback is specified, all callbacks
* for the Reference will be removed.
*
* If a context is specified, it too is used as a filter parameter: a callback
* will only be detached if, when it was attached with on(), the same event type,
* callback function and context were provided.
*
* If no callbacks matching the parameters provided are found, no callbacks are
* detached.
*
* @param {('value'|'child_added'|'child_changed'|'child_removed'|'child_moved')=} eventType - Type of event to detach callback for.
* @param {Function=} originalCallback - Original callback passed to on()
* TODO @param {*=} context - The context passed to on() when the callback was bound
*
* {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#off}
* @param eventType
* @param originalCallback
*/
off(eventType?: string = '', originalCallback?: () => any) {
// $FlowFixMe
const listeners: Array<DatabaseListener> = Object.values(this._refListeners);
let listenersToRemove;
if (eventType && originalCallback) {
listenersToRemove = listeners.filter((listener) => {
return listener.eventName === eventType && listener.successCallback === originalCallback;
});
// Only remove a single listener as per the web spec
if (listenersToRemove.length > 1) listenersToRemove = [listenersToRemove[0]];
} else if (eventType) {
listenersToRemove = listeners.filter((listener) => {
return listener.eventName === eventType;
});
} else if (originalCallback) {
listenersToRemove = listeners.filter((listener) => {
return listener.successCallback === originalCallback;
});
} else {
listenersToRemove = listeners;
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(', ')}"`);
}
if (originalCallback && !isFunction(originalCallback)) {
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}`;
if (originalCallback) {
INTERNALS.SharedEventEmitter.removeListener(eventQueryKey, originalCallback);
} else {
INTERNALS.SharedEventEmitter.removeAllListeners(eventQueryKey);
INTERNALS.SharedEventEmitter.removeAllListeners(`${this.makeQueryKey()}: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 {
}
// Remove the listeners from the reference to prevent memory leaks
listenersToRemove.forEach((listener) => {
delete this._refListeners[listener.listenerId];
});
return this._database.off(this._refId, listenersToRemove, Object.keys(this._refListeners).length);
}