[android,js][database] transactions support implemented

This commit is contained in:
Salakar 2017-03-24 22:53:56 +00:00
parent 61d6358ce0
commit bb6b1aa7f2
6 changed files with 487 additions and 50 deletions

View File

@ -20,6 +20,7 @@ import com.facebook.react.bridge.ReadableType;
import com.facebook.react.bridge.ReadableArray;
import com.google.firebase.database.DataSnapshot;
import com.facebook.react.bridge.ReadableMapKeySetIterator;
import com.google.firebase.database.MutableData;
@SuppressWarnings("WeakerAccess")
public class Utils {
@ -94,7 +95,7 @@ public class Utils {
if (!dataSnapshot.hasChildren()) {
mapPutValue("value", dataSnapshot.getValue(), snapshot);
} else {
Object value = Utils.castSnapshotValue(dataSnapshot);
Object value = Utils.castValue(dataSnapshot);
if (value instanceof WritableNativeArray) {
snapshot.putArray("value", (WritableArray) value);
} else {
@ -113,13 +114,43 @@ public class Utils {
return eventMap;
}
/**
*
* @param dataSnapshot
* @return
*/
public static WritableMap snapshotToMap(DataSnapshot dataSnapshot) {
WritableMap snapshot = Arguments.createMap();
snapshot.putString("key", dataSnapshot.getKey());
snapshot.putBoolean("exists", dataSnapshot.exists());
snapshot.putBoolean("hasChildren", dataSnapshot.hasChildren());
snapshot.putDouble("childrenCount", dataSnapshot.getChildrenCount());
if (!dataSnapshot.hasChildren()) {
mapPutValue("value", dataSnapshot.getValue(), snapshot);
} else {
Object value = Utils.castValue(dataSnapshot);
if (value instanceof WritableNativeArray) {
snapshot.putArray("value", (WritableArray) value);
} else {
snapshot.putMap("value", (WritableMap) value);
}
}
snapshot.putArray("childKeys", Utils.getChildKeys(dataSnapshot));
mapPutValue("priority", dataSnapshot.getPriority(), snapshot);
return snapshot;
}
/**
*
* @param snapshot
* @param <Any>
* @return
*/
public static <Any> Any castSnapshotValue(DataSnapshot snapshot) {
public static <Any> Any castValue(DataSnapshot snapshot) {
if (snapshot.hasChildren()) {
if (isArray(snapshot)) {
return (Any) buildArray(snapshot);
@ -144,6 +175,37 @@ public class Utils {
}
}
/**
*
* @param mutableData
* @param <Any>
* @return
*/
public static <Any> Any castValue(MutableData mutableData) {
if (mutableData.hasChildren()) {
if (isArray(mutableData)) {
return (Any) buildArray(mutableData);
} else {
return (Any) buildMap(mutableData);
}
} else {
if (mutableData.getValue() != null) {
String type = mutableData.getValue().getClass().getName();
switch (type) {
case "java.lang.Boolean":
case "java.lang.Long":
case "java.lang.Double":
case "java.lang.String":
return (Any) (mutableData.getValue());
default:
Log.w(TAG, "Invalid type: " + type);
return null;
}
}
return null;
}
}
/**
*
* @param snapshot
@ -166,6 +228,28 @@ public class Utils {
return true;
}
/**
*
* @param mutableData
* @return
*/
private static boolean isArray(MutableData mutableData) {
long expectedKey = 0;
for (MutableData child : mutableData.getChildren()) {
try {
long key = Long.parseLong(child.getKey());
if (key == expectedKey) {
expectedKey++;
} else {
return false;
}
} catch (NumberFormatException ex) {
return false;
}
}
return true;
}
/**
*
* @param snapshot
@ -175,7 +259,45 @@ public class Utils {
private static <Any> WritableArray buildArray(DataSnapshot snapshot) {
WritableArray array = Arguments.createArray();
for (DataSnapshot child : snapshot.getChildren()) {
Any castedChild = castSnapshotValue(child);
Any castedChild = castValue(child);
switch (castedChild.getClass().getName()) {
case "java.lang.Boolean":
array.pushBoolean((Boolean) castedChild);
break;
case "java.lang.Long":
Long longVal = (Long) castedChild;
array.pushDouble((double) longVal);
break;
case "java.lang.Double":
array.pushDouble((Double) castedChild);
break;
case "java.lang.String":
array.pushString((String) castedChild);
break;
case "com.facebook.react.bridge.WritableNativeMap":
array.pushMap((WritableMap) castedChild);
break;
case "com.facebook.react.bridge.WritableNativeArray":
array.pushArray((WritableArray) castedChild);
break;
default:
Log.w(TAG, "Invalid type: " + castedChild.getClass().getName());
break;
}
}
return array;
}
/**
*
* @param mutableData
* @param <Any>
* @return
*/
private static <Any> WritableArray buildArray(MutableData mutableData) {
WritableArray array = Arguments.createArray();
for (MutableData child : mutableData.getChildren()) {
Any castedChild = castValue(child);
switch (castedChild.getClass().getName()) {
case "java.lang.Boolean":
array.pushBoolean((Boolean) castedChild);
@ -213,7 +335,45 @@ public class Utils {
private static <Any> WritableMap buildMap(DataSnapshot snapshot) {
WritableMap map = Arguments.createMap();
for (DataSnapshot child : snapshot.getChildren()) {
Any castedChild = castSnapshotValue(child);
Any castedChild = castValue(child);
switch (castedChild.getClass().getName()) {
case "java.lang.Boolean":
map.putBoolean(child.getKey(), (Boolean) castedChild);
break;
case "java.lang.Long":
map.putDouble(child.getKey(), (double) ((Long) castedChild));
break;
case "java.lang.Double":
map.putDouble(child.getKey(), (Double) castedChild);
break;
case "java.lang.String":
map.putString(child.getKey(), (String) castedChild);
break;
case "com.facebook.react.bridge.WritableNativeMap":
map.putMap(child.getKey(), (WritableMap) castedChild);
break;
case "com.facebook.react.bridge.WritableNativeArray":
map.putArray(child.getKey(), (WritableArray) castedChild);
break;
default:
Log.w(TAG, "Invalid type: " + castedChild.getClass().getName());
break;
}
}
return map;
}
/**
*
* @param mutableData
* @param <Any>
* @return
*/
private static <Any> WritableMap buildMap(MutableData mutableData) {
WritableMap map = Arguments.createMap();
for (MutableData child : mutableData.getChildren()) {
Any castedChild = castValue(child);
switch (castedChild.getClass().getName()) {
case "java.lang.Boolean":

View File

@ -1,15 +1,16 @@
package io.invertase.firebase.database;
import java.util.List;
import java.util.Map;
import java.util.List;
import java.util.HashMap;
import android.net.Uri;
import android.os.AsyncTask;
import android.util.Log;
import java.util.HashMap;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReactMethod;
@ -19,18 +20,22 @@ import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReadableMapKeySetIterator;
import com.facebook.react.bridge.WritableNativeArray;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.MutableData;
import com.google.firebase.database.ServerValue;
import com.google.firebase.database.OnDisconnect;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.ServerValue;
import com.google.firebase.database.Transaction;
import io.invertase.firebase.Utils;
public class RNFirebaseDatabase extends ReactContextBaseJavaModule {
private static final String TAG = "RNFirebaseDatabase";
private HashMap<String, RNFirebaseDatabaseReference> mDBListeners = new HashMap<String, RNFirebaseDatabaseReference>();
private HashMap<String, RNFirebaseDatabaseReference> mDBListeners = new HashMap<>();
private HashMap<String, RNFirebaseTransactionHandler> mTransactionHandlers = new HashMap<>();
private FirebaseDatabase mFirebaseDatabase;
public RNFirebaseDatabase(ReactApplicationContext reactContext) {
@ -170,6 +175,109 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule {
}
}
/**
* @param path
* @param id
* @param applyLocally
*/
@ReactMethod
public void startTransaction(final String path, final String id, final Boolean applyLocally) {
AsyncTask.execute(new Runnable() {
@Override
public void run() {
DatabaseReference transactionRef = FirebaseDatabase.getInstance().getReference(path);
transactionRef.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData mutableData) {
final WritableMap updatesMap = Arguments.createMap();
updatesMap.putString("id", id);
updatesMap.putString("type", "update");
if (!mutableData.hasChildren()) {
Utils.mapPutValue("value", mutableData.getValue(), updatesMap);
} else {
Object value = Utils.castValue(mutableData);
if (value instanceof WritableNativeArray) {
updatesMap.putArray("value", (WritableArray) value);
} else {
updatesMap.putMap("value", (WritableMap) value);
}
}
RNFirebaseTransactionHandler rnFirebaseTransactionHandler = new RNFirebaseTransactionHandler();
mTransactionHandlers.put(id, rnFirebaseTransactionHandler);
AsyncTask.execute(new Runnable() {
@Override
public void run() {
Utils.sendEvent(getReactApplicationContext(), "database_transaction_event", updatesMap);
}
});
try {
rnFirebaseTransactionHandler.await();
} catch (InterruptedException e) {
rnFirebaseTransactionHandler.interrupted = true;
return Transaction.abort();
}
if (rnFirebaseTransactionHandler.abort) {
return Transaction.abort();
}
mutableData.setValue(rnFirebaseTransactionHandler.value);
return Transaction.success(mutableData);
}
@Override
public void onComplete(DatabaseError databaseError, boolean committed, DataSnapshot dataSnapshot) {
final WritableMap updatesMap = Arguments.createMap();
updatesMap.putString("id", id);
RNFirebaseTransactionHandler rnFirebaseTransactionHandler = mTransactionHandlers.get(id);
// TODO error conversion util for database to create web sdk codes based on DatabaseError
if (databaseError != null) {
updatesMap.putString("type", "error");
updatesMap.putInt("code", databaseError.getCode());
updatesMap.putString("message", databaseError.getMessage());
} else if (rnFirebaseTransactionHandler.interrupted) {
updatesMap.putString("type", "error");
updatesMap.putInt("code", 666);
updatesMap.putString("message", "RNFirebase transaction was interrupted, aborting.");
} else {
updatesMap.putString("type", "complete");
updatesMap.putBoolean("committed", committed);
updatesMap.putMap("snapshot", Utils.snapshotToMap(dataSnapshot));
}
Utils.sendEvent(getReactApplicationContext(), "database_transaction_event", updatesMap);
mTransactionHandlers.remove(id);
}
}, applyLocally);
}
});
}
/**
*
* @param id
* @param updates
*/
@ReactMethod
public void tryCommitTransaction(final String id, final ReadableMap updates) {
Map<String, Object> updatesReturned = Utils.recursivelyDeconstructReadableMap(updates);
RNFirebaseTransactionHandler rnFirebaseTransactionHandler = mTransactionHandlers.get(id);
if (rnFirebaseTransactionHandler != null) {
rnFirebaseTransactionHandler.signalUpdateReceived(updatesReturned);
}
}
@ReactMethod
public void on(final String path, final String modifiersString, final ReadableArray modifiersArray, final String eventName, final Callback callback) {
RNFirebaseDatabaseReference ref = this.getDBHandle(path, modifiersArray, modifiersString);
@ -187,11 +295,7 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule {
}
@ReactMethod
public void once(final String path,
final String modifiersString,
final ReadableArray modifiersArray,
final String eventName,
final Callback callback) {
public void once(final String path, final String modifiersString, final ReadableArray modifiersArray, final String eventName, final Callback callback) {
RNFirebaseDatabaseReference ref = this.getDBHandle(path, modifiersArray, modifiersString);
ref.addOnceValueEventListener(callback);
}
@ -336,9 +440,7 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule {
}
}
private RNFirebaseDatabaseReference getDBHandle(final String path,
final ReadableArray modifiersArray,
final String modifiersString) {
private RNFirebaseDatabaseReference getDBHandle(final String path, final ReadableArray modifiersArray, final String modifiersString) {
String key = this.getDBListenerKey(path, modifiersString);
RNFirebaseDatabaseReference r = mDBListeners.get(key);

View File

@ -5,8 +5,9 @@
import { NativeModules, NativeEventEmitter } from 'react-native';
import { Base } from './../base';
import Snapshot from './snapshot.js';
import Reference from './reference.js';
import Snapshot from './snapshot';
import Reference from './reference';
import TransactionHandler from './transaction';
import { promisify } from './../../utils';
const FirebaseDatabase = NativeModules.RNFirebaseDatabase;
@ -19,10 +20,11 @@ export default class Database extends Base {
constructor(firebase: Object, options: Object = {}) {
super(firebase, options);
this.subscriptions = {};
this.errorSubscriptions = {};
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);

View File

@ -16,7 +16,7 @@ export default class Query extends ReferenceBase {
ref: Reference;
constructor(ref: Reference, path: string, existingModifiers?: Array<string>) {
super(ref.db, path);
super(ref.database, path);
this.log.debug('creating Query ', path, existingModifiers);
this.ref = ref;
this.modifiers = existingModifiers ? [...existingModifiers] : [];

View File

@ -17,15 +17,15 @@ const FirebaseDatabase = NativeModules.RNFirebaseDatabase;
*/
export default class Reference extends ReferenceBase {
db: FirebaseDatabase;
database: FirebaseDatabase;
query: Query;
constructor(db: FirebaseDatabase, path: string, existingModifiers?: Array<string>) {
super(db.firebase, path);
this.db = db;
constructor(database: FirebaseDatabase, path: string, existingModifiers?: Array<string>) {
super(database.firebase, path);
this.database = database;
this.namespace = 'firebase:db:ref';
this.query = new Query(this, path, existingModifiers);
this.log.debug('Created new Reference', this.db._handle(path, existingModifiers));
this.log.debug('Created new Reference', this.database._handle(path, existingModifiers));
}
/**
@ -34,7 +34,7 @@ export default class Reference extends ReferenceBase {
* @returns {*}
*/
keepSynced(bool: boolean) {
const path = this._dbPath();
const path = this.path;
return promisify('keepSynced', FirebaseDatabase)(path, bool);
}
@ -44,7 +44,7 @@ export default class Reference extends ReferenceBase {
* @returns {*}
*/
set(value: any) {
const path = this._dbPath();
const path = this.path;
const _value = this._serializeAnyType(value);
return promisify('set', FirebaseDatabase)(path, _value);
}
@ -55,7 +55,7 @@ export default class Reference extends ReferenceBase {
* @returns {*}
*/
update(val: Object) {
const path = this._dbPath();
const path = this.path;
const value = this._serializeObject(val);
return promisify('update', FirebaseDatabase)(path, value);
}
@ -65,7 +65,7 @@ export default class Reference extends ReferenceBase {
* @returns {*}
*/
remove() {
return promisify('remove', FirebaseDatabase)(this._dbPath());
return promisify('remove', FirebaseDatabase)(this.path);
}
/**
@ -76,15 +76,15 @@ export default class Reference extends ReferenceBase {
*/
push(value: any, onComplete: Function) {
if (value === null || value === undefined) {
return new Reference(this.db, `${this.path}/${generatePushID(this.db.serverTimeOffset)}`);
return new Reference(this.database, `${this.path}/${generatePushID(this.database.serverTimeOffset)}`);
}
const path = this._dbPath();
const path = this.path;
const _value = this._serializeAnyType(value);
return promisify('push', FirebaseDatabase)(path, _value)
.then(({ ref }) => {
const newRef = new Reference(this.db, ref);
const newRef = new Reference(this.database, ref);
if (isFunction(onComplete)) return onComplete(null, newRef);
return newRef;
}).catch((e) => {
@ -104,11 +104,12 @@ export default class Reference extends ReferenceBase {
on(eventType: string, successCallback: () => any, failureCallback: () => any) {
if (!isFunction(successCallback)) throw new Error('The specified callback must be a function');
if (failureCallback && !isFunction(failureCallback)) throw new Error('The specified error callback must be a function');
const path = this._dbPath();
const path = this.path;
const modifiers = this.query.getModifiers();
const modifiersString = this.query.getModifiersString();
this.log.debug('adding reference.on', path, modifiersString, eventType);
return this.db.on(path, modifiersString, modifiers, eventType, successCallback, failureCallback);
this.database.on(path, modifiersString, modifiers, eventType, successCallback, failureCallback);
return successCallback;
}
/**
@ -120,7 +121,7 @@ export default class Reference extends ReferenceBase {
* @returns {Promise.<TResult>}
*/
once(eventType: string = 'value', successCallback: (snapshot: Object) => void, failureCallback: (error: Error) => void) {
const path = this._dbPath();
const path = this.path;
const modifiers = this.query.getModifiers();
const modifiersString = this.query.getModifiersString();
return promisify('once', FirebaseDatabase)(path, modifiersString, modifiers, eventType)
@ -130,7 +131,7 @@ export default class Reference extends ReferenceBase {
return snapshot;
})
.catch((error) => {
const firebaseError = this.db._toFirebaseError(error);
const firebaseError = this.database._toFirebaseError(error);
if (isFunction(failureCallback)) return failureCallback(firebaseError);
return Promise.reject(firebaseError);
});
@ -143,10 +144,40 @@ export default class Reference extends ReferenceBase {
* @returns {*}
*/
off(eventType?: string = '', origCB?: () => any) {
const path = this._dbPath();
const path = this.path;
const modifiersString = this.query.getModifiersString();
this.log.debug('ref.off(): ', path, modifiersString, eventType);
return this.db.off(path, modifiersString, eventType, origCB);
return this.database.off(path, modifiersString, eventType, origCB);
}
/**
* Atomically modifies the data at this location.
* @url https://firebase.google.com/docs/reference/js/firebase.database.Reference#transaction
* @param transactionUpdate
* @param onComplete
* @param applyLocally
*/
transaction(transactionUpdate, onComplete?: () => any, applyLocally: boolean = false) {
if (!isFunction(transactionUpdate)) return Promise.reject(new Error('Missing transactionUpdate function argument.'));
return new Promise((resolve, reject) => {
const onCompleteWrapper = (error, committed, snapshotData) => {
if (error) {
if (isFunction(onComplete)) onComplete(error, committed, null);
return reject(error);
}
const snapshot = new Snapshot(this, snapshotData);
if (isFunction(onComplete)) {
onComplete(null, committed, snapshot);
}
return resolve({ committed, snapshot });
};
this.database.transaction.add(this, transactionUpdate, onCompleteWrapper, applyLocally);
});
}
/**
@ -193,7 +224,7 @@ export default class Reference extends ReferenceBase {
* @returns {Reference}
*/
orderBy(name: string, key?: string): Reference {
const newRef = new Reference(this.db, this.path, this.query.getModifiers());
const newRef = new Reference(this.database, this.path, this.query.getModifiers());
newRef.query.setOrderBy(name, key);
return newRef;
}
@ -227,7 +258,7 @@ export default class Reference extends ReferenceBase {
* @returns {Reference}
*/
limit(name: string, limit: number): Reference {
const newRef = new Reference(this.db, this.path, this.query.getModifiers());
const newRef = new Reference(this.database, this.path, this.query.getModifiers());
newRef.query.setLimit(name, limit);
return newRef;
}
@ -274,7 +305,7 @@ export default class Reference extends ReferenceBase {
* @returns {Reference}
*/
filter(name: string, value: any, key?: string): Reference {
const newRef = new Reference(this.db, this.path, this.query.getModifiers());
const newRef = new Reference(this.database, this.path, this.query.getModifiers());
newRef.query.setFilter(name, value, key);
return newRef;
}
@ -293,7 +324,7 @@ export default class Reference extends ReferenceBase {
* @returns {Reference}
*/
child(path: string) {
return new Reference(this.db, `${this.path}/${path}`);
return new Reference(this.database, `${this.path}/${path}`);
}
/**
@ -301,7 +332,7 @@ export default class Reference extends ReferenceBase {
* @returns {string}
*/
toString(): string {
return this._dbPath();
return this.path;
}
/**
@ -314,7 +345,7 @@ export default class Reference extends ReferenceBase {
*/
get parent(): Reference|null {
if (this.path === '/') return null;
return new Reference(this.db, this.path.substring(0, this.path.lastIndexOf('/')));
return new Reference(this.database, this.path.substring(0, this.path.lastIndexOf('/')));
}
@ -323,16 +354,13 @@ export default class Reference extends ReferenceBase {
* @returns {Reference}
*/
get root(): Reference {
return new Reference(this.db, '/');
return new Reference(this.database, '/');
}
/**
* INTERNALS
*/
_dbPath(): string {
return this.path;
}
/**
*

View File

@ -0,0 +1,145 @@
/**
* @flow
* Database representation wrapper
*/
import { NativeModules } from 'react-native';
import { Base } from './../base';
import { generatePushID } from './../../utils';
const FirebaseDatabase = NativeModules.RNFirebaseDatabase;
/**
* @class Database
*/
export default class TransactionHandler extends Base {
constructor(firebase: Object, database: Object, FirebaseDatabaseEvt) {
super(firebase, {});
this.transactions = {};
this.database = database;
this.namespace = 'firebase:database:transaction';
this.transactionListener = FirebaseDatabaseEvt.addListener(
'database_transaction_event',
event => this._handleTransactionEvent(event)
);
}
/**
* Add a new transaction and begin starts it natively.
* @param reference
* @param transactionUpdater
* @param onComplete
* @param applyLocally
*/
add(reference, transactionUpdater, onComplete, applyLocally = false) {
const id = this._generateTransactionId();
this.transactions[id] = {
id,
reference,
transactionUpdater,
onComplete,
applyLocally,
completed: false,
started: true,
};
FirebaseDatabase.startTransaction(reference.path, id, applyLocally || false);
}
/**
* INTERNALS
*/
/**
* Uses the push id generator to create a transaction id
* @returns {string}
* @private
*/
_generateTransactionId() {
return generatePushID(this.database.serverTimeOffset);
}
/**
*
* @param event
* @returns {*}
* @private
*/
_handleTransactionEvent(event: Object = {}) {
switch (event.type) {
case 'update':
return this._handleUpdate(event);
case 'error':
return this._handleError(error);
case 'complete':
return this._handleComplete(event);
default:
this.log.warn(`Unknown transaction event type: '${event.type}'`, event);
return undefined;
}
}
/**
*
* @param event
* @private
*/
_handleUpdate(event: Object = {}) {
let newValue;
const { id, value } = event;
try {
const transaction = this.transactions[id];
// todo handle when transaction no longer exists on js side?
newValue = transaction.transactionUpdater(value);
} finally {
let abort = false;
if (newValue === undefined) {
abort = true;
}
FirebaseDatabase.tryCommitTransaction(id, { value: newValue, abort });
}
}
/**
*
* @param event
* @private
*/
_handleError(event: Object = {}) {
const transaction = this.transactions[event.id];
if (transaction && !transaction.completed) {
transaction.completed = true;
try {
transaction.onComplete(new Error(event.message, event.code), null);
} finally {
setImmediate(() => {
delete this.transactions[event.id];
});
}
}
}
/**
*
* @param event
* @private
*/
_handleComplete(event: Object = {}) {
const transaction = this.transactions[event.id];
if (transaction && !transaction.completed) {
transaction.completed = true;
try {
transaction.onComplete(null, event.committed, Object.assign({}, event.snapshot));
} finally {
setImmediate(() => {
delete this.transactions[event.id];
});
}
}
}
}