mirror of
https://github.com/zhigang1992/react-native-firebase.git
synced 2026-04-23 12:06:47 +08:00
[database][wip] on/off logic refactor - heavily still wip so things still to change
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user