253 lines
7.1 KiB
JavaScript
253 lines
7.1 KiB
JavaScript
/**
|
|
* @flow
|
|
* Database representation wrapper
|
|
*/
|
|
import { NativeModules, NativeEventEmitter } from 'react-native';
|
|
|
|
import { Base } from './../base';
|
|
import Snapshot from './snapshot';
|
|
import Reference from './reference';
|
|
import TransactionHandler from './transaction';
|
|
import { promisify } from './../../utils';
|
|
|
|
const FirebaseDatabase = NativeModules.RNFirebaseDatabase;
|
|
const FirebaseDatabaseEvt = new NativeEventEmitter(FirebaseDatabase);
|
|
|
|
/**
|
|
* @class Database
|
|
*/
|
|
export default class Database extends Base {
|
|
constructor(firebase: Object, options: Object = {}) {
|
|
super(firebase, options);
|
|
this.subscriptions = {};
|
|
this.serverTimeOffset = 0;
|
|
this.errorSubscriptions = {};
|
|
this.persistenceEnabled = false;
|
|
this.namespace = 'firebase:database';
|
|
this.transaction = new TransactionHandler(firebase, this, FirebaseDatabaseEvt);
|
|
|
|
if (firebase.options.persistence === true) {
|
|
this._setPersistence(true);
|
|
}
|
|
|
|
this.successListener = FirebaseDatabaseEvt.addListener(
|
|
'database_event',
|
|
event => this._handleDatabaseEvent(event)
|
|
);
|
|
|
|
this.errorListener = FirebaseDatabaseEvt.addListener(
|
|
'database_error',
|
|
err => this._handleDatabaseError(err)
|
|
);
|
|
|
|
this.offsetRef = this.ref('.info/serverTimeOffset');
|
|
|
|
this.offsetRef.on('value', (snapshot) => {
|
|
this.serverTimeOffset = snapshot.val() || this.serverTimeOffset;
|
|
});
|
|
|
|
this.log.debug('Created new Database instance', this.options);
|
|
}
|
|
|
|
/**
|
|
* Returns a new firebase reference instance
|
|
* @param path
|
|
* @returns {Reference}
|
|
*/
|
|
ref(path: string) {
|
|
return new Reference(this, path);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param path
|
|
* @param modifiersString
|
|
* @param modifiers
|
|
* @param eventName
|
|
* @param cb
|
|
* @param errorCb
|
|
* @returns {*}
|
|
*/
|
|
on(path: string, modifiersString: string, modifiers: Array<string>, eventName: string, cb: () => void, errorCb: () => void) {
|
|
const handle = this._handle(path, modifiersString);
|
|
this.log.debug('adding on listener', handle);
|
|
|
|
if (!this.subscriptions[handle]) this.subscriptions[handle] = {};
|
|
if (!this.subscriptions[handle][eventName]) this.subscriptions[handle][eventName] = [];
|
|
this.subscriptions[handle][eventName].push(cb);
|
|
|
|
if (errorCb) {
|
|
if (!this.errorSubscriptions[handle]) this.errorSubscriptions[handle] = [];
|
|
this.errorSubscriptions[handle].push(errorCb);
|
|
}
|
|
|
|
return promisify('on', FirebaseDatabase)(path, modifiersString, modifiers, eventName);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param path
|
|
* @param modifiersString
|
|
* @param eventName
|
|
* @param origCB
|
|
* @returns {*}
|
|
*/
|
|
off(path: string, modifiersString: string, eventName?: string, origCB?: () => void) {
|
|
const handle = this._handle(path, modifiersString);
|
|
this.log.debug('off() : ', handle, eventName);
|
|
|
|
if (!this.subscriptions[handle] || (eventName && !this.subscriptions[handle][eventName])) {
|
|
this.log.warn('off() called, but not currently listening at that location (bad path)', handle, eventName);
|
|
return Promise.resolve();
|
|
}
|
|
|
|
if (eventName && origCB) {
|
|
const i = this.subscriptions[handle][eventName].indexOf(origCB);
|
|
|
|
if (i === -1) {
|
|
this.log.warn('off() called, but the callback specified is not listening at that location (bad path)', handle, eventName);
|
|
return Promise.resolve();
|
|
}
|
|
|
|
this.subscriptions[handle][eventName].splice(i, 1);
|
|
if (this.subscriptions[handle][eventName].length > 0) return Promise.resolve();
|
|
} else if (eventName) {
|
|
this.subscriptions[handle][eventName] = [];
|
|
} else {
|
|
this.subscriptions[handle] = {};
|
|
}
|
|
this.errorSubscriptions[handle] = [];
|
|
return promisify('off', FirebaseDatabase)(path, modifiersString, eventName);
|
|
}
|
|
|
|
/**
|
|
* Removes all event handlers and their native subscriptions
|
|
* @returns {Promise.<*>}
|
|
*/
|
|
cleanup() {
|
|
const promises = [];
|
|
Object.keys(this.subscriptions).forEach((handle) => {
|
|
Object.keys(this.subscriptions[handle]).forEach((eventName) => {
|
|
const separator = handle.indexOf('|');
|
|
const path = handle.substring(0, separator);
|
|
const modifiersString = handle.substring(separator + 1);
|
|
promises.push(this.off(path, modifiersString, eventName));
|
|
});
|
|
});
|
|
|
|
return Promise.all(promises);
|
|
}
|
|
|
|
goOnline() {
|
|
FirebaseDatabase.goOnline();
|
|
}
|
|
|
|
goOffline() {
|
|
FirebaseDatabase.goOffline();
|
|
}
|
|
|
|
/**
|
|
* INTERNALS
|
|
*/
|
|
_getServerTime() {
|
|
return new Date().getTime() + this.serverTimeOffset;
|
|
}
|
|
|
|
/**
|
|
* Enabled / disable database persistence
|
|
* @param enable
|
|
* @returns {*}
|
|
* @private
|
|
*/
|
|
_setPersistence(enable: boolean = true) {
|
|
if (this.persistenceEnabled !== enable) {
|
|
this.persistenceEnabled = enable;
|
|
this.log.debug(`${enable ? 'Enabling' : 'Disabling'} persistence.`);
|
|
return promisify('enablePersistence', FirebaseDatabase)(enable);
|
|
}
|
|
|
|
return Promise.reject({ status: 'Already enabled' });
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param path
|
|
* @param modifiersString
|
|
* @returns {string}
|
|
* @private
|
|
*/
|
|
_handle(path: string = '', modifiersString: string = '') {
|
|
return `${path}|${modifiersString}`;
|
|
}
|
|
|
|
|
|
/**
|
|
*
|
|
* @param event
|
|
* @private
|
|
*/
|
|
_handleDatabaseEvent(event: Object) {
|
|
const body = event.body || {};
|
|
const { path, modifiersString, eventName, snapshot } = body;
|
|
const handle = this._handle(path, modifiersString);
|
|
|
|
this.log.debug('_handleDatabaseEvent: ', handle, eventName, snapshot && snapshot.key);
|
|
|
|
if (this.subscriptions[handle] && this.subscriptions[handle][eventName]) {
|
|
this.subscriptions[handle][eventName].forEach((cb) => {
|
|
cb(new Snapshot(new Reference(this, path, modifiersString.split('|')), snapshot), body);
|
|
});
|
|
} else {
|
|
FirebaseDatabase.off(path, modifiersString, eventName, () => {
|
|
this.log.debug('_handleDatabaseEvent: No JS listener registered, removed native listener', handle, eventName);
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Converts an native error object to a 'firebase like' error.
|
|
* @param error
|
|
* @returns {Error}
|
|
* @private
|
|
*/
|
|
_toFirebaseError(error) {
|
|
const { path, message, modifiers, code, details } = error;
|
|
let firebaseMessage = `FirebaseError: ${message.toLowerCase().replace(/\s/g, '_')}`;
|
|
|
|
if (path) {
|
|
firebaseMessage = `${firebaseMessage} at /${path}\r\n`;
|
|
}
|
|
|
|
const firebaseError = new Error(firebaseMessage);
|
|
|
|
firebaseError.code = code;
|
|
firebaseError.path = path;
|
|
firebaseError.details = details;
|
|
firebaseError.modifiers = modifiers;
|
|
|
|
return firebaseError;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param error
|
|
* @private
|
|
*/
|
|
_handleDatabaseError(error: Object = {}) {
|
|
const { path, modifiers } = error;
|
|
const handle = this._handle(path, modifiers);
|
|
const firebaseError = this._toFirebaseError(error);
|
|
|
|
this.log.debug('_handleDatabaseError ->', handle, 'database_error', error);
|
|
|
|
if (this.errorSubscriptions[handle]) this.errorSubscriptions[handle].forEach(listener => listener(firebaseError));
|
|
}
|
|
}
|
|
|
|
export const statics = {
|
|
ServerValue: {
|
|
TIMESTAMP: FirebaseDatabase.serverValueTimestamp || { '.sv': 'timestamp' },
|
|
},
|
|
};
|
|
|