mirror of
https://github.com/zhigang1992/react-native-firebase.git
synced 2026-04-23 20:10:05 +08:00
[js][android] database sync tree implementation with adjusted tests
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user