@react-native-firebase/database
Version:
React Native Firebase - The Firebase Realtime Database is a cloud-hosted database. Data is stored as JSON and synchronized in realtime to every connected client. React Native Firebase provides native integration with the Android & iOS Firebase SDKs, suppo
373 lines (322 loc) • 11.4 kB
JavaScript
/*
* Copyright (c) 2016-present Invertase Limited & Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this library except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import { isString } from '@react-native-firebase/app/lib/common';
import { getReactNativeModule } from '@react-native-firebase/app/lib/internal/nativeModule';
import NativeError from '@react-native-firebase/app/lib/internal/NativeFirebaseError';
import SharedEventEmitter from '@react-native-firebase/app/lib/internal/SharedEventEmitter';
import DatabaseDataSnapshot from './DatabaseDataSnapshot';
class DatabaseSyncTree {
constructor() {
this._tree = {};
this._reverseLookup = {};
// we need to be able to register multiple listeners for a single event,
// *then delete a specific single listener for that event*, so we have to
// be able to identify specific listeners for removal which means we need
// to mirror the private registration accounting from upstream EventEmitter
// so we have access here and can do our single emitter removal
// This is a map of emitter-key <-> set of listener registrations
// The listener registrations have { context, listener, remove() }
this._registry = {};
SharedEventEmitter.addListener('database_sync_event', this._handleSyncEvent.bind(this));
}
get native() {
return getReactNativeModule('RNFBDatabaseQueryModule');
}
// from upstream EventEmitter: initialize registrations for an emitter key
_allocate(registry, eventType) {
let registrations = registry[eventType];
if (registrations == null) {
registrations = new Set();
registry[eventType] = registrations;
}
return registrations;
}
/**
* Handles an incoming event from native
* @param event
* @private
*/
_handleSyncEvent(event) {
const { body } = event;
if (body.error) {
this._handleErrorEvent(body);
} else {
this._handleValueEvent(body);
}
}
/**
* Routes native database query listener cancellation events to their js counterparts.
*
* @param event
* @private
*/
_handleErrorEvent(event) {
// console.log('SyncTree.ERROR >>>', event);
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 = NativeError.fromEvent(event.error, 'database');
// forward on to users .on(successCallback, cancellationCallback <-- listener
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);
}
}
/**
* Routes native database 'on' events to their js equivalent counterpart.
* If t 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) {
// console.log('SyncTree.VALUE >>>', event);
const { key, eventRegistrationKey } = event.registration;
const registration = this.getRegistration(eventRegistrationKey);
// console.log('SyncTree.registration >>>', registration);
if (!registration) {
// registration previously revoked
// notify native that the registration
// no longer exists so it can remove
// the native listeners
return this.native.off(key, eventRegistrationKey);
}
let snapshot;
let previousChildName;
// Value events don't return a previousChildName
if (event.eventType === 'value') {
snapshot = new DatabaseDataSnapshot(registration.ref, event.data);
} else {
snapshot = new DatabaseDataSnapshot(registration.ref, event.data.snapshot);
previousChildName = event.data.previousChildName;
}
// forward on to users .on(successCallback <-- listener
return SharedEventEmitter.emit(eventRegistrationKey, snapshot, previousChildName);
}
/**
* Returns registration information such as appName, ref, path and registration keys.
*
* @param registration
* @return {null}
*/
getRegistration(registration) {
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);
SharedEventEmitter.removeAllListeners(registrations);
// mirror upstream accounting - clear out all registrations for this key
if (registrations == null) {
this._registry = {};
} else {
delete this._registry[registrations];
}
return 1;
}
if (!Array.isArray(registrations)) {
return 0;
}
for (let i = 0, len = registrations.length; i < len; i++) {
this.removeRegistration(registrations[i]);
SharedEventEmitter.removeAllListeners(registrations[i]);
// mirror upstream accounting - clear out all registrations for this key
if (registrations[i] == null) {
this._registry = {};
} else {
delete this._registry[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];
let subscriptions;
// get all registrations for this key so we can find our specific listener
const registrySubscriptionsSet = this._registry[registration];
if (registrySubscriptionsSet) {
subscriptions = Array.from(registrySubscriptionsSet);
}
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) {
this._registry[registration].delete(subscription);
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) {
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) {
if (!this._tree[path]) {
return [];
}
if (!this._tree[path][eventType]) {
return [];
}
return Object.keys(this._tree[path][eventType]);
}
/**
* Returns a single registration key for the specified path, eventType, and listener
*
* @param path
* @param eventType
* @param listener
* @return {Array}
*/
getOneByPathEventListener(path, eventType, listener) {
if (!this._tree[path]) {
return null;
}
if (!this._tree[path][eventType]) {
return null;
}
const registrationsForPathEvent = Object.entries(this._tree[path][eventType]);
for (let i = 0; i < registrationsForPathEvent.length; i++) {
const registration = registrationsForPathEvent[i];
if (registration[1] === listener) {
return registration[0];
}
}
return null;
}
/**
* Register a new listener.
*
* @param registration
*/
addRegistration(registration) {
const { eventRegistrationKey, eventType, listener, once, path } = registration;
if (!this._tree[path]) {
this._tree[path] = {};
}
if (!this._tree[path][eventType]) {
this._tree[path][eventType] = {};
}
this._tree[path][eventType][eventRegistrationKey] = listener;
this._reverseLookup[eventRegistrationKey] = registration;
if (once) {
const subscription = SharedEventEmitter.addListener(eventRegistrationKey, event => {
this._onOnceRemoveRegistration(eventRegistrationKey, listener)(event);
subscription.remove();
});
} else {
const registration = SharedEventEmitter.addListener(eventRegistrationKey, listener);
// add this listener registration info to our emitter-key map
// in case we need to identify and remove a specific listener later
const registrations = this._allocate(this._registry, eventRegistrationKey);
registrations.add(registration);
}
return eventRegistrationKey;
}
/**
* 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) {
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.native.off(registrationObj.key, 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);
};
}
}
export default new DatabaseSyncTree();