From e3d126197360a2b9f2deb8f92927cb83fa743960 Mon Sep 17 00:00:00 2001 From: Salakar Date: Sun, 30 Jul 2017 07:34:41 +0100 Subject: [PATCH] [database][wip] refactor & improvements to add support for multiple apps --- .../java/io/invertase/firebase/Utils.java | 23 +- .../firebase/database/RNFirebaseDatabase.java | 762 ++++++++++-------- .../database/RNFirebaseDatabaseOld.java | 504 ++++++++++++ .../database/RNFirebaseDatabaseReference.java | 260 +++--- .../RNFirebaseTransactionHandler.java | 113 ++- ios/RNFirebase/auth/RNFirebaseAuth.m | 2 +- lib/modules/database/disconnect.js | 11 +- lib/modules/database/index.js | 186 +---- lib/modules/database/index.old.js | 194 +++++ lib/modules/database/reference.js | 549 ++++++------- lib/modules/database/transaction.js | 27 +- lib/utils/ModuleBase.js | 11 +- lib/utils/index.js | 67 +- tests/ios/Podfile.lock | 18 +- tests/lib/TestRun.js | 5 + tests/src/main.js | 2 + tests/src/tests/database/ref/childTests.js | 4 +- tests/src/tests/database/ref/factoryTests.js | 4 +- tests/src/tests/database/ref/isEqualTests.js | 4 +- .../tests/database/ref/issueSpecificTests.js | 8 +- tests/src/tests/database/ref/keyTests.js | 4 +- tests/src/tests/database/ref/onceTests.js | 7 +- tests/src/tests/database/ref/parentTests.js | 4 +- tests/src/tests/database/ref/priorityTests.js | 4 +- tests/src/tests/database/ref/pushTests.js | 4 +- tests/src/tests/database/ref/queryTests.js | 4 +- tests/src/tests/database/ref/refTests.js | 4 +- tests/src/tests/database/ref/removeTests.js | 4 +- tests/src/tests/database/ref/rootTests.js | 4 +- tests/src/tests/database/ref/setTests.js | 8 +- .../tests/database/ref/transactionTests.js | 56 +- tests/src/tests/database/ref/updateTests.js | 4 +- tests/src/tests/storage/storageTests.js | 7 +- 33 files changed, 1768 insertions(+), 1100 deletions(-) create mode 100644 android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabaseOld.java create mode 100644 lib/modules/database/index.old.js diff --git a/android/src/main/java/io/invertase/firebase/Utils.java b/android/src/main/java/io/invertase/firebase/Utils.java index b2b07b99..37c76ee4 100644 --- a/android/src/main/java/io/invertase/firebase/Utils.java +++ b/android/src/main/java/io/invertase/firebase/Utils.java @@ -77,15 +77,14 @@ public class Utils { } /** - * * @param name - * @param refId - * @param listenerId * @param path * @param dataSnapshot + * @param refId + * @param listenerId * @return */ - public static WritableMap snapshotToMap(String name, int refId, Integer listenerId, String path, DataSnapshot dataSnapshot, @Nullable String previousChildName) { + public static WritableMap snapshotToMap(String name, String path, DataSnapshot dataSnapshot, @Nullable String previousChildName, int refId, int listenerId) { WritableMap snapshot = Arguments.createMap(); WritableMap eventMap = Arguments.createMap(); @@ -109,19 +108,16 @@ public class Utils { mapPutValue("priority", dataSnapshot.getPriority(), snapshot); eventMap.putInt("refId", refId); - if (listenerId != null) { - eventMap.putInt("listenerId", listenerId); - } eventMap.putString("path", path); eventMap.putMap("snapshot", snapshot); eventMap.putString("eventName", name); + eventMap.putInt("listenerId", listenerId); eventMap.putString("previousChildName", previousChildName); return eventMap; } /** - * * @param dataSnapshot * @return */ @@ -151,7 +147,6 @@ public class Utils { } /** - * * @param snapshot * @param * @return @@ -182,7 +177,6 @@ public class Utils { } /** - * * @param mutableData * @param * @return @@ -216,7 +210,7 @@ public class Utils { * Data should be treated as an array if: * 1) All the keys are integers * 2) More than half the keys between 0 and the maximum key in the object have non-empty values - * + *

* Definition from: https://firebase.googleblog.com/2014/04/best-practices-arrays-in-firebase.html * * @param snapshot @@ -244,7 +238,7 @@ public class Utils { * Data should be treated as an array if: * 1) All the keys are integers * 2) More than half the keys between 0 and the maximum key in the object have non-empty values - * + *

* Definition from: https://firebase.googleblog.com/2014/04/best-practices-arrays-in-firebase.html * * @param mutableData @@ -269,7 +263,6 @@ public class Utils { } /** - * * @param snapshot * @param * @return @@ -316,7 +309,6 @@ public class Utils { } /** - * * @param mutableData * @param * @return @@ -363,7 +355,6 @@ public class Utils { } /** - * * @param snapshot * @param * @return @@ -401,7 +392,6 @@ public class Utils { } /** - * * @param mutableData * @param * @return @@ -439,7 +429,6 @@ public class Utils { } /** - * * @param snapshot * @return */ diff --git a/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabase.java b/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabase.java index 23b3d059..d1c9adbe 100644 --- a/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabase.java +++ b/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabase.java @@ -1,252 +1,124 @@ package io.invertase.firebase.database; -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 android.util.SparseArray; -import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.Promise; 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; -import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableMap; 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.FirebaseApp; 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.DatabaseException; +import com.google.firebase.database.ServerValue; import com.google.firebase.database.Transaction; +import com.google.firebase.database.DataSnapshot; +import com.google.firebase.database.DatabaseError; +import com.google.firebase.database.FirebaseDatabase; +import com.google.firebase.database.DatabaseReference; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; import io.invertase.firebase.Utils; + public class RNFirebaseDatabase extends ReactContextBaseJavaModule { private static final String TAG = "RNFirebaseDatabase"; - private HashMap mReferences = new HashMap<>(); - private HashMap mTransactionHandlers = new HashMap<>(); - private FirebaseDatabase mFirebaseDatabase; + private SparseArray references = new SparseArray<>(); + private SparseArray transactionHandlers = new SparseArray<>(); - public RNFirebaseDatabase(ReactApplicationContext reactContext) { + RNFirebaseDatabase(ReactApplicationContext reactContext) { super(reactContext); - mFirebaseDatabase = FirebaseDatabase.getInstance(); } - @Override - public String getName() { - return TAG; - } + /* + * REACT NATIVE METHODS + */ - // Persistence + /** + * @param appName + */ @ReactMethod - public void enablePersistence( - final Boolean enable, - final Callback callback) { - try { - mFirebaseDatabase.setPersistenceEnabled(enable); - } catch (DatabaseException t) { - - } - - WritableMap res = Arguments.createMap(); - res.putString("status", "success"); - callback.invoke(null, res); + public void goOnline(String appName) { + getDatabaseForApp(appName).goOnline(); } + /** + * @param appName + */ @ReactMethod - public void keepSynced( - final String path, - final Boolean enable, - final Callback callback) { - DatabaseReference ref = mFirebaseDatabase.getReference(path); - ref.keepSynced(enable); - - WritableMap res = Arguments.createMap(); - res.putString("status", "success"); - res.putString("path", path); - callback.invoke(null, res); + public void goOffline(String appName) { + getDatabaseForApp(appName).goOffline(); } - // RNFirebaseDatabase + /** + * @param appName + * @param state + */ @ReactMethod - public void set( - final String path, - final ReadableMap props, - final Callback callback) { - DatabaseReference ref = mFirebaseDatabase.getReference(path); - Map m = Utils.recursivelyDeconstructReadableMap(props); - - DatabaseReference.CompletionListener listener = new DatabaseReference.CompletionListener() { - @Override - public void onComplete(DatabaseError error, DatabaseReference ref) { - handleCallback("set", callback, error); - } - }; - - ref.setValue(m.get("value"), listener); + public void setPersistence(String appName, Boolean state) { + getDatabaseForApp(appName).setPersistenceEnabled(state); } + /** + * @param appName + * @param path + * @param state + */ @ReactMethod - public void priority( - final String path, - final ReadableMap priority, - final Callback callback) { - DatabaseReference ref = mFirebaseDatabase.getReference(path); - Map priorityMap = Utils.recursivelyDeconstructReadableMap(priority); - - DatabaseReference.CompletionListener listener = new DatabaseReference.CompletionListener() { - @Override - public void onComplete(DatabaseError error, DatabaseReference ref) { - handleCallback("priority", callback, error); - } - }; - - ref.setPriority(priorityMap.get("value"), listener); + public void keepSynced(String appName, String path, Boolean state) { + getReferenceForAppPath(appName, path).keepSynced(state); } + + /* + * TRANSACTIONS + */ + + /** + * @param transactionId + * @param updates + */ @ReactMethod - public void withPriority( - final String path, - final ReadableMap data, - final ReadableMap priority, - final Callback callback) { - DatabaseReference ref = mFirebaseDatabase.getReference(path); - Map dataMap = Utils.recursivelyDeconstructReadableMap(data); - Map priorityMap = Utils.recursivelyDeconstructReadableMap(priority); + public void transactionTryCommit(String appName, int transactionId, ReadableMap updates) { + RNFirebaseTransactionHandler handler = transactionHandlers.get(transactionId); - DatabaseReference.CompletionListener listener = new DatabaseReference.CompletionListener() { - @Override - public void onComplete(DatabaseError error, DatabaseReference ref) { - handleCallback("withPriority", callback, error); - } - }; - - ref.setValue(dataMap.get("value"), priorityMap.get("value"), listener); - } - - @ReactMethod - public void update(final String path, - final ReadableMap props, - final Callback callback) { - DatabaseReference ref = mFirebaseDatabase.getReference(path); - Map m = Utils.recursivelyDeconstructReadableMap(props); - - DatabaseReference.CompletionListener listener = new DatabaseReference.CompletionListener() { - @Override - public void onComplete(DatabaseError error, DatabaseReference ref) { - handleCallback("update", callback, error); - } - }; - - ref.updateChildren(m, listener); - } - - @ReactMethod - public void remove(final String path, - final Callback callback) { - DatabaseReference ref = mFirebaseDatabase.getReference(path); - DatabaseReference.CompletionListener listener = new DatabaseReference.CompletionListener() { - @Override - public void onComplete(DatabaseError error, DatabaseReference ref) { - handleCallback("remove", callback, error); - } - }; - - ref.removeValue(listener); - } - - @ReactMethod - public void push(final String path, - final ReadableMap props, - final Callback callback) { - - Log.d(TAG, "Called push with " + path); - DatabaseReference ref = mFirebaseDatabase.getReference(path); - DatabaseReference newRef = ref.push(); - - final Uri url = Uri.parse(newRef.toString()); - final String newPath = url.getPath(); - - ReadableMapKeySetIterator iterator = props.keySetIterator(); - if (iterator.hasNextKey()) { - Log.d(TAG, "Passed value to push"); - // lame way to check if the `props` are empty - Map m = Utils.recursivelyDeconstructReadableMap(props); - - DatabaseReference.CompletionListener listener = new DatabaseReference.CompletionListener() { - @Override - public void onComplete(DatabaseError error, DatabaseReference ref) { - if (error != null) { - WritableMap err = Arguments.createMap(); - err.putInt("code", error.getCode()); - err.putString("details", error.getDetails()); - err.putString("description", error.getMessage()); - callback.invoke(err); - } else { - WritableMap res = Arguments.createMap(); - res.putString("status", "success"); - res.putString("ref", newPath); - callback.invoke(null, res); - } - } - }; - - newRef.setValue(m.get("value"), listener); - } else { - Log.d(TAG, "No value passed to push: " + newPath); - WritableMap res = Arguments.createMap(); - res.putString("status", "success"); - res.putString("ref", newPath); - callback.invoke(null, res); + if (handler != null) { + handler.signalUpdateReceived(updates); } } /** + * Start a native transaction and store it's state in + * + * @param appName * @param path - * @param id + * @param transactionId * @param applyLocally */ @ReactMethod - public void startTransaction(final String path, final String id, final Boolean applyLocally) { + public void transactionStart(final String appName, final String path, final int transactionId, final Boolean applyLocally) { AsyncTask.execute(new Runnable() { @Override public void run() { - DatabaseReference transactionRef = FirebaseDatabase.getInstance().getReference(path); + DatabaseReference reference = getReferenceForAppPath(appName, path); - transactionRef.runTransaction(new Transaction.Handler() { + reference.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); + final RNFirebaseTransactionHandler transactionHandler = new RNFirebaseTransactionHandler(transactionId, appName); + transactionHandlers.put(transactionId, transactionHandler); + final WritableMap updatesMap = transactionHandler.createUpdateMap(mutableData); + // emit the updates to js using an async task + // otherwise it gets blocked by the lock await AsyncTask.execute(new Runnable() { @Override public void run() { @@ -254,245 +126,305 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { } }); + // wait for js to return the updates (js calls transactionTryCommit) try { - rnFirebaseTransactionHandler.await(); + transactionHandler.await(); } catch (InterruptedException e) { - rnFirebaseTransactionHandler.interrupted = true; + transactionHandler.interrupted = true; return Transaction.abort(); } - if (rnFirebaseTransactionHandler.abort) { + if (transactionHandler.abort) { return Transaction.abort(); } - mutableData.setValue(rnFirebaseTransactionHandler.value); + mutableData.setValue(transactionHandler.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); + public void onComplete(DatabaseError error, boolean committed, DataSnapshot snapshot) { + RNFirebaseTransactionHandler transactionHandler = transactionHandlers.get(transactionId); + WritableMap resultMap = transactionHandler.createResultMap(error, committed, snapshot); + Utils.sendEvent(getReactApplicationContext(), "database_transaction_event", resultMap); + transactionHandlers.delete(transactionId); } + }, applyLocally); } }); } + + /* + * ON DISCONNECT + */ + /** + * Set a value on a ref when the client disconnects from the firebase server. * - * @param id - * @param updates + * @param appName + * @param path + * @param props + * @param promise */ @ReactMethod - public void tryCommitTransaction(final String id, final ReadableMap updates) { - Map updatesReturned = Utils.recursivelyDeconstructReadableMap(updates); - RNFirebaseTransactionHandler rnFirebaseTransactionHandler = mTransactionHandlers.get(id); - - if (rnFirebaseTransactionHandler != null) { - rnFirebaseTransactionHandler.signalUpdateReceived(updatesReturned); - } - } - - @ReactMethod - public void on(final int refId, final String path, final ReadableArray modifiers, final int listenerId, final String eventName, final Callback callback) { - RNFirebaseDatabaseReference ref = this.getDBHandle(refId, path, modifiers); - - if (eventName.equals("value")) { - ref.addValueEventListener(listenerId); - } else { - ref.addChildEventListener(listenerId, eventName); - } - - WritableMap resp = Arguments.createMap(); - resp.putString("status", "success"); - resp.putInt("refId", refId); - resp.putString("handle", path); - callback.invoke(null, resp); - } - - @ReactMethod - public void once(final int refId, final String path, final ReadableArray modifiers, final String eventName, final Callback callback) { - RNFirebaseDatabaseReference ref = this.getDBHandle(refId, path, modifiers); - - if (eventName.equals("value")) { - ref.addOnceValueEventListener(callback); - } else { - ref.addChildOnceEventListener(eventName, callback); - } - } - - /** - * At the time of this writing, off() only gets called when there are no more subscribers to a given path. - * `mListeners` might therefore be out of sync (though javascript isnt listening for those eventNames, so - * it doesn't really matter- just polluting the RN bridge a little more than necessary. - * off() should therefore clean *everything* up - */ - @ReactMethod - public void off( - final int refId, - final ReadableArray listeners, - final Callback callback) { - - RNFirebaseDatabaseReference r = mReferences.get(refId); - - if (r != null) { - List listenersList = Utils.recursivelyDeconstructReadableArray(listeners); - - for (Object l : listenersList) { - Map listener = (Map) l; - int listenerId = ((Double) listener.get("listenerId")).intValue(); - String eventName = (String) listener.get("eventName"); - r.removeEventListener(listenerId, eventName); - if (!r.hasListeners()) { - mReferences.remove(refId); - } - } - } - - Log.d(TAG, "Removed listeners refId: " + refId + " ; count: " + listeners.size()); - WritableMap resp = Arguments.createMap(); - resp.putInt("refId", refId); - resp.putString("status", "success"); - callback.invoke(null, resp); - } - - @ReactMethod - public void onDisconnectSet(final String path, final ReadableMap props, final Callback callback) { + public void onDisconnectSet(String appName, String path, ReadableMap props, final Promise promise) { String type = props.getString("type"); - DatabaseReference ref = mFirebaseDatabase.getReference(path); - OnDisconnect od = ref.onDisconnect(); + DatabaseReference ref = getReferenceForAppPath(appName, path); + + OnDisconnect onDisconnect = ref.onDisconnect(); DatabaseReference.CompletionListener listener = new DatabaseReference.CompletionListener() { @Override public void onComplete(DatabaseError error, DatabaseReference ref) { - handleCallback("onDisconnectSet", callback, error); + handlePromise(promise, error); } }; switch (type) { case "object": Map map = Utils.recursivelyDeconstructReadableMap(props.getMap("value")); - od.setValue(map, listener); + onDisconnect.setValue(map, listener); break; case "array": List list = Utils.recursivelyDeconstructReadableArray(props.getArray("value")); - od.setValue(list, listener); + onDisconnect.setValue(list, listener); break; case "string": - od.setValue(props.getString("value"), listener); + onDisconnect.setValue(props.getString("value"), listener); break; case "number": - od.setValue(props.getDouble("value"), listener); + onDisconnect.setValue(props.getDouble("value"), listener); break; case "boolean": - od.setValue(props.getBoolean("value"), listener); + onDisconnect.setValue(props.getBoolean("value"), listener); break; case "null": - od.setValue(null, listener); + onDisconnect.setValue(null, listener); break; } } + /** + * Update a value on a ref when the client disconnects from the firebase server. + * + * @param appName + * @param path + * @param props + * @param promise + */ @ReactMethod - public void onDisconnectUpdate(final String path, final ReadableMap props, final Callback callback) { - DatabaseReference ref = mFirebaseDatabase.getReference(path); - OnDisconnect od = ref.onDisconnect(); + public void onDisconnectUpdate(String appName, String path, ReadableMap props, final Promise promise) { + DatabaseReference ref = getReferenceForAppPath(appName, path); + OnDisconnect ondDisconnect = ref.onDisconnect(); + Map map = Utils.recursivelyDeconstructReadableMap(props); - od.updateChildren(map, new DatabaseReference.CompletionListener() { + + ondDisconnect.updateChildren(map, new DatabaseReference.CompletionListener() { @Override public void onComplete(DatabaseError error, DatabaseReference ref) { - handleCallback("onDisconnectUpdate", callback, error); + handlePromise(promise, error); } }); } + /** + * Remove a ref when the client disconnects from the firebase server. + * + * @param appName + * @param path + * @param promise + */ @ReactMethod - public void onDisconnectRemove(final String path, final Callback callback) { - DatabaseReference ref = mFirebaseDatabase.getReference(path); + public void onDisconnectRemove(String appName, String path, final Promise promise) { + DatabaseReference ref = getReferenceForAppPath(appName, path); + OnDisconnect onDisconnect = ref.onDisconnect(); - OnDisconnect od = ref.onDisconnect(); - od.removeValue(new DatabaseReference.CompletionListener() { + onDisconnect.removeValue(new DatabaseReference.CompletionListener() { @Override public void onComplete(DatabaseError error, DatabaseReference ref) { - handleCallback("onDisconnectRemove", callback, error); + handlePromise(promise, error); } }); } + /** + * Cancel a pending onDisconnect action. + * + * @param appName + * @param path + * @param promise + */ @ReactMethod - public void onDisconnectCancel(final String path, final Callback callback) { - DatabaseReference ref = mFirebaseDatabase.getReference(path); + public void onDisconnectCancel(String appName, String path, final Promise promise) { + DatabaseReference ref = getReferenceForAppPath(appName, path); + OnDisconnect onDisconnect = ref.onDisconnect(); - OnDisconnect od = ref.onDisconnect(); - od.cancel(new DatabaseReference.CompletionListener() { + onDisconnect.cancel(new DatabaseReference.CompletionListener() { @Override public void onComplete(DatabaseError error, DatabaseReference ref) { - handleCallback("onDisconnectCancel", callback, error); + handlePromise(promise, error); } }); } + /** + * @param appName + * @param path + * @param props + * @param promise + */ @ReactMethod - public void goOnline() { - mFirebaseDatabase.goOnline(); + public void set(String appName, String path, ReadableMap props, final Promise promise) { + DatabaseReference ref = getReferenceForAppPath(appName, path); + Object value = Utils.recursivelyDeconstructReadableMap(props).get("value"); + + DatabaseReference.CompletionListener listener = new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + handlePromise(promise, error); + } + }; + + ref.setValue(value, listener); } + /** + * @param appName + * @param path + * @param priority + * @param promise + */ @ReactMethod - public void goOffline() { - mFirebaseDatabase.goOffline(); + public void setPriority(String appName, String path, ReadableMap priority, final Promise promise) { + DatabaseReference ref = getReferenceForAppPath(appName, path); + Object priorityValue = Utils.recursivelyDeconstructReadableMap(priority).get("value"); + + DatabaseReference.CompletionListener listener = new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + handlePromise(promise, error); + } + }; + + ref.setPriority(priorityValue, listener); } - private void handleCallback( - final String methodName, - final Callback callback, - final DatabaseError databaseError) { - if (databaseError != null) { - WritableMap err = Arguments.createMap(); - err.putInt("code", databaseError.getCode()); - err.putString("details", databaseError.getDetails()); - err.putString("description", databaseError.getMessage()); - callback.invoke(err); + /** + * @param appName + * @param path + * @param data + * @param priority + * @param promise + */ + @ReactMethod + public void setWithPriority(String appName, String path, ReadableMap data, ReadableMap priority, final Promise promise) { + DatabaseReference ref = getReferenceForAppPath(appName, path); + Object dataValue = Utils.recursivelyDeconstructReadableMap(data).get("value"); + Object priorityValue = Utils.recursivelyDeconstructReadableMap(priority).get("value"); + + DatabaseReference.CompletionListener listener = new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + handlePromise(promise, error); + } + }; + + ref.setValue(dataValue, priorityValue, listener); + } + + /** + * @param appName + * @param path + * @param props + * @param promise + */ + @ReactMethod + public void update(String appName, String path, ReadableMap props, final Promise promise) { + DatabaseReference ref = getReferenceForAppPath(appName, path); + Map updates = Utils.recursivelyDeconstructReadableMap(props); + + DatabaseReference.CompletionListener listener = new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + handlePromise(promise, error); + } + }; + + ref.updateChildren(updates, listener); + } + + /** + * @param appName + * @param path + * @param promise + */ + @ReactMethod + public void remove(String appName, String path, final Promise promise) { + DatabaseReference ref = getReferenceForAppPath(appName, path); + + DatabaseReference.CompletionListener listener = new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + handlePromise(promise, error); + } + }; + + ref.removeValue(listener); + } + + // Push no longer required, handled in JS now. + + /** + * Subcribe once to a firebase reference. + * + * @param appName + * @param refId + * @param path + * @param modifiers + * @param eventName + * @param promise + */ + @ReactMethod + public void once(String appName, int refId, String path, ReadableArray modifiers, String eventName, Promise promise) { + RNFirebaseDatabaseReference internalRef = getInternalReferenceForApp(appName, refId, path, modifiers, false); + + if (eventName.equals("value")) { + internalRef.addOnceValueEventListener(promise); } else { - WritableMap res = Arguments.createMap(); - res.putString("status", "success"); - res.putString("method", methodName); - callback.invoke(null, res); + internalRef.addChildOnceEventListener(eventName, promise); } } - private RNFirebaseDatabaseReference getDBHandle(final int refId, final String path, - final ReadableArray modifiers) { - RNFirebaseDatabaseReference r = mReferences.get(refId); - if (r == null) { - ReactContext ctx = getReactApplicationContext(); - r = new RNFirebaseDatabaseReference(ctx, mFirebaseDatabase, refId, path, modifiers); - mReferences.put(refId, r); + + + /* + * INTERNALS/UTILS + */ + + /** + * Resolve null or reject with a js like error if databaseError exists + * + * @param promise + * @param databaseError + */ + static void handlePromise(Promise promise, DatabaseError databaseError) { + if (databaseError != null) { + WritableMap jsError = getJSError(databaseError); + promise.reject( + jsError.getString("code"), + jsError.getString("message"), + databaseError.toException() + ); + } else { + promise.resolve(null); } + } - return r; + @Override + public String getName() { + return "RNFirebaseDatabase"; } @Override @@ -501,4 +433,130 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { constants.put("serverValueTimestamp", ServerValue.TIMESTAMP); return constants; } + + /** + * Get a database instance for a specific firebase app instance + * + * @param appName + * @return + */ + static FirebaseDatabase getDatabaseForApp(String appName) { + FirebaseApp firebaseApp = FirebaseApp.getInstance(appName); + return FirebaseDatabase.getInstance(firebaseApp); + } + + /** + * Get a database reference for a specific app and path + * + * @param appName + * @param path + * @return + */ + private DatabaseReference getReferenceForAppPath(String appName, String path) { + return getDatabaseForApp(appName).getReference(path); + } + + /** + * @param appName + * @param refId + * @param path + * @param modifiers + * @param keep + * @return + */ + private RNFirebaseDatabaseReference getInternalReferenceForApp(String appName, int refId, String path, ReadableArray modifiers, Boolean keep) { + RNFirebaseDatabaseReference existingRef = references.get(refId); + + if (existingRef == null) { + existingRef = new RNFirebaseDatabaseReference( + getReactApplicationContext(), + appName, + refId, + path, + modifiers + ); + + if (keep) references.put(refId, existingRef); + } + + return existingRef; + } + + // todo move to error util for use in other modules + static String getMessageWithService(String message, String service, String fullCode) { + // Service: Error message (service/code). + return service + ": " + message + " (" + fullCode.toLowerCase() + ")."; + } + + static String getCodeWithService(String service, String code) { + return service.toUpperCase() + "/" + code.toUpperCase(); + } + + static WritableMap getJSError(DatabaseError nativeError) { + WritableMap errorMap = Arguments.createMap(); + errorMap.putInt("nativeErrorCode", nativeError.getCode()); + errorMap.putString("nativeErrorMessage", nativeError.getMessage()); + + String code; + String message; + String service = "Database"; + + switch (nativeError.getCode()) { + case DatabaseError.DATA_STALE: + code = getCodeWithService(service, "data-stale"); + message = getMessageWithService("The transaction needs to be run again with current data.", service, code); + break; + case DatabaseError.OPERATION_FAILED: + code = getCodeWithService(service, "failure"); + message = getMessageWithService("The server indicated that this operation failed.", service, code); + break; + case DatabaseError.PERMISSION_DENIED: + code = getCodeWithService(service, "permission-denied"); + message = getMessageWithService("Client doesn't have permission to access the desired data.", service, code); + break; + case DatabaseError.DISCONNECTED: + code = getCodeWithService(service, "disconnected"); + message = getMessageWithService("The operation had to be aborted due to a network disconnect.", service, code); + break; + case DatabaseError.EXPIRED_TOKEN: + code = getCodeWithService(service, "expired-token"); + message = getMessageWithService("The supplied auth token has expired.", service, code); + break; + case DatabaseError.INVALID_TOKEN: + code = getCodeWithService(service, "invalid-token"); + message = getMessageWithService("The supplied auth token was invalid.", service, code); + break; + case DatabaseError.MAX_RETRIES: + code = getCodeWithService(service, "max-retries"); + message = getMessageWithService("The transaction had too many retries.", service, code); + break; + case DatabaseError.OVERRIDDEN_BY_SET: + code = getCodeWithService(service, "overridden-by-set"); + message = getMessageWithService("The transaction was overridden by a subsequent set.", service, code); + break; + case DatabaseError.UNAVAILABLE: + code = getCodeWithService(service, "unavailable"); + message = getMessageWithService("The service is unavailable.", service, code); + break; + case DatabaseError.USER_CODE_EXCEPTION: + code = getCodeWithService(service, "user-code-exception"); + message = getMessageWithService("User code called from the Firebase Database runloop threw an exception.", service, code); + break; + case DatabaseError.NETWORK_ERROR: + code = getCodeWithService(service, "network-error"); + message = getMessageWithService("The operation could not be performed due to a network error.", service, code); + break; + case DatabaseError.WRITE_CANCELED: + code = getCodeWithService(service, "write-cancelled"); + message = getMessageWithService("The write was canceled by the user.", service, code); + break; + default: + code = getCodeWithService(service, "unknown"); + message = getMessageWithService("An unknown error occurred", service, code); + } + + errorMap.putString("code", code); + errorMap.putString("message", message); + return errorMap; + } } diff --git a/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabaseOld.java b/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabaseOld.java new file mode 100644 index 00000000..116a36ca --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabaseOld.java @@ -0,0 +1,504 @@ +package io.invertase.firebase.database; + +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 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; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReadableArray; +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.DatabaseException; +import com.google.firebase.database.Transaction; + +import io.invertase.firebase.Utils; + +public class RNFirebaseDatabaseOld extends ReactContextBaseJavaModule { +// private static final String TAG = "RNFirebaseDatabase"; +// private HashMap mReferences = new HashMap<>(); +// private HashMap mTransactionHandlers = new HashMap<>(); +// private FirebaseDatabase mFirebaseDatabase; +// +// public RNFirebaseDatabaseOld(ReactApplicationContext reactContext) { +// super(reactContext); +// mFirebaseDatabase = FirebaseDatabase.getInstance(); +// } + +// @Override +// public String getName() { +// return TAG; +// } + +// // Persistence +// @ReactMethod +// public void enablePersistence( +// final Boolean enable, +// final Callback callback) { +// try { +// mFirebaseDatabase.setPersistenceEnabled(enable); +// } catch (DatabaseException t) { +// +// } +// +// WritableMap res = Arguments.createMap(); +// res.putString("status", "success"); +// callback.invoke(null, res); +// } + +// @ReactMethod +// public void keepSynced( +// final String path, +// final Boolean enable, +// final Callback callback) { +// DatabaseReference ref = mFirebaseDatabase.getReference(path); +// ref.keepSynced(enable); +// +// WritableMap res = Arguments.createMap(); +// res.putString("status", "success"); +// res.putString("path", path); +// callback.invoke(null, res); +// } + +// // RNFirebaseDatabase +// @ReactMethod +// public void set( +// final String path, +// final ReadableMap props, +// final Callback callback) { +// DatabaseReference ref = mFirebaseDatabase.getReference(path); +// Map m = Utils.recursivelyDeconstructReadableMap(props); +// +// DatabaseReference.CompletionListener listener = new DatabaseReference.CompletionListener() { +// @Override +// public void onComplete(DatabaseError error, DatabaseReference ref) { +// handleCallback("set", callback, error); +// } +// }; +// +// ref.setValue(m.get("value"), listener); +// } + +// @ReactMethod +// public void priority( +// final String path, +// final ReadableMap priority, +// final Callback callback) { +// DatabaseReference ref = mFirebaseDatabase.getReference(path); +// Map priorityMap = Utils.recursivelyDeconstructReadableMap(priority); +// +// DatabaseReference.CompletionListener listener = new DatabaseReference.CompletionListener() { +// @Override +// public void onComplete(DatabaseError error, DatabaseReference ref) { +// handleCallback("priority", callback, error); +// } +// }; +// +// ref.setPriority(priorityMap.get("value"), listener); +// } +// +// @ReactMethod +// public void withPriority( +// final String path, +// final ReadableMap data, +// final ReadableMap priority, +// final Callback callback) { +// DatabaseReference ref = mFirebaseDatabase.getReference(path); +// Map dataMap = Utils.recursivelyDeconstructReadableMap(data); +// Map priorityMap = Utils.recursivelyDeconstructReadableMap(priority); +// +// DatabaseReference.CompletionListener listener = new DatabaseReference.CompletionListener() { +// @Override +// public void onComplete(DatabaseError error, DatabaseReference ref) { +// handleCallback("withPriority", callback, error); +// } +// }; +// +// ref.setValue(dataMap.get("value"), priorityMap.get("value"), listener); +// } +// +// @ReactMethod +// public void update(final String path, +// final ReadableMap props, +// final Callback callback) { +// DatabaseReference ref = mFirebaseDatabase.getReference(path); +// Map m = Utils.recursivelyDeconstructReadableMap(props); +// +// DatabaseReference.CompletionListener listener = new DatabaseReference.CompletionListener() { +// @Override +// public void onComplete(DatabaseError error, DatabaseReference ref) { +// handleCallback("update", callback, error); +// } +// }; +// +// ref.updateChildren(m, listener); +// } + +// @ReactMethod +// public void remove(final String path, +// final Callback callback) { +// DatabaseReference ref = mFirebaseDatabase.getReference(path); +// DatabaseReference.CompletionListener listener = new DatabaseReference.CompletionListener() { +// @Override +// public void onComplete(DatabaseError error, DatabaseReference ref) { +// handleCallback("remove", callback, error); +// } +// }; +// +// ref.removeValue(listener); +// } + +// @ReactMethod +// public void push(final String path, +// final ReadableMap props, +// final Callback callback) { +// +// Log.d(TAG, "Called push with " + path); +// DatabaseReference ref = mFirebaseDatabase.getReference(path); +// DatabaseReference newRef = ref.push(); +// +// final Uri url = Uri.parse(newRef.toString()); +// final String newPath = url.getPath(); +// +// ReadableMapKeySetIterator iterator = props.keySetIterator(); +// if (iterator.hasNextKey()) { +// Log.d(TAG, "Passed value to push"); +// // lame way to check if the `props` are empty +// Map m = Utils.recursivelyDeconstructReadableMap(props); +// +// DatabaseReference.CompletionListener listener = new DatabaseReference.CompletionListener() { +// @Override +// public void onComplete(DatabaseError error, DatabaseReference ref) { +// if (error != null) { +// WritableMap err = Arguments.createMap(); +// err.putInt("code", error.getCode()); +// err.putString("details", error.getDetails()); +// err.putString("description", error.getMessage()); +// callback.invoke(err); +// } else { +// WritableMap res = Arguments.createMap(); +// res.putString("status", "success"); +// res.putString("ref", newPath); +// callback.invoke(null, res); +// } +// } +// }; +// +// newRef.setValue(m.get("value"), listener); +// } else { +// Log.d(TAG, "No value passed to push: " + newPath); +// WritableMap res = Arguments.createMap(); +// res.putString("status", "success"); +// res.putString("ref", newPath); +// callback.invoke(null, res); +// } +// } + +// /** +// * @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 updatesReturned = Utils.recursivelyDeconstructReadableMap(updates); +// RNFirebaseTransactionHandler rnFirebaseTransactionHandler = mTransactionHandlers.get(id); +// +// if (rnFirebaseTransactionHandler != null) { +// rnFirebaseTransactionHandler.signalUpdateReceived(updatesReturned); +// } +// } + + @ReactMethod + public void on(final int refId, final String path, final ReadableArray modifiers, final int listenerId, final String eventName, final Callback callback) { + RNFirebaseDatabaseReference ref = this.getDBHandle(refId, path, modifiers); + + if (eventName.equals("value")) { + ref.addValueEventListener(listenerId); + } else { + ref.addChildEventListener(listenerId, eventName); + } + + WritableMap resp = Arguments.createMap(); + resp.putString("status", "success"); + resp.putInt("refId", refId); + resp.putString("handle", path); + callback.invoke(null, resp); + } + +// @ReactMethod +// public void once(final int refId, final String path, final ReadableArray modifiers, final String eventName, final Callback callback) { +// RNFirebaseDatabaseReference ref = this.getDBHandle(refId, path, modifiers); +// +// if (eventName.equals("value")) { +// ref.addOnceValueEventListener(callback); +// } else { +// ref.addChildOnceEventListener(eventName, callback); +// } +// } + + /** + * At the time of this writing, off() only gets called when there are no more subscribers to a given path. + * `mListeners` might therefore be out of sync (though javascript isnt listening for those eventNames, so + * it doesn't really matter- just polluting the RN bridge a little more than necessary. + * off() should therefore clean *everything* up + */ + @ReactMethod + public void off( + final int refId, + final ReadableArray listeners, + final Callback callback) { + + RNFirebaseDatabaseReference r = mReferences.get(refId); + + if (r != null) { + List listenersList = Utils.recursivelyDeconstructReadableArray(listeners); + + for (Object l : listenersList) { + Map listener = (Map) l; + int listenerId = ((Double) listener.get("listenerId")).intValue(); + String eventName = (String) listener.get("eventName"); + r.removeEventListener(listenerId, eventName); + if (!r.hasListeners()) { + mReferences.remove(refId); + } + } + } + + Log.d(TAG, "Removed listeners refId: " + refId + " ; count: " + listeners.size()); + WritableMap resp = Arguments.createMap(); + resp.putInt("refId", refId); + resp.putString("status", "success"); + callback.invoke(null, resp); + } + +// @ReactMethod +// public void onDisconnectSet(final String path, final ReadableMap props, final Callback callback) { +// String type = props.getString("type"); +// DatabaseReference ref = mFirebaseDatabase.getReference(path); +// OnDisconnect od = ref.onDisconnect(); +// DatabaseReference.CompletionListener listener = new DatabaseReference.CompletionListener() { +// @Override +// public void onComplete(DatabaseError error, DatabaseReference ref) { +// handleCallback("onDisconnectSet", callback, error); +// } +// }; +// +// switch (type) { +// case "object": +// Map map = Utils.recursivelyDeconstructReadableMap(props.getMap("value")); +// od.setValue(map, listener); +// break; +// case "array": +// List list = Utils.recursivelyDeconstructReadableArray(props.getArray("value")); +// od.setValue(list, listener); +// break; +// case "string": +// od.setValue(props.getString("value"), listener); +// break; +// case "number": +// od.setValue(props.getDouble("value"), listener); +// break; +// case "boolean": +// od.setValue(props.getBoolean("value"), listener); +// break; +// case "null": +// od.setValue(null, listener); +// break; +// } +// } +// +// @ReactMethod +// public void onDisconnectUpdate(final String path, final ReadableMap props, final Callback callback) { +// DatabaseReference ref = mFirebaseDatabase.getReference(path); +// OnDisconnect od = ref.onDisconnect(); +// Map map = Utils.recursivelyDeconstructReadableMap(props); +// od.updateChildren(map, new DatabaseReference.CompletionListener() { +// @Override +// public void onComplete(DatabaseError error, DatabaseReference ref) { +// handleCallback("onDisconnectUpdate", callback, error); +// } +// }); +// } +// +// @ReactMethod +// public void onDisconnectRemove(final String path, final Callback callback) { +// DatabaseReference ref = mFirebaseDatabase.getReference(path); +// +// OnDisconnect od = ref.onDisconnect(); +// od.removeValue(new DatabaseReference.CompletionListener() { +// @Override +// public void onComplete(DatabaseError error, DatabaseReference ref) { +// handleCallback("onDisconnectRemove", callback, error); +// } +// }); +// } +// +// @ReactMethod +// public void onDisconnectCancel(final String path, final Callback callback) { +// DatabaseReference ref = mFirebaseDatabase.getReference(path); +// +// OnDisconnect od = ref.onDisconnect(); +// od.cancel(new DatabaseReference.CompletionListener() { +// @Override +// public void onComplete(DatabaseError error, DatabaseReference ref) { +// handleCallback("onDisconnectCancel", callback, error); +// } +// }); +// } + +// @ReactMethod +// public void goOnline() { +// mFirebaseDatabase.goOnline(); +// } +// +// @ReactMethod +// public void goOffline() { +// mFirebaseDatabase.goOffline(); +// } + +// private void handleCallback( +// final String methodName, +// final Callback callback, +// final DatabaseError databaseError) { +// if (databaseError != null) { +// WritableMap err = Arguments.createMap(); +// err.putInt("code", databaseError.getCode()); +// err.putString("details", databaseError.getDetails()); +// err.putString("description", databaseError.getMessage()); +// callback.invoke(err); +// } else { +// WritableMap res = Arguments.createMap(); +// res.putString("status", "success"); +// res.putString("method", methodName); +// callback.invoke(null, res); +// } +// } + +// private RNFirebaseDatabaseReference getDBHandle(final int refId, final String path, +// final ReadableArray modifiers) { +// RNFirebaseDatabaseReference r = mReferences.get(refId); +// +// if (r == null) { +// ReactContext ctx = getReactApplicationContext(); +// r = new RNFirebaseDatabaseReference(ctx, mFirebaseDatabase, refId, path, modifiers); +// mReferences.put(refId, r); +// } +// +// return r; +// } + +// @Override +// public Map getConstants() { +// final Map constants = new HashMap<>(); +// constants.put("serverValueTimestamp", ServerValue.TIMESTAMP); +// return constants; +// } +} diff --git a/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabaseReference.java b/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabaseReference.java index d1374514..c05f1cf2 100644 --- a/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabaseReference.java +++ b/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabaseReference.java @@ -7,8 +7,8 @@ import android.util.Log; import android.support.annotation.Nullable; import android.util.SparseArray; -import com.facebook.react.bridge.Callback; import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReadableArray; @@ -25,28 +25,125 @@ import io.invertase.firebase.Utils; public class RNFirebaseDatabaseReference { private static final String TAG = "RNFirebaseDBReference"; - private int mRefId; - private String mPath; - private Query mQuery; - private ReactContext mReactContext; - private SparseArray mChildEventListeners; - private SparseArray mValueEventListeners; + private int refId; + private Query query; + private String path; + private String appName; + private ReactContext reactContext; + private SparseArray childEventListeners; + private SparseArray valueEventListeners; - RNFirebaseDatabaseReference(final ReactContext context, - final FirebaseDatabase firebaseDatabase, - final int refId, - final String path, - final ReadableArray modifiersArray) { - mPath = path; - mRefId = refId; - mReactContext = context; - mChildEventListeners = new SparseArray(); - mValueEventListeners = new SparseArray(); - mQuery = this.buildDatabaseQueryAtPathAndModifiers(firebaseDatabase, path, modifiersArray); + /** + * @param context + * @param app + * @param id + * @param refPath + * @param modifiersArray + */ + RNFirebaseDatabaseReference(ReactContext context, String app, int id, String refPath, ReadableArray modifiersArray) { + refId = id; + appName = app; + path = refPath; + reactContext = context; + + // todo only create if needed + childEventListeners = new SparseArray(); + valueEventListeners = new SparseArray(); + + query = buildDatabaseQueryAtPathAndModifiers(path, modifiersArray); } + /** + * Listen for a single 'value' event from firebase. + * @param promise + */ + void addOnceValueEventListener(final Promise promise) { + ValueEventListener onceValueEventListener = new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot dataSnapshot) { + WritableMap data = Utils.snapshotToMap("value", path, dataSnapshot, null, refId, 0); + promise.resolve(data); + } + + @Override + public void onCancelled(DatabaseError error) { + RNFirebaseDatabase.handlePromise(promise, error); + } + }; + + query.addListenerForSingleValueEvent(onceValueEventListener); + + Log.d(TAG, "Added OnceValueEventListener for refId: " + refId); + } + + /** + * Listen for single 'child_X' event from firebase. + * @param eventName + * @param promise + */ + void addChildOnceEventListener(final String eventName, final Promise promise) { + ChildEventListener childEventListener = new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot dataSnapshot, String previousChildName) { + if ("child_added".equals(eventName)) { + query.removeEventListener(this); + WritableMap data = Utils.snapshotToMap("child_added", path, dataSnapshot, previousChildName, refId, 0); + promise.resolve(data); + } + } + + @Override + public void onChildChanged(DataSnapshot dataSnapshot, String previousChildName) { + if ("child_changed".equals(eventName)) { + query.removeEventListener(this); + WritableMap data = Utils.snapshotToMap("child_changed", path, dataSnapshot, previousChildName, refId, 0); + promise.resolve(data); + } + } + + @Override + public void onChildRemoved(DataSnapshot dataSnapshot) { + if ("child_removed".equals(eventName)) { + query.removeEventListener(this); + WritableMap data = Utils.snapshotToMap("child_removed", path, dataSnapshot, null, refId, 0); + promise.resolve(data); + } + } + + @Override + public void onChildMoved(DataSnapshot dataSnapshot, String previousChildName) { + if ("child_moved".equals(eventName)) { + query.removeEventListener(this); + WritableMap data = Utils.snapshotToMap("child_moved", path, dataSnapshot, previousChildName, refId, 0); + promise.resolve(data); + } + } + + @Override + public void onCancelled(DatabaseError error) { + query.removeEventListener(this); + RNFirebaseDatabase.handlePromise(promise, error); + } + }; + + query.addChildEventListener(childEventListener); + } + + + + + + + + + + + + + // todo cleanup all below + void addChildEventListener(final int listenerId, final String eventName) { - if (mChildEventListeners.get(listenerId) != null) { + if (childEventListeners.get(listenerId) != null) { ChildEventListener childEventListener = new ChildEventListener() { @Override public void onChildAdded(DataSnapshot dataSnapshot, String previousChildName) { @@ -83,16 +180,16 @@ public class RNFirebaseDatabaseReference { } }; - mChildEventListeners.put(listenerId, childEventListener); - mQuery.addChildEventListener(childEventListener); - Log.d(TAG, "Added ChildEventListener for refId: " + mRefId + " listenerId: " + listenerId); + childEventListeners.put(listenerId, childEventListener); + query.addChildEventListener(childEventListener); + Log.d(TAG, "Added ChildEventListener for refId: " + refId + " listenerId: " + listenerId); } else { - Log.d(TAG, "ChildEventListener for refId: " + mRefId + " listenerId: " + listenerId + " already exists"); + Log.d(TAG, "ChildEventListener for refId: " + refId + " listenerId: " + listenerId + " already exists"); } } void addValueEventListener(final int listenerId) { - if (mValueEventListeners.get(listenerId) != null) { + if (valueEventListeners.get(listenerId) != null) { ValueEventListener valueEventListener = new ValueEventListener() { @Override public void onDataChange(DataSnapshot dataSnapshot) { @@ -106,91 +203,15 @@ public class RNFirebaseDatabaseReference { } }; - mValueEventListeners.put(listenerId, valueEventListener); - mQuery.addValueEventListener(valueEventListener); - Log.d(TAG, "Added ValueEventListener for refId: " + mRefId + " listenerId: " + listenerId); + valueEventListeners.put(listenerId, valueEventListener); + query.addValueEventListener(valueEventListener); + Log.d(TAG, "Added ValueEventListener for refId: " + refId + " listenerId: " + listenerId); } else { - Log.d(TAG, "ValueEventListener for refId: " + mRefId + " listenerId: " + listenerId + " already exists"); + Log.d(TAG, "ValueEventListener for refId: " + refId + " listenerId: " + listenerId + " already exists"); } } - void addOnceValueEventListener(final Callback callback) { - final ValueEventListener onceValueEventListener = new ValueEventListener() { - @Override - public void onDataChange(DataSnapshot dataSnapshot) { - WritableMap data = Utils.snapshotToMap("value", mRefId, null, mPath, dataSnapshot, null); - callback.invoke(null, data); - } - @Override - public void onCancelled(DatabaseError error) { - WritableMap err = Arguments.createMap(); - err.putInt("refId", mRefId); - err.putString("path", mPath); - err.putInt("code", error.getCode()); - err.putString("details", error.getDetails()); - err.putString("message", error.getMessage()); - callback.invoke(err); - } - }; - - mQuery.addListenerForSingleValueEvent(onceValueEventListener); - Log.d(TAG, "Added OnceValueEventListener for refId: " + mRefId); - } - - void addChildOnceEventListener(final String eventName, final Callback callback) { - ChildEventListener childEventListener = new ChildEventListener() { - @Override - public void onChildAdded(DataSnapshot dataSnapshot, String previousChildName) { - if ("child_added".equals(eventName)) { - mQuery.removeEventListener(this); - WritableMap data = Utils.snapshotToMap("child_added", mRefId, null, mPath, dataSnapshot, previousChildName); - callback.invoke(null, data); - } - } - - @Override - public void onChildChanged(DataSnapshot dataSnapshot, String previousChildName) { - if ("child_changed".equals(eventName)) { - mQuery.removeEventListener(this); - WritableMap data = Utils.snapshotToMap("child_changed", mRefId, null, mPath, dataSnapshot, previousChildName); - callback.invoke(null, data); - } - } - - @Override - public void onChildRemoved(DataSnapshot dataSnapshot) { - if ("child_removed".equals(eventName)) { - mQuery.removeEventListener(this); - WritableMap data = Utils.snapshotToMap("child_removed", mRefId, null, mPath, dataSnapshot, null); - callback.invoke(null, data); - } - } - - @Override - public void onChildMoved(DataSnapshot dataSnapshot, String previousChildName) { - if ("child_moved".equals(eventName)) { - mQuery.removeEventListener(this); - WritableMap data = Utils.snapshotToMap("child_moved", mRefId, null, mPath, dataSnapshot, previousChildName); - callback.invoke(null, data); - } - } - - @Override - public void onCancelled(DatabaseError error) { - mQuery.removeEventListener(this); - WritableMap err = Arguments.createMap(); - err.putInt("refId", mRefId); - err.putString("path", mPath); - err.putInt("code", error.getCode()); - err.putString("details", error.getDetails()); - err.putString("message", error.getMessage()); - callback.invoke(err); - } - }; - - mQuery.addChildEventListener(childEventListener); - } void removeEventListener(int listenerId, String eventName) { if ("value".equals(eventName)) { @@ -201,7 +222,7 @@ public class RNFirebaseDatabaseReference { } boolean hasListeners() { - return mChildEventListeners.size() > 0 || mValueEventListeners.size() > 0; + return childEventListeners.size() > 0 || valueEventListeners.size() > 0; } public void cleanup() { @@ -211,51 +232,52 @@ public class RNFirebaseDatabaseReference { } private void removeChildEventListener(Integer listenerId) { - ChildEventListener listener = mChildEventListeners.get(listenerId); + ChildEventListener listener = childEventListeners.get(listenerId); if (listener != null) { - mQuery.removeEventListener(listener); - mChildEventListeners.delete(listenerId); + query.removeEventListener(listener); + childEventListeners.delete(listenerId); } } private void removeValueEventListener(Integer listenerId) { - ValueEventListener listener = mValueEventListeners.get(listenerId); + ValueEventListener listener = valueEventListeners.get(listenerId); if (listener != null) { - mQuery.removeEventListener(listener); - mValueEventListeners.delete(listenerId); + query.removeEventListener(listener); + valueEventListeners.delete(listenerId); } } private void handleDatabaseEvent(final String name, final Integer listenerId, final DataSnapshot dataSnapshot, @Nullable String previousChildName) { - WritableMap data = Utils.snapshotToMap(name, mRefId, listenerId, mPath, dataSnapshot, previousChildName); + WritableMap data = Utils.snapshotToMap(name, path, dataSnapshot, previousChildName, refId, listenerId); WritableMap evt = Arguments.createMap(); evt.putString("eventName", name); evt.putMap("body", data); - Utils.sendEvent(mReactContext, "database_event", evt); + Utils.sendEvent(reactContext, "database_event", evt); } private void handleDatabaseError(final Integer listenerId, final DatabaseError error) { WritableMap errMap = Arguments.createMap(); - errMap.putInt("refId", mRefId); + errMap.putInt("refId", refId); if (listenerId != null) { errMap.putInt("listenerId", listenerId); } - errMap.putString("path", mPath); + errMap.putString("path", path); errMap.putInt("code", error.getCode()); errMap.putString("details", error.getDetails()); errMap.putString("message", error.getMessage()); - Utils.sendEvent(mReactContext, "database_error", errMap); + Utils.sendEvent(reactContext, "database_error", errMap); } - private Query buildDatabaseQueryAtPathAndModifiers(final FirebaseDatabase firebaseDatabase, - final String path, - final ReadableArray modifiers) { + private Query buildDatabaseQueryAtPathAndModifiers(String path, ReadableArray modifiers) { + FirebaseDatabase firebaseDatabase = RNFirebaseDatabase.getDatabaseForApp(appName); + Query query = firebaseDatabase.getReference(path); List modifiersList = Utils.recursivelyDeconstructReadableArray(modifiers); + // todo cleanup into utils for (Object m : modifiersList) { Map modifier = (Map) m; String type = (String) modifier.get("type"); diff --git a/android/src/main/java/io/invertase/firebase/database/RNFirebaseTransactionHandler.java b/android/src/main/java/io/invertase/firebase/database/RNFirebaseTransactionHandler.java index 7af403d8..19022e98 100644 --- a/android/src/main/java/io/invertase/firebase/database/RNFirebaseTransactionHandler.java +++ b/android/src/main/java/io/invertase/firebase/database/RNFirebaseTransactionHandler.java @@ -1,22 +1,39 @@ package io.invertase.firebase.database; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeArray; +import com.google.firebase.database.DataSnapshot; +import com.google.firebase.database.DatabaseError; +import com.google.firebase.database.MutableData; + import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; +import javax.annotation.Nullable; + +import io.invertase.firebase.Utils; public class RNFirebaseTransactionHandler { + private int transactionId; + private String appName; private final ReentrantLock lock; private final Condition condition; private Map data; - private volatile boolean isReady; + private boolean signalled; public Object value; - public boolean interrupted; - public boolean abort = false; + boolean interrupted; + boolean abort = false; + boolean timeout = false; - RNFirebaseTransactionHandler() { + RNFirebaseTransactionHandler(int id, String app) { + appName = app; + transactionId = id; lock = new ReentrantLock(); condition = lock.newCondition(); } @@ -24,19 +41,22 @@ public class RNFirebaseTransactionHandler { /** * Signal that the transaction data has been received * - * @param updateData + * @param updates */ - public void signalUpdateReceived(Map updateData) { - lock.lock(); + void signalUpdateReceived(ReadableMap updates) { + Map updateData = Utils.recursivelyDeconstructReadableMap(updates); - abort = (Boolean) updateData.get("abort"); + lock.lock(); value = updateData.get("value"); + abort = (Boolean) updateData.get("abort"); try { - if (isReady) - throw new IllegalStateException("This transactionUpdateCallback has already been called."); + if (signalled) { + throw new IllegalStateException("This transactionUpdateHandler has already been signalled."); + } + + signalled = true; data = updateData; - isReady = true; condition.signalAll(); } finally { lock.unlock(); @@ -44,16 +64,20 @@ public class RNFirebaseTransactionHandler { } /** - * Wait for transactionUpdateReceived to signal condition + * Wait for signalUpdateReceived to signal condition + * * @throws InterruptedException */ void await() throws InterruptedException { lock.lock(); - Boolean notTimedOut = false; + + long timeoutExpired = System.currentTimeMillis() + 5000; try { - while (!notTimedOut && !isReady) { - notTimedOut = condition.await(30, TimeUnit.SECONDS); + while (!timeout && !condition.await(250, TimeUnit.MILLISECONDS) && !signalled) { + if (!signalled && System.currentTimeMillis() > timeoutExpired) { + timeout = true; + } } } finally { lock.unlock(); @@ -62,9 +86,68 @@ public class RNFirebaseTransactionHandler { /** * Get the + * * @return */ Map getUpdates() { return data; } + + /** + * Create a RN map of transaction mutable data for sending to js + * + * @param updatesData + * @return + */ + WritableMap createUpdateMap(MutableData updatesData) { + final WritableMap updatesMap = Arguments.createMap(); + + updatesMap.putInt("id", transactionId); + updatesMap.putString("type", "update"); + + // all events get distributed js side based on app name + updatesMap.putString("appName", appName); + + if (!updatesData.hasChildren()) { + Utils.mapPutValue("value", updatesData.getValue(), updatesMap); + } else { + Object value = Utils.castValue(updatesData); + + if (value instanceof WritableNativeArray) { + updatesMap.putArray("value", (WritableArray) value); + } else { + updatesMap.putMap("value", (WritableMap) value); + } + } + + return updatesMap; + } + + + WritableMap createResultMap(@Nullable DatabaseError error, boolean committed, DataSnapshot snapshot) { + WritableMap resultMap = Arguments.createMap(); + + resultMap.putInt("id", transactionId); + resultMap.putString("appName", appName); + + resultMap.putBoolean("timeout", timeout); + resultMap.putBoolean("committed", committed); + resultMap.putBoolean("interrupted", interrupted); + + if (error != null || timeout || interrupted) { + resultMap.putString("type", "error"); + if (error != null) resultMap.putMap("error", RNFirebaseDatabase.getJSError(error)); + if (error == null && timeout) { + WritableMap timeoutError = Arguments.createMap(); + timeoutError.putString("code", "DATABASE/INTERNAL-TIMEOUT"); + timeoutError.putString("message", "A timeout occurred whilst waiting for RN JS thread to send transaction updates."); + resultMap.putMap("error", timeoutError); + } + } else { + resultMap.putString("type", "complete"); + resultMap.putMap("snapshot", Utils.snapshotToMap(snapshot)); + } + + return resultMap; + } } diff --git a/ios/RNFirebase/auth/RNFirebaseAuth.m b/ios/RNFirebase/auth/RNFirebaseAuth.m index c8821a25..042f2eb3 100644 --- a/ios/RNFirebase/auth/RNFirebaseAuth.m +++ b/ios/RNFirebase/auth/RNFirebaseAuth.m @@ -777,7 +777,7 @@ RCT_EXPORT_METHOD(fetchProvidersForEmail: } else if ([provider compare:@"google" options:NSCaseInsensitiveSearch] == NSOrderedSame) { credential = [FIRGoogleAuthProvider credentialWithIDToken:authToken accessToken:authTokenSecret]; } else if ([provider compare:@"password" options:NSCaseInsensitiveSearch] == NSOrderedSame) { - credential = [FIREmailPasswordAuthProvider credentialWithEmail:authToken password:authTokenSecret]; + credential = [FIREmailAuthProvider credentialWithEmail:authToken password:authTokenSecret]; } else if ([provider compare:@"github" options:NSCaseInsensitiveSearch] == NSOrderedSame) { credential = [FIRGitHubAuthProvider credentialWithToken:authToken]; } else { diff --git a/lib/modules/database/disconnect.js b/lib/modules/database/disconnect.js index 8b5c1f93..68e6ff79 100644 --- a/lib/modules/database/disconnect.js +++ b/lib/modules/database/disconnect.js @@ -1,10 +1,9 @@ /* @flow */ import { NativeModules } from 'react-native'; -import { promisify, typeOf } from './../../utils'; +import { typeOf } from './../../utils'; import Reference from './reference'; -const FirebaseDatabase = NativeModules.RNFirebaseDatabase; /** * @url https://firebase.google.com/docs/reference/js/firebase.database.OnDisconnect @@ -28,7 +27,7 @@ export default class Disconnect { * @returns {*} */ set(value: string | Object) { - return promisify('onDisconnectSet', FirebaseDatabase)(this.path, { type: typeOf(value), value }); + return this.database._native.onDisconnectSet(this.path, { type: typeOf(value), value }); } /** @@ -37,7 +36,7 @@ export default class Disconnect { * @returns {*} */ update(values: Object) { - return promisify('onDisconnectUpdate', FirebaseDatabase)(this.path, values); + return this.database._native.onDisconnectUpdate(this.path, values); } /** @@ -45,7 +44,7 @@ export default class Disconnect { * @returns {*} */ remove() { - return promisify('onDisconnectRemove', FirebaseDatabase)(this.path); + return this.database._native.onDisconnectRemove(this.path); } /** @@ -53,6 +52,6 @@ export default class Disconnect { * @returns {*} */ cancel() { - return promisify('onDisconnectCancel', FirebaseDatabase)(this.path); + return this.database._native.onDisconnectCancel(this.path); } } diff --git a/lib/modules/database/index.js b/lib/modules/database/index.js index 63fa7005..593c9cbb 100644 --- a/lib/modules/database/index.js +++ b/lib/modules/database/index.js @@ -4,45 +4,37 @@ */ import { NativeModules } from 'react-native'; -import ModuleBase from './../../utils/ModuleBase'; -import Snapshot from './snapshot'; import Reference from './reference'; import TransactionHandler from './transaction'; -import { promisify } from './../../utils'; +import ModuleBase from './../../utils/ModuleBase'; /** * @class Database */ -// TODO refactor native and js - legacy code here using old fb methods + export default class Database extends ModuleBase { constructor(firebaseApp: Object, options: Object = {}) { super(firebaseApp, options, 'Database', true); - this.references = {}; - this.serverTimeOffset = 0; - this.persistenceEnabled = false; - this.transaction = new TransactionHandler(this); + this._transactionHandler = new TransactionHandler(this); + if (this._options.persistence) this._native.setPersistence(this._options.persistence); - if (options.persistence === true) { - this._setPersistence(true); - } + // todo event & error listeners + // todo serverTimeOffset event/listener - make ref natively and switch to events + // todo use nativeToJSError for on/off error events + } - this.successListener = this._eventEmitter.addListener( - 'database_event', - event => this._handleDatabaseEvent(event), - ); + /** + * + */ + goOnline() { + this._native.goOnline(); + } - this.errorListener = this._eventEmitter.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); + /** + * + */ + goOffline() { + this._native.goOffline(); } /** @@ -53,146 +45,6 @@ export default class Database extends ModuleBase { ref(path: string) { return new Reference(this, path); } - - /** - * - * @returns {*} - * @param ref - * @param listener - */ - on(ref: Reference, listener: DatabaseListener) { - const { refId, path, query } = ref; - const { listenerId, eventName } = listener; - this.log.debug('on() : ', ref.refId, listenerId, eventName); - this.references[refId] = ref; - return promisify('on', this._native)(refId, path, query.getModifiers(), listenerId, eventName); - } - - /** - * - * @returns {*} - * @param refId - * @param listeners - * @param remainingListenersCount - */ - off(refId: number, listeners: Array, remainingListenersCount: number) { - this.log.debug('off() : ', refId, listeners); - - // Delete the reference if there are no more listeners - if (remainingListenersCount === 0) delete this.references[refId]; - - if (listeners.length === 0) return Promise.resolve(); - - return promisify('off', this._native)(refId, listeners.map(listener => ({ - listenerId: listener.listenerId, - eventName: listener.eventName, - }))); - } - - /** - * Removes all references and their native listeners - * @returns {Promise.<*>} - */ - cleanup() { - const promises = []; - Object.keys(this.references).forEach((refId) => { - const ref = this.references[refId]; - promises.push(this.off(Number(refId), Object.values(ref.refListeners), 0)); - }); - return Promise.all(promises); - } - - goOnline() { - this._native.goOnline(); - } - - goOffline() { - this._native.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', this._native)(enable); - } - - return Promise.reject({ status: 'Already enabled' }); - } - - /** - * - * @param event - * @private - */ - _handleDatabaseEvent(event: Object) { - const body = event.body || {}; - const { refId, listenerId, path, eventName, snapshot, previousChildName } = body; - this.log.debug('_handleDatabaseEvent: ', refId, listenerId, path, eventName, snapshot && snapshot.key); - if (this.references[refId] && this.references[refId].refListeners[listenerId]) { - const cb = this.references[refId].refListeners[listenerId].successCallback; - cb(new Snapshot(this.references[refId], snapshot), previousChildName); - } else { - this._native.off(refId, [{ listenerId, eventName }], () => { - this.log.debug('_handleDatabaseEvent: No JS listener registered, removed native listener', refId, listenerId, 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`; - } - - // $FlowFixMe - const firebaseError: 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 { refId, listenerId, path } = error; - const firebaseError = this._toFirebaseError(error); - - this.log.debug('_handleDatabaseError ->', refId, listenerId, path, 'database_error', error); - - if (this.references[refId] && this.references[refId].refListeners[listenerId]) { - const failureCb = this.references[refId].refListeners[listenerId].failureCallback; - if (failureCb) failureCb(firebaseError); - } - } } export const statics = { diff --git a/lib/modules/database/index.old.js b/lib/modules/database/index.old.js new file mode 100644 index 00000000..b02316b0 --- /dev/null +++ b/lib/modules/database/index.old.js @@ -0,0 +1,194 @@ +/** + * @flow + * Database representation wrapper + */ +import { NativeModules } from 'react-native'; + +import ModuleBase from './../../utils/ModuleBase'; +import Snapshot from './snapshot'; +import Reference from './reference'; +import TransactionHandler from './transaction'; +import { promisify } from './../../utils'; + +/** + * @class Database + */ +// TODO refactor native and js - legacy code here using old fb methods +export default class Database extends ModuleBase { + constructor(firebaseApp: Object, options: Object = {}) { + super(firebaseApp, options, 'Database', true); + this.references = {}; + this.serverTimeOffset = 0; + this.persistenceEnabled = false; + this.transaction = new TransactionHandler(this); + + if (options.persistence === true) { + this._setPersistence(true); + } + + this.successListener = this._eventEmitter.addListener( + 'database_event', + event => this._handleDatabaseEvent(event), + ); + + this.errorListener = this._eventEmitter.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); + } + + + /** + * TODO wtf is this doing here ;p + * @returns {*} + * @param ref + * @param listener + */ + on(ref: Reference, listener: DatabaseListener) { + const { refId, path, query } = ref; + const { listenerId, eventName } = listener; + this.log.debug('on() : ', ref.refId, listenerId, eventName); + this.references[refId] = ref; + return promisify('on', this._native)(refId, path, query.getModifiers(), listenerId, eventName); + } + + /** + * + * @returns {*} + * @param refId + * @param listeners + * @param remainingListenersCount + */ + off(refId: number, listeners: Array, remainingListenersCount: number) { + this.log.debug('off() : ', refId, listeners); + + // Delete the reference if there are no more listeners + if (remainingListenersCount === 0) delete this.references[refId]; + + if (listeners.length === 0) return Promise.resolve(); + + return promisify('off', this._native)(refId, listeners.map(listener => ({ + listenerId: listener.listenerId, + eventName: listener.eventName, + }))); + } + + /** + * Removes all references and their native listeners + * @returns {Promise.<*>} + */ + cleanup() { + const promises = []; + Object.keys(this.references).forEach((refId) => { + const ref = this.references[refId]; + promises.push(this.off(Number(refId), Object.values(ref.refListeners), 0)); + }); + return Promise.all(promises); + } + + // goOnline() { + // this._native.goOnline(); + // } + // + // goOffline() { + // this._native.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', this._native)(enable); + // } + // + // return Promise.reject({ status: 'Already enabled' }); + // } + + /** + * + * @param event + * @private + */ + _handleDatabaseEvent(event: Object) { + const body = event.body || {}; + const { refId, listenerId, path, eventName, snapshot, previousChildName } = body; + this.log.debug('_handleDatabaseEvent: ', refId, listenerId, path, eventName, snapshot && snapshot.key); + if (this.references[refId] && this.references[refId].refListeners[listenerId]) { + const cb = this.references[refId].refListeners[listenerId].successCallback; + cb(new Snapshot(this.references[refId], snapshot), previousChildName); + } else { + this._native.off(refId, [{ listenerId, eventName }], () => { + this.log.debug('_handleDatabaseEvent: No JS listener registered, removed native listener', refId, listenerId, 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`; + } + + // $FlowFixMe + const firebaseError: 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 { refId, listenerId, path } = error; + const firebaseError = this._toFirebaseError(error); + + this.log.debug('_handleDatabaseError ->', refId, listenerId, path, 'database_error', error); + + if (this.references[refId] && this.references[refId].refListeners[listenerId]) { + const failureCb = this.references[refId].refListeners[listenerId].failureCallback; + if (failureCb) failureCb(firebaseError); + } + } +} + +export const statics = { + ServerValue: NativeModules.FirebaseDatabase ? { + TIMESTAMP: NativeModules.FirebaseDatabase.serverValueTimestamp || { '.sv': 'timestamp' }, + } : {}, +}; diff --git a/lib/modules/database/reference.js b/lib/modules/database/reference.js index bc206677..846dbca8 100644 --- a/lib/modules/database/reference.js +++ b/lib/modules/database/reference.js @@ -5,7 +5,7 @@ import Query from './query.js'; import Snapshot from './snapshot'; import Disconnect from './disconnect'; import ReferenceBase from './../../utils/ReferenceBase'; -import { promisify, isFunction, isObject, tryJSONParse, tryJSONStringify, generatePushID } from './../../utils'; +import { isFunction, isObject, tryJSONParse, tryJSONStringify, generatePushID } from './../../utils'; // Unique Reference ID for native events let refId = 1; @@ -57,7 +57,6 @@ export default class Reference extends ReferenceBase { database: Object; query: Query; - // todo logger missing as reference base no longer extends module base constructor(database: Object, path: string, existingModifiers?: Array) { super(path, database); this.refId = refId++; @@ -65,7 +64,7 @@ export default class Reference extends ReferenceBase { this.database = database; this.namespace = 'firebase:db:ref'; this.query = new Query(this, path, existingModifiers); - this.log.debug('Created new Reference', this.refId, this.path); + // TODO this.log.debug('Created new Reference', this.refId, this.path); } /** @@ -74,8 +73,7 @@ export default class Reference extends ReferenceBase { * @returns {*} */ keepSynced(bool: boolean) { - const path = this.path; - return promisify('keepSynced', this.database._native)(path, bool); + return this.database._native.keepSynced(this.path, bool); } /** @@ -84,9 +82,7 @@ export default class Reference extends ReferenceBase { * @returns {*} */ set(value: any) { - const path = this.path; - const _value = this._serializeAnyType(value); - return promisify('set', this.database._native)(path, _value); + return this.database._native.set(this.path, this._serializeAnyType(value)); } /** @@ -95,21 +91,20 @@ export default class Reference extends ReferenceBase { * @returns {*} */ setPriority(priority: string | number | null) { - const path = this.path; const _priority = this._serializeAnyType(priority); - return promisify('priority', FirebaseDatabase)(path, _priority); + return this.database._native.setPriority(this.path, _priority); } /** * + * @param value * @param priority * @returns {*} */ setWithPriority(value: any, priority: string | number | null) { - const path = this.path; - const _priority = this._serializeAnyType(priority); const _value = this._serializeAnyType(value); - return promisify('withPriority', FirebaseDatabase)(path, _value, _priority); + const _priority = this._serializeAnyType(priority); + return this.database._native.setWithPriority(this.path, _value, _priority); } /** @@ -118,9 +113,8 @@ export default class Reference extends ReferenceBase { * @returns {*} */ update(val: Object) { - const path = this.path; const value = this._serializeObject(val); - return promisify('update', this.database._native)(path, value); + return this.database._native.update(this.path, value); } /** @@ -128,261 +122,7 @@ export default class Reference extends ReferenceBase { * @returns {*} */ remove() { - return promisify('remove', this.database._native)(this.path); - } - - /** - * - * @param value - * @param onComplete - * @returns {*} - */ - push(value: any, onComplete: Function) { - if (value === null || value === undefined) { - return new Reference(this.database, `${this.path}/${generatePushID(this.database.serverTimeOffset)}`); - } - - const path = this.path; - const _value = this._serializeAnyType(value); - return promisify('push', this.database._native)(path, _value) - .then(({ ref }) => { - const newRef = new Reference(this.database, ref); - if (isFunction(onComplete)) return onComplete(null, newRef); - return newRef; - }).catch((e) => { - if (isFunction(onComplete)) return onComplete(e, null); - return e; - }); - } - - /** - * iOS: Called once with the initial data at the specified location and then once each - * time the data changes. It won't trigger until the entire contents have been - * synchronized. - * - * Android: (@link https://github.com/invertase/react-native-firebase/issues/92) - * - Array & number values: Called once with the initial data at the specified - * location and then twice each time the value changes. - * - Other data types: Called once with the initial data at the specified location - * and once each time the data type changes. - * - * @callback onValueCallback - * @param {!DataSnapshot} dataSnapshot - Snapshot representing data at the location - * specified by the current ref. If location has no data, .val() will return null. - */ - - /** - * Called once for each initial child at the specified location and then again - * every time a new child is added. - * - * @callback onChildAddedCallback - * @param {!DataSnapshot} dataSnapshot - Snapshot reflecting the data for the - * relevant child. - * @param {?ReferenceKey} previousChildKey - For ordering purposes, the key - * of the previous sibling child by sort order, or null if it is the first child. - */ - - /** - * Called once every time a child is removed. - * - * A child will get removed when either: - * - remove() is explicitly called on a child or one of its ancestors - * - set(null) is called on that child or one of its ancestors - * - a child has all of its children removed - * - there is a query in effect which now filters out the child (because it's sort - * order changed or the max limit was hit) - * - * @callback onChildRemovedCallback - * @param {!DataSnapshot} dataSnapshot - Snapshot reflecting the old data for - * the child that was removed. - */ - - /** - * Called when a child (or any of its descendants) changes. - * - * A single child_changed event may represent multiple changes to the child. - * - * @callback onChildChangedCallback - * @param {!DataSnapshot} dataSnapshot - Snapshot reflecting new child contents. - * @param {?ReferenceKey} previousChildKey - For ordering purposes, the key - * of the previous sibling child by sort order, or null if it is the first child. - */ - - /** - * Called when a child's sort order changes, i.e. its position relative to its - * siblings changes. - * - * @callback onChildMovedCallback - * @param {!DataSnapshot} dataSnapshot - Snapshot reflecting the data of the moved - * child. - * @param {?ReferenceKey} previousChildKey - For ordering purposes, the key - * of the previous sibling child by sort order, or null if it is the first child. - */ - - /** - * @typedef (onValueCallback|onChildAddedCallback|onChildRemovedCallback|onChildChangedCallback|onChildMovedCallback) ReferenceEventCallback - */ - - /** - * Called if the event subscription is cancelled because the client does - * not have permission to read this data (or has lost the permission to do so). - * - * @callback onFailureCallback - * @param {Error} error - Object indicating why the failure occurred - */ - - /** - * Binds callback handlers to when data changes at the current ref's location. - * The primary method of reading data from a Database. - * - * Callbacks can be unbound using {@link off}. - * - * Event Types: - * - * - value: {@link onValueCallback}. - * - child_added: {@link onChildAddedCallback} - * - child_removed: {@link onChildRemovedCallback} - * - child_changed: {@link onChildChangedCallback} - * - child_moved: {@link onChildMovedCallback} - * - * @param {ReferenceEventType} eventType - Type of event to attach a callback for. - * @param {ReferenceEventCallback} successCallback - Function that will be called - * when the event occurs with the new data. - * @param {onFailureCallback=} failureCallbackOrContext - Optional callback that is called - * if the event subscription fails. {@link onFailureCallback} - * @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} - */ - on(eventType: string, successCallback: () => any, failureCallbackOrContext: () => any, context: any) { - if (!eventType) throw new Error('Error: Query on failed: Was called with 0 arguments. Expects at least 2'); - if (!ReferenceEventTypes[eventType]) throw new Error('Query.on failed: First argument must be a valid event type: "value", "child_added", "child_removed", "child_changed", or "child_moved".'); - if (!successCallback) throw new Error('Query.on failed: Was called with 1 argument. Expects at least 2.'); - if (!isFunction(successCallback)) throw new Error('Query.on failed: Second argument must be a valid function.'); - if (arguments.length > 2 && !failureCallbackOrContext) throw new Error('Query.on failed: third argument must either be a cancel callback or a context object.'); - - this.log.debug('adding reference.on', this.refId, eventType); - let _failureCallback; - let _context; - - if (context) { - _context = context; - _failureCallback = failureCallbackOrContext; - } else if (isFunction(failureCallbackOrContext)) { - _failureCallback = failureCallbackOrContext; - } else { - _context = failureCallbackOrContext; - } - - if (_failureCallback) { - _failureCallback = (error) => { - if (error.message.startsWith('FirebaseError: permission_denied')) { - // eslint-disable-next-line - error.message = `permission_denied at /${this.path}: Client doesn't have permission to access the desired data.` - } - - failureCallbackOrContext(error); - }; - } - - let _successCallback; - - if (_context) { - _successCallback = successCallback.bind(_context); - } else { - _successCallback = successCallback; - } - - const listener = { - listenerId: Object.keys(this.refListeners).length + 1, - eventName: eventType, - successCallback: _successCallback, - failureCallback: _failureCallback, - }; - - this.refListeners[listener.listenerId] = listener; - this.database.on(this, listener); - return successCallback; - } - - /** - * - * @param eventName - * @param successCallback - * @param failureCallback - * TODO @param context - * @returns {Promise.} - */ - once(eventName: string = 'value', successCallback: (snapshot: Object) => void, failureCallback: (error: FirebaseError) => void) { - return promisify('once', this.database._native)(this.refId, this.path, this.query.getModifiers(), eventName) - .then(({ snapshot }) => new Snapshot(this, snapshot)) - .then((snapshot) => { - if (isFunction(successCallback)) successCallback(snapshot); - return snapshot; - }) - .catch((error) => { - const firebaseError = this.database._toFirebaseError(error); - if (isFunction(failureCallback)) return failureCallback(firebaseError); - return Promise.reject(firebaseError); - }); - } - - /** - * Detaches a callback attached with on(). - * - * Calling off() on a parent listener will not automatically remove listeners - * registered on child nodes. - * - * If on() was called multiple times with the same eventType off() must be - * called multiple times to completely remove it. - * - * If a callback is not specified, all callbacks for the specified eventType - * will be removed. If no eventType or callback is specified, all callbacks - * for the Reference will be removed. - * - * If a context is specified, it too is used as a filter parameter: a callback - * will only be detached if, when it was attached with on(), the same event type, - * callback function and context were provided. - * - * If no callbacks matching the parameters provided are found, no callbacks are - * detached. - * - * @param {('value'|'child_added'|'child_changed'|'child_removed'|'child_moved')=} eventType - Type of event to detach callback for. - * @param {Function=} originalCallback - Original callback passed to on() - * TODO @param {*=} context - The context passed to on() when the callback was bound - * - * {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#off} - */ - off(eventType?: string = '', originalCallback?: () => any) { - this.log.debug('ref.off(): ', this.refId, eventType); - // $FlowFixMe - const listeners: Array = Object.values(this.refListeners); - let listenersToRemove; - if (eventType && originalCallback) { - listenersToRemove = listeners.filter((listener) => { - return listener.eventName === eventType && listener.successCallback === originalCallback; - }); - // Only remove a single listener as per the web spec - if (listenersToRemove.length > 1) listenersToRemove = [listenersToRemove[0]]; - } else if (eventType) { - listenersToRemove = listeners.filter((listener) => { - return listener.eventName === eventType; - }); - } else if (originalCallback) { - listenersToRemove = listeners.filter((listener) => { - return listener.successCallback === originalCallback; - }); - } else { - listenersToRemove = listeners; - } - // Remove the listeners from the reference to prevent memory leaks - listenersToRemove.forEach((listener) => { - delete this.refListeners[listener.listenerId]; - }); - return this.database.off(this.refId, listenersToRemove, Object.keys(this.refListeners).length); + return this.database._native.remove(this.path); } /** @@ -418,10 +158,57 @@ export default class Reference extends ReferenceBase { return resolve({ committed, snapshot }); }; - this.database.transaction.add(this, transactionUpdate, onCompleteWrapper, applyLocally); + this.database._transactionHandler.add(this, transactionUpdate, onCompleteWrapper, applyLocally); }); } + + /** + * + * @param eventName + * @param successCallback + * @param failureCallback + * TODO @param context + * @returns {Promise.} + */ + once(eventName: string = 'value', successCallback: (snapshot: Object) => void, failureCallback: (error: FirebaseError) => void) { + return this.database._native.once(this.refId, this.path, this.query.getModifiers(), eventName) + .then(({ snapshot }) => new Snapshot(this, snapshot)) + .then((snapshot) => { + if (isFunction(successCallback)) successCallback(snapshot); + return snapshot; + }) + .catch((error) => { + if (isFunction(failureCallback)) return failureCallback(error); + return error; + }); + } + + /** + * + * @param value + * @param onComplete + * @returns {*} + */ + push(value: any, onComplete: Function) { + if (value === null || value === undefined) { + return new Reference(this.database, `${this.path}/${generatePushID(this.database.serverTimeOffset)}`); + } + + const newRef = new Reference(this.database, `${this.path}/${generatePushID(this.database.serverTimeOffset)}`); + const promise = newRef.set(value); + + // todo 'ThenableReference' + return promise + .then(() => { + if (isFunction(onComplete)) return onComplete(null, newRef); + return newRef; + }).catch((error) => { + if (isFunction(onComplete)) return onComplete(error, null); + return error; + }); + } + /** * MODIFIERS */ @@ -665,4 +452,224 @@ export default class Reference extends ReferenceBase { value, }; } + + + // todo below methods need refactoring + // todo below methods need refactoring + // todo below methods need refactoring + // todo below methods need refactoring + // todo below methods need refactoring + // todo below methods need refactoring + // todo below methods need refactoring + // todo below methods need refactoring + + /** + * iOS: Called once with the initial data at the specified location and then once each + * time the data changes. It won't trigger until the entire contents have been + * synchronized. + * + * Android: (@link https://github.com/invertase/react-native-firebase/issues/92) + * - Array & number values: Called once with the initial data at the specified + * location and then twice each time the value changes. + * - Other data types: Called once with the initial data at the specified location + * and once each time the data type changes. + * + * @callback onValueCallback + * @param {!DataSnapshot} dataSnapshot - Snapshot representing data at the location + * specified by the current ref. If location has no data, .val() will return null. + */ + + /** + * Called once for each initial child at the specified location and then again + * every time a new child is added. + * + * @callback onChildAddedCallback + * @param {!DataSnapshot} dataSnapshot - Snapshot reflecting the data for the + * relevant child. + * @param {?ReferenceKey} previousChildKey - For ordering purposes, the key + * of the previous sibling child by sort order, or null if it is the first child. + */ + + /** + * Called once every time a child is removed. + * + * A child will get removed when either: + * - remove() is explicitly called on a child or one of its ancestors + * - set(null) is called on that child or one of its ancestors + * - a child has all of its children removed + * - there is a query in effect which now filters out the child (because it's sort + * order changed or the max limit was hit) + * + * @callback onChildRemovedCallback + * @param {!DataSnapshot} dataSnapshot - Snapshot reflecting the old data for + * the child that was removed. + */ + + /** + * Called when a child (or any of its descendants) changes. + * + * A single child_changed event may represent multiple changes to the child. + * + * @callback onChildChangedCallback + * @param {!DataSnapshot} dataSnapshot - Snapshot reflecting new child contents. + * @param {?ReferenceKey} previousChildKey - For ordering purposes, the key + * of the previous sibling child by sort order, or null if it is the first child. + */ + + /** + * Called when a child's sort order changes, i.e. its position relative to its + * siblings changes. + * + * @callback onChildMovedCallback + * @param {!DataSnapshot} dataSnapshot - Snapshot reflecting the data of the moved + * child. + * @param {?ReferenceKey} previousChildKey - For ordering purposes, the key + * of the previous sibling child by sort order, or null if it is the first child. + */ + + /** + * @typedef (onValueCallback|onChildAddedCallback|onChildRemovedCallback|onChildChangedCallback|onChildMovedCallback) ReferenceEventCallback + */ + + /** + * Called if the event subscription is cancelled because the client does + * not have permission to read this data (or has lost the permission to do so). + * + * @callback onFailureCallback + * @param {Error} error - Object indicating why the failure occurred + */ + + /** + * Binds callback handlers to when data changes at the current ref's location. + * The primary method of reading data from a Database. + * + * Callbacks can be unbound using {@link off}. + * + * Event Types: + * + * - value: {@link onValueCallback}. + * - child_added: {@link onChildAddedCallback} + * - child_removed: {@link onChildRemovedCallback} + * - child_changed: {@link onChildChangedCallback} + * - child_moved: {@link onChildMovedCallback} + * + * @param {ReferenceEventType} eventType - Type of event to attach a callback for. + * @param {ReferenceEventCallback} successCallback - Function that will be called + * when the event occurs with the new data. + * @param {onFailureCallback=} failureCallbackOrContext - Optional callback that is called + * if the event subscription fails. {@link onFailureCallback} + * @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} + */ + on(eventType: string, successCallback: () => any, failureCallbackOrContext: () => any, context: any) { + if (!eventType) throw new Error('Error: Query on failed: Was called with 0 arguments. Expects at least 2'); + if (!ReferenceEventTypes[eventType]) throw new Error('Query.on failed: First argument must be a valid event type: "value", "child_added", "child_removed", "child_changed", or "child_moved".'); + if (!successCallback) throw new Error('Query.on failed: Was called with 1 argument. Expects at least 2.'); + if (!isFunction(successCallback)) throw new Error('Query.on failed: Second argument must be a valid function.'); + if (arguments.length > 2 && !failureCallbackOrContext) throw new Error('Query.on failed: third argument must either be a cancel callback or a context object.'); + + // TODO this.log.debug('adding reference.on', this.refId, eventType); + let _failureCallback; + let _context; + + if (context) { + _context = context; + _failureCallback = failureCallbackOrContext; + } else if (isFunction(failureCallbackOrContext)) { + _failureCallback = failureCallbackOrContext; + } else { + _context = failureCallbackOrContext; + } + + if (_failureCallback) { + _failureCallback = (error) => { + if (error.message.startsWith('FirebaseError: permission_denied')) { + // eslint-disable-next-line + error.message = `permission_denied at /${this.path}: Client doesn't have permission to access the desired data.` + } + + failureCallbackOrContext(error); + }; + } + + let _successCallback; + + if (_context) { + _successCallback = successCallback.bind(_context); + } else { + _successCallback = successCallback; + } + + const listener = { + listenerId: Object.keys(this.refListeners).length + 1, + eventName: eventType, + successCallback: _successCallback, + failureCallback: _failureCallback, + }; + + this.refListeners[listener.listenerId] = listener; + this.database.on(this, listener); + return successCallback; + } + + /** + * Detaches a callback attached with on(). + * + * Calling off() on a parent listener will not automatically remove listeners + * registered on child nodes. + * + * If on() was called multiple times with the same eventType off() must be + * called multiple times to completely remove it. + * + * If a callback is not specified, all callbacks for the specified eventType + * will be removed. If no eventType or callback is specified, all callbacks + * for the Reference will be removed. + * + * If a context is specified, it too is used as a filter parameter: a callback + * will only be detached if, when it was attached with on(), the same event type, + * callback function and context were provided. + * + * If no callbacks matching the parameters provided are found, no callbacks are + * detached. + * + * @param {('value'|'child_added'|'child_changed'|'child_removed'|'child_moved')=} eventType - Type of event to detach callback for. + * @param {Function=} originalCallback - Original callback passed to on() + * TODO @param {*=} context - The context passed to on() when the callback was bound + * + * {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#off} + */ + off(eventType?: string = '', originalCallback?: () => any) { + // TODO this.log.debug('ref.off(): ', this.refId, eventType); + // $FlowFixMe + const listeners: Array = Object.values(this.refListeners); + let listenersToRemove; + if (eventType && originalCallback) { + listenersToRemove = listeners.filter((listener) => { + return listener.eventName === eventType && listener.successCallback === originalCallback; + }); + // Only remove a single listener as per the web spec + if (listenersToRemove.length > 1) listenersToRemove = [listenersToRemove[0]]; + } else if (eventType) { + listenersToRemove = listeners.filter((listener) => { + return listener.eventName === eventType; + }); + } else if (originalCallback) { + listenersToRemove = listeners.filter((listener) => { + return listener.successCallback === originalCallback; + }); + } else { + listenersToRemove = listeners; + } + // Remove the listeners from the reference to prevent memory leaks + listenersToRemove.forEach((listener) => { + delete this.refListeners[listener.listenerId]; + }); + return this.database.off(this.refId, listenersToRemove, Object.keys(this.refListeners).length); + } + + } diff --git a/lib/modules/database/transaction.js b/lib/modules/database/transaction.js index 5c65e2c1..3dfcdd99 100644 --- a/lib/modules/database/transaction.js +++ b/lib/modules/database/transaction.js @@ -3,7 +3,7 @@ * Database representation wrapper */ -import { generatePushID } from './../../utils'; +let transactionId = 0; /** * @class Database @@ -12,9 +12,10 @@ export default class TransactionHandler { constructor(database: Object) { this._transactions = {}; this._database = database; - this._transactionListener = this._database._eventEmitter.addListener( - 'database_transaction_event', - event => this._handleTransactionEvent(event), + + this._transactionListener = this._database.addListener( + this._database._getAppEventName('database_transaction_event'), + this._handleTransactionEvent.bind(this), ); } @@ -25,12 +26,7 @@ export default class TransactionHandler { * @param onComplete * @param applyLocally */ - add( - reference: Object, - transactionUpdater: Function, - onComplete?: Function, - applyLocally?: boolean = false - ) { + add(reference: Object, transactionUpdater: Function, onComplete?: Function, applyLocally?: boolean = false) { const id = this._generateTransactionId(); this._transactions[id] = { @@ -43,7 +39,7 @@ export default class TransactionHandler { started: true, }; - this._database._native.startTransaction(reference.path, id, applyLocally); + this._database._native.transactionStart(reference.path, id, applyLocally); } /** @@ -56,7 +52,7 @@ export default class TransactionHandler { * @private */ _generateTransactionId(): string { - return generatePushID(this._database.serverTimeOffset); + return transactionId++; } /** @@ -90,7 +86,8 @@ export default class TransactionHandler { try { const transaction = this._transactions[id]; - // todo handle when transaction no longer exists on js side? + if (!transaction) return; + newValue = transaction.transactionUpdater(value); } finally { let abort = false; @@ -99,7 +96,7 @@ export default class TransactionHandler { abort = true; } - this._database._native.tryCommitTransaction(id, { value: newValue, abort }); + this._database._native.transactionTryCommit(id, { value: newValue, abort }); } } @@ -113,7 +110,7 @@ export default class TransactionHandler { if (transaction && !transaction.completed) { transaction.completed = true; try { - transaction.onComplete(new Error(event.message, event.code), null); + transaction.onComplete(new Error(event.error.message, event.error.code), null); } finally { setImmediate(() => { delete this._transactions[event.id]; diff --git a/lib/utils/ModuleBase.js b/lib/utils/ModuleBase.js index 25f51be5..516751bb 100644 --- a/lib/utils/ModuleBase.js +++ b/lib/utils/ModuleBase.js @@ -24,6 +24,15 @@ const NATIVE_MODULE_EVENTS = { Auth: [ 'onAuthStateChanged', ], + Database: [ + 'database_transaction_event', + ], +}; + +const DEFAULTS = { + Database: { + persistence: false, + }, }; export default class ModuleBase { @@ -35,7 +44,7 @@ export default class ModuleBase { * @param withEventEmitter */ constructor(firebaseApp, options, moduleName, withEventEmitter = false) { - this._options = Object.assign({}, options); + this._options = Object.assign({}, DEFAULTS[moduleName] || {}, options); this._module = moduleName; this._firebaseApp = firebaseApp; this._appName = firebaseApp._name; diff --git a/lib/utils/index.js b/lib/utils/index.js index f5cd1389..6a180bcb 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -9,37 +9,6 @@ const PUSH_CHARS = '-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuv const hasOwnProperty = Object.hasOwnProperty; const DEFAULT_CHUNK_SIZE = 50; -// internal promise handler -const _handler = (resolve, reject, errorPrefix, err, resp) => { - // resolve / reject after events etc - setImmediate(() => { - if (err) { - // $FlowFixMe - const firebaseError: FirebaseError = new Error(err.message); - - if (isObject(err)) { - Object.keys(err).forEach(key => Object.defineProperty(firebaseError, key, { value: err[key] })); - if (errorPrefix) { - firebaseError.code = toWebSDKErrorCode(err.code || '', errorPrefix); - } - } - - return reject(firebaseError); - } - return resolve(resp); - }); -}; - - -export function nativeSDKMissing(sdkName) { - console.warn(`Firebase ${sdkName} native sdk has not been included in your ${Platform.OS === 'ios' ? 'podfile' : 'build.gradle'} - ${sdkName} methods have been disabled.`); -} - -export function toWebSDKErrorCode(code: any, prefix: string): string { - if (!code || typeof code !== 'string') return ''; - return code.toLowerCase().replace('error_', prefix).replace(/_/g, '-'); -} - /** * Deep get a value from an object. * @website https://github.com/Salakar/deeps @@ -149,43 +118,12 @@ export function tryJSONStringify(data: any): string | null { // noinspection Eslint export const windowOrGlobal = (typeof self === 'object' && self.self === self && self) || (typeof global === 'object' && global.global === global && global) || this; -/** - * Makes an objects keys it's values - * @param object - * @returns {{}} - */ -export function reverseKeyValues(object: Object): Object { - const output = {}; - for (const key in object) { - output[object[key]] = key; - } - return output; -} - /** * No operation func */ export function noop(): void { } -/** - * Wraps a native module method to support promises. - * @param fn - * @param NativeModule - * @param errorPrefix - */ -export function promisify(fn: Function | string, - NativeModule: Object, - errorPrefix?: string): (args: any) => Promise<> { - return (...args) => { - return new Promise((resolve, reject) => { - const _fn = typeof fn === 'function' ? fn : NativeModule[fn]; - if (!_fn || typeof _fn !== 'function') return reject(new Error('Missing function for promisify.')); - return _fn.apply(NativeModule, [...args, _handler.bind(_handler, resolve, reject, errorPrefix)]); - }); - }; -} - /** * Delays chunks based on sizes per event loop. @@ -245,6 +183,11 @@ export function each(array: Array<*>, } } +/** + * Returns a string typeof that's valid for Firebase usage + * @param value + * @return {*} + */ export function typeOf(value: any): string { if (value === null) return 'null'; if (Array.isArray(value)) return 'array'; diff --git a/tests/ios/Podfile.lock b/tests/ios/Podfile.lock index 846f0ca9..28d8e728 100644 --- a/tests/ios/Podfile.lock +++ b/tests/ios/Podfile.lock @@ -90,17 +90,17 @@ PODS: - GoogleToolboxForMac/NSString+URLArguments (2.1.1) - GTMSessionFetcher/Core (1.1.10) - Protobuf (3.3.0) - - React (0.44.0): - - React/Core (= 0.44.0) - - React/Core (0.44.0): + - React (0.44.3): + - React/Core (= 0.44.3) + - React/Core (0.44.3): - React/cxxreact - - Yoga (= 0.44.0.React) - - React/cxxreact (0.44.0): + - Yoga (= 0.44.3.React) + - React/cxxreact (0.44.3): - React/jschelpers - - React/jschelpers (0.44.0) + - React/jschelpers (0.44.3) - RNFirebase (2.0.4): - React - - Yoga (0.44.0.React) + - Yoga (0.44.3.React) DEPENDENCIES: - Firebase/AdMob @@ -143,9 +143,9 @@ SPEC CHECKSUMS: GoogleToolboxForMac: 8e329f1b599f2512c6b10676d45736bcc2cbbeb0 GTMSessionFetcher: 30d874b96d0d76028f61fbd122801e3f030d47db Protobuf: d582fecf68201eac3d79ed61369ef45734394b9c - React: d2077cc20245ccdc8bfe1fdc002f2003318ae8d8 + React: 6361345ebeb769a929e10a06baf0c868d6d03ad5 RNFirebase: 3e5a3ff431c5c8a997152f5f19d354e1ca66f65d - Yoga: a92a5d8e128905bf9f29c82f870192a6e873dd98 + Yoga: c90474ca3ec1edba44c97b6c381f03e222a9e287 PODFILE CHECKSUM: 45666f734ebfc8b3b0f2be0a83bc2680caeb502f diff --git a/tests/lib/TestRun.js b/tests/lib/TestRun.js index 762cf463..bcabe188 100644 --- a/tests/lib/TestRun.js +++ b/tests/lib/TestRun.js @@ -6,6 +6,11 @@ const EVENTS = { TEST_STATUS: 'TEST_STATUS', }; +if (!console.groupCollapsed) { + console.groupCollapsed = console.log; + console.groupEnd = () => console.log(''); +} + const locationRegex = /\(?http:.*:([0-9]+):([0-9]+)\)?/g; function cleanStack(stack, maxLines = 5) { diff --git a/tests/src/main.js b/tests/src/main.js index 3bf208c0..35664e57 100644 --- a/tests/src/main.js +++ b/tests/src/main.js @@ -6,6 +6,8 @@ import { setupSuites } from './tests/index'; global.Promise = require('bluebird'); +console.ignoredYellowBox = ['Setting a timer for a long period of time, i.e. multiple minutes']; + type State = { loading: boolean, store: any, diff --git a/tests/src/tests/database/ref/childTests.js b/tests/src/tests/database/ref/childTests.js index 9a97c434..82a26f3c 100644 --- a/tests/src/tests/database/ref/childTests.js +++ b/tests/src/tests/database/ref/childTests.js @@ -1,5 +1,5 @@ -function childTests({ describe, it, context, firebase }) { - describe('ref().child', () => { +function childTests({ fdescribe, it, context, firebase }) { + fdescribe('ref().child', () => { context('when passed a shallow path', () => { it('returns correct child ref', () => { // Setup diff --git a/tests/src/tests/database/ref/factoryTests.js b/tests/src/tests/database/ref/factoryTests.js index 3a4e0c1b..d2ce57aa 100644 --- a/tests/src/tests/database/ref/factoryTests.js +++ b/tests/src/tests/database/ref/factoryTests.js @@ -1,7 +1,7 @@ import DatabaseContents from '../../support/DatabaseContents'; -function factoryTests({ describe, it, firebase }) { - describe('ref()', () => { +function factoryTests({ fdescribe, it, firebase }) { + fdescribe('ref()', () => { it('returns root reference when provided no path', () => { // Setup diff --git a/tests/src/tests/database/ref/isEqualTests.js b/tests/src/tests/database/ref/isEqualTests.js index 3fee2a6a..e334e69f 100644 --- a/tests/src/tests/database/ref/isEqualTests.js +++ b/tests/src/tests/database/ref/isEqualTests.js @@ -1,5 +1,5 @@ -function isEqualTests({ describe, before, it, firebase }) { - describe('ref().isEqual()', () => { +function isEqualTests({ fdescribe, before, it, firebase }) { + fdescribe('ref().isEqual()', () => { before(() => { this.ref = firebase.native.database().ref('tests/types'); }); diff --git a/tests/src/tests/database/ref/issueSpecificTests.js b/tests/src/tests/database/ref/issueSpecificTests.js index 9338ac3c..7513a1f5 100644 --- a/tests/src/tests/database/ref/issueSpecificTests.js +++ b/tests/src/tests/database/ref/issueSpecificTests.js @@ -1,8 +1,8 @@ import should from 'should'; import DatabaseContents from '../../support/DatabaseContents'; -function issueTests({ describe, it, context, firebase }) { - describe('issue_100', () => { +function issueTests({ fdescribe, it, context, firebase }) { + fdescribe('issue_100', () => { context('array-like values should', () => { it('return null in returned array at positions where a key is missing', async () => { // Setup @@ -18,7 +18,7 @@ function issueTests({ describe, it, context, firebase }) { }); }); - describe('issue_108', () => { + fdescribe('issue_108', () => { context('filters using floats', () => { it('return correct results', async () => { // Setup @@ -67,7 +67,7 @@ function issueTests({ describe, it, context, firebase }) { }); }); - describe('issue_171', () => { + fdescribe('issue_171', () => { context('non array-like values should', () => { it('return as objects', async () => { // Setup diff --git a/tests/src/tests/database/ref/keyTests.js b/tests/src/tests/database/ref/keyTests.js index 30b2aac0..290a7aac 100644 --- a/tests/src/tests/database/ref/keyTests.js +++ b/tests/src/tests/database/ref/keyTests.js @@ -1,5 +1,5 @@ -function keyTests({ describe, it, firebase }) { - describe('ref().key', () => { +function keyTests({ fdescribe, it, firebase }) { + fdescribe('ref().key', () => { it('returns null for root ref', () => { // Setup diff --git a/tests/src/tests/database/ref/onceTests.js b/tests/src/tests/database/ref/onceTests.js index 6b5ee6b6..5fee3029 100644 --- a/tests/src/tests/database/ref/onceTests.js +++ b/tests/src/tests/database/ref/onceTests.js @@ -3,8 +3,8 @@ import 'should-sinon'; import DatabaseContents from '../../support/DatabaseContents'; -function onceTests({ describe, firebase, it, tryCatch }) { - describe('ref().once()', () => { +function onceTests({ fdescribe, firebase, it, tryCatch }) { + fdescribe('ref().once()', () => { it('returns a promise', () => { // Setup @@ -62,8 +62,7 @@ function onceTests({ describe, firebase, it, tryCatch }) { const failureCb = tryCatch((error) => { // Assertion - - error.message.includes('permission_denied').should.be.true(); + error.code.includes('DATABASE/PERMISSION-DENIED').should.be.true(); resolve(); }, reject); diff --git a/tests/src/tests/database/ref/parentTests.js b/tests/src/tests/database/ref/parentTests.js index 95ec2d03..0b022eca 100644 --- a/tests/src/tests/database/ref/parentTests.js +++ b/tests/src/tests/database/ref/parentTests.js @@ -1,5 +1,5 @@ -function parentTests({ describe, context, it, firebase }) { - describe('ref().parent', () => { +function parentTests({ fdescribe, context, it, firebase }) { + fdescribe('ref().parent', () => { context('on the root ref', () => { it('returns null', () => { // Setup diff --git a/tests/src/tests/database/ref/priorityTests.js b/tests/src/tests/database/ref/priorityTests.js index cc73ff93..1ab9e624 100644 --- a/tests/src/tests/database/ref/priorityTests.js +++ b/tests/src/tests/database/ref/priorityTests.js @@ -1,7 +1,7 @@ import DatabaseContents from '../../support/DatabaseContents'; -function setTests({ describe, it, firebase }) { - describe('ref().priority', () => { +function setTests({ fdescribe, it, firebase }) { + fdescribe('ref().priority', () => { it('setPriority() should correctly set a priority for all non-null values', async () => { await Promise.map(Object.keys(DatabaseContents.DEFAULT), async (dataRef) => { // Setup diff --git a/tests/src/tests/database/ref/pushTests.js b/tests/src/tests/database/ref/pushTests.js index 063ce35c..c464a19e 100644 --- a/tests/src/tests/database/ref/pushTests.js +++ b/tests/src/tests/database/ref/pushTests.js @@ -3,8 +3,8 @@ import 'should-sinon'; import DatabaseContents from '../../support/DatabaseContents'; -function pushTests({ describe, it, firebase }) { - describe('ref().push()', () => { +function pushTests({ fdescribe, it, firebase }) { + fdescribe('ref().push()', () => { it('returns a ref that can be used to set value later', async () => { // Setup diff --git a/tests/src/tests/database/ref/queryTests.js b/tests/src/tests/database/ref/queryTests.js index 5282fb97..c66f0597 100644 --- a/tests/src/tests/database/ref/queryTests.js +++ b/tests/src/tests/database/ref/queryTests.js @@ -1,8 +1,8 @@ import 'should-sinon'; import Promise from 'bluebird'; -function queryTests({ describe, it, firebase, tryCatch }) { - describe('ref query', () => { +function queryTests({ fdescribe, it, firebase, tryCatch }) { + fdescribe('ref query', () => { it('orderByChild().equalTo()', () => { return new Promise((resolve, reject) => { const successCb = tryCatch((snapshot) => { diff --git a/tests/src/tests/database/ref/refTests.js b/tests/src/tests/database/ref/refTests.js index 787bce9d..ca3060c3 100644 --- a/tests/src/tests/database/ref/refTests.js +++ b/tests/src/tests/database/ref/refTests.js @@ -1,5 +1,5 @@ -function refTests({ describe, it, firebase }) { - describe('ref().ref', () => { +function refTests({ fdescribe, it, firebase }) { + fdescribe('ref().ref', () => { it('returns the reference', () => { // Setup const ref = firebase.native.database().ref(); diff --git a/tests/src/tests/database/ref/removeTests.js b/tests/src/tests/database/ref/removeTests.js index 701fecd2..f22c3076 100644 --- a/tests/src/tests/database/ref/removeTests.js +++ b/tests/src/tests/database/ref/removeTests.js @@ -1,7 +1,7 @@ import DatabaseContents from '../../support/DatabaseContents'; -function removeTests({ describe, it, firebase }) { - describe('ref().remove()', () => { +function removeTests({ fdescribe, it, firebase }) { + fdescribe('ref().remove()', () => { it('returns a promise', () => { // Setup diff --git a/tests/src/tests/database/ref/rootTests.js b/tests/src/tests/database/ref/rootTests.js index 53e1a35d..86f1977c 100644 --- a/tests/src/tests/database/ref/rootTests.js +++ b/tests/src/tests/database/ref/rootTests.js @@ -1,5 +1,5 @@ -function rootTests({ describe, it, context, firebase }) { - describe('ref().root', () => { +function rootTests({ fdescribe, it, context, firebase }) { + fdescribe('ref().root', () => { context('when called on a non-root reference', () => { it('returns root ref', () => { // Setup diff --git a/tests/src/tests/database/ref/setTests.js b/tests/src/tests/database/ref/setTests.js index 1275664f..20251252 100644 --- a/tests/src/tests/database/ref/setTests.js +++ b/tests/src/tests/database/ref/setTests.js @@ -1,8 +1,8 @@ import DatabaseContents from '../../support/DatabaseContents'; -function setTests({ describe, it, xit, firebase }) { - describe('ref.set()', () => { - xit('returns a promise', async () => { +function setTests({ fdescribe, it, xit, firebase }) { + fdescribe('ref.set()', () => { + it('returns a promise', async () => { // Setup const ref = firebase.native.database().ref('tests/types/number'); @@ -16,7 +16,7 @@ function setTests({ describe, it, xit, firebase }) { returnValue.should.be.Promise(); await returnValue.then((value) => { - (value === undefined).should.be.true(); + (value === null).should.be.true(); }); }); diff --git a/tests/src/tests/database/ref/transactionTests.js b/tests/src/tests/database/ref/transactionTests.js index f37682b0..1c255b9c 100644 --- a/tests/src/tests/database/ref/transactionTests.js +++ b/tests/src/tests/database/ref/transactionTests.js @@ -1,31 +1,31 @@ import Promise from 'bluebird'; -function onTests({ describe, it, firebase, tryCatch }) { - describe('ref.transaction()', () => { - it('works', () => { +function onTests({ fdescribe, it, firebase, tryCatch }) { + fdescribe('ref.transaction()', () => { + it('increments a value on a ref', () => { return new Promise((resolve, reject) => { let valueBefore = 1; firebase.native.database() .ref('tests/transaction').transaction((currentData) => { - if (currentData === null) { - return valueBefore + 10; - } - valueBefore = currentData; + if (currentData === null) { return valueBefore + 10; - }, tryCatch((error, committed, snapshot) => { - if (error) { - return reject(error); - } + } + valueBefore = currentData; + return valueBefore + 10; + }, tryCatch((error, committed, snapshot) => { + if (error) { + return reject(error); + } - if (!committed) { - return reject(new Error('Transaction did not commit.')); - } + if (!committed) { + return reject(new Error('Transaction did not commit.')); + } - snapshot.val().should.equal(valueBefore + 10); - - return resolve(); - }, reject), true); + snapshot.val().should.equal(valueBefore + 10); + + return resolve(); + }, reject), true); }); }); @@ -33,18 +33,18 @@ function onTests({ describe, it, firebase, tryCatch }) { return new Promise((resolve, reject) => { firebase.native.database() .ref('tests/transaction').transaction(() => { - return undefined; - }, (error, committed) => { - if (error) { - return reject(error); - } + return undefined; + }, (error, committed) => { + if (error) { + return reject(error); + } - if (!committed) { - return resolve(); - } + if (!committed) { + return resolve(); + } - return reject(new Error('Transaction did not abort commit.')); - }, true); + return reject(new Error('Transaction did not abort commit.')); + }, true); }); }); }); diff --git a/tests/src/tests/database/ref/updateTests.js b/tests/src/tests/database/ref/updateTests.js index 09af4383..3271a57c 100644 --- a/tests/src/tests/database/ref/updateTests.js +++ b/tests/src/tests/database/ref/updateTests.js @@ -1,8 +1,8 @@ import Promise from 'bluebird'; import DatabaseContents from '../../support/DatabaseContents'; -function updateTests({ describe, it, firebase }) { - describe('ref().update()', () => { +function updateTests({ fdescribe, it, firebase }) { + fdescribe('ref().update()', () => { it('returns a promise', () => { // Setup diff --git a/tests/src/tests/storage/storageTests.js b/tests/src/tests/storage/storageTests.js index 2e3953de..531e4f23 100644 --- a/tests/src/tests/storage/storageTests.js +++ b/tests/src/tests/storage/storageTests.js @@ -12,7 +12,12 @@ function storageTests({ describe, it, firebase, tryCatch }) { resolve(); }, reject); - firebase.native.storage().ref('/not.jpg').downloadFile(`${firebase.native.storage.Native.DOCUMENT_DIRECTORY_PATH}/not.jpg`).then(successCb).catch(failureCb); + firebase.native.storage().ref('/not.jpg') + .downloadFile( + `${firebase.native.storage.Native.DOCUMENT_DIRECTORY_PATH}/not.jpg`, + ) + .then(successCb) + .catch(failureCb); }); });