[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

@@ -25,8 +25,8 @@ const NATIVE_MODULE_EVENTS = {
'onAuthStateChanged',
],
Database: [
'database_on_event',
'database_cancel_event',
// 'database_on_event',
// 'database_cancel_event',
'database_transaction_event',
// 'database_server_offset', // TODO
],
@@ -47,11 +47,11 @@ export default class ModuleBase {
* @param withEventEmitter
*/
constructor(firebaseApp, options, moduleName, withEventEmitter = false) {
this._options = Object.assign({}, DEFAULTS[moduleName] || {}, options);
this._module = moduleName;
this._firebaseApp = firebaseApp;
this._appName = firebaseApp._name;
this._namespace = `${this._appName}:${this._module}`;
this._options = Object.assign({}, DEFAULTS[moduleName] || {}, options);
// check if native module exists as all native
// modules are now optionally part of build

277
lib/utils/SyncTree.js Normal file
View File

@@ -0,0 +1,277 @@
import { NativeEventEmitter } from 'react-native';
import INTERNALS from './../internals';
import DatabaseSnapshot from './../modules/database/snapshot';
import DatabaseReference from './../modules/database/reference';
import { isString, nativeToJSError } from './../utils';
type Registration = {
key: String,
path: String,
once?: Boolean,
appName: String,
eventType: String,
registration: String,
ref: DatabaseReference,
}
/**
* Internally used to manage firebase database realtime event
* subscriptions and keep the listeners in sync in js vs native.
*/
export default class SyncTree {
constructor(databaseNative) {
this._tree = {};
this._reverseLookup = {};
this._databaseNative = databaseNative;
this._nativeEmitter = new NativeEventEmitter(databaseNative);
this._nativeEmitter.addListener(
'database_sync_event',
this._handleSyncEvent.bind(this),
);
}
/**
*
* @param event
* @private
*/
_handleSyncEvent(event) {
if (event.error) {
this._handleErrorEvent(event);
} else {
this._handleValueEvent(event);
}
}
/**
* Routes native database 'on' events to their js equivalent counterpart.
* If there is no longer any listeners remaining for this event we internally
* call the native unsub method to prevent further events coming through.
*
* @param event
* @private
*/
_handleValueEvent(event) {
const { eventRegistrationKey } = event.registration;
const registration = this.getRegistration(eventRegistrationKey);
if (!registration) {
// registration previously revoked
// notify native that the registration
// no longer exists so it can remove
// the native listeners
return this._databaseNative.off({
key: event.key,
eventRegistrationKey,
});
}
const { snapshot, previousChildName } = event.data;
// forward on to users .on(successCallback <-- listener
return INTERNALS.SharedEventEmitter.emit(
eventRegistrationKey,
new DatabaseSnapshot(registration.ref, snapshot),
previousChildName,
);
}
/**
* Routes native database query listener cancellation events to their js counterparts.
*
* @param event
* @private
*/
_handleErrorEvent(event) {
const { code, message } = event.error;
const { eventRegistrationKey, registrationCancellationKey } = event.registration;
const registration = this.getRegistration(registrationCancellationKey);
if (registration) {
// build a new js error - we additionally attach
// the ref as a property for easier debugging
const error = nativeToJSError(code, message, { ref: registration.ref });
// forward on to users .on(successCallback, cancellationCallback <-- listener
INTERNALS.SharedEventEmitter.emit(registrationCancellationKey, error);
// remove the paired event registration - if we received a cancellation
// event then it's guaranteed that they'll be no further value events
this.removeRegistration(eventRegistrationKey);
}
}
/**
* Returns registration information such as appName, ref, path and registration keys.
*
* @param registration
* @return {null}
*/
getRegistration(registration): Registration | null {
return this._reverseLookup[registration] ? Object.assign({}, this._reverseLookup[registration]) : null;
}
/**
* Removes all listeners for the specified registration keys.
*
* @param registrations
* @return {number}
*/
removeListenersForRegistrations(registrations) {
if (isString(registrations)) {
this.removeRegistration(registrations);
INTERNALS.SharedEventEmitter.removeAllListeners(registrations);
return 1;
}
if (!Array.isArray(registrations)) return 0;
for (let i = 0, len = registrations.length; i < len; i++) {
this.removeRegistration(registrations[i]);
INTERNALS.SharedEventEmitter.removeAllListeners(registrations[i]);
}
return registrations.length;
}
/**
* Removes a specific listener from the specified registrations.
*
* @param listener
* @param registrations
* @return {Array} array of registrations removed
*/
removeListenerRegistrations(listener, registrations) {
if (!Array.isArray(registrations)) return [];
const removed = [];
for (let i = 0, len = registrations.length; i < len; i++) {
const registration = registrations[i];
const subscriptions = INTERNALS.SharedEventEmitter._subscriber.getSubscriptionsForType(registration);
if (subscriptions) {
for (let j = 0, l = subscriptions.length; j < l; j++) {
const subscription = subscriptions[j];
// The subscription may have been removed during this event loop.
// its listener matches the listener in method parameters
if (subscription && subscription.listener === listener) {
subscription.remove();
removed.push(registration);
this.removeRegistration(registration);
}
}
}
}
return removed;
}
/**
* Returns an array of all registration keys for the specified path.
*
* @param path
* @return {Array}
*/
getRegistrationsByPath(path): Array {
const out = [];
const eventKeys = Object.keys(this._tree[path] || {});
for (let i = 0, len = eventKeys.length; i < len; i++) {
Array.prototype.push.apply(out, Object.keys(this._tree[path][eventKeys[i]]));
}
return out;
}
/**
* Returns an array of all registration keys for the specified path and eventType.
*
* @param path
* @param eventType
* @return {Array}
*/
getRegistrationsByPathEvent(path, eventType): Array {
if (!this._tree[path]) return [];
if (!this._tree[path][eventType]) return [];
return Object.keys(this._tree[path][eventType]);
}
/**
* Register a new listener.
*
* @param parameters
* @param listener
* @return {String}
*/
addRegistration(parameters: Registration, listener): String {
const { path, eventType, registration, once } = parameters;
if (!this._tree[path]) this._tree[path] = {};
if (!this._tree[path][eventType]) this._tree[path][eventType] = {};
this._tree[path][eventType][registration] = 0;
this._reverseLookup[registration] = Object.assign({}, parameters);
if (once) INTERNALS.SharedEventEmitter.once(registration, this._onOnceRemoveRegistration(registration, listener));
else INTERNALS.SharedEventEmitter.addListener(registration, listener);
return registration;
}
/**
* Remove a registration, if it's not a `once` registration then instructs native
* to also remove the underlying database query listener.
*
* @param registration
* @return {boolean}
*/
removeRegistration(registration: String): Boolean {
if (!this._reverseLookup[registration]) return false;
const { path, eventType, once } = this._reverseLookup[registration];
if (!this._tree[path]) {
delete this._reverseLookup[registration];
return false;
}
if (!this._tree[path][eventType]) {
delete this._reverseLookup[registration];
return false;
}
// we don't want `once` events to notify native as they're already
// automatically unsubscribed on native when the first event is sent
const registrationObj = this._reverseLookup[registration];
if (registrationObj && !once) {
this._databaseNative.off({
key: registrationObj.key,
eventType: registrationObj.eventType,
eventRegistrationKey: registration,
});
}
delete this._tree[path][eventType][registration];
delete this._reverseLookup[registration];
return !!registrationObj;
}
/**
* Wraps a `once` listener with a new function that self de-registers.
*
* @param registration
* @param listener
* @return {function(...[*])}
* @private
*/
_onOnceRemoveRegistration(registration, listener) {
return (...args) => {
this.removeRegistration(registration);
listener(...args);
};
}
}

View File

@@ -231,6 +231,14 @@ export function map(
}, () => cb && cb(result));
}
/**
*
* @param string
* @return {string}
*/
export function capitalizeFirstLetter(string: String) {
return `${string.charAt(0).toUpperCase()}${string.slice(1)}`;
}
// timestamp of last push, used to prevent local collisions if you push twice in one ms.
let lastPushTime = 0;
@@ -319,6 +327,11 @@ export function nativeWithApp(appName, NativeModule) {
return native;
}
/**
*
* @param object
* @return {string}
*/
export function objectToUniqueId(object: Object): String {
if (!isObject(object) || object === null) return JSON.stringify(object);