[js][android] database sync tree implementation with adjusted tests

This commit is contained in:
Salakar 2017-08-15 21:29:50 +01:00
parent 67985f8e90
commit e4d27029b9
14 changed files with 647 additions and 233 deletions

View File

@ -77,20 +77,21 @@ public class Utils {
} }
/** /**
* @param name
* @param path
* @param dataSnapshot * @param dataSnapshot
* @param refId * @param registration
* @param previousChildName
* @return * @return
*/ */
public static WritableMap snapshotToMap(String name, String path, DataSnapshot dataSnapshot, @Nullable String previousChildName, int refId) { public static WritableMap snapshotToMap(DataSnapshot dataSnapshot, @Nullable String previousChildName) {
WritableMap result = Arguments.createMap();
WritableMap snapshot = Arguments.createMap(); WritableMap snapshot = Arguments.createMap();
WritableMap eventMap = Arguments.createMap();
snapshot.putString("key", dataSnapshot.getKey()); snapshot.putString("key", dataSnapshot.getKey());
snapshot.putBoolean("exists", dataSnapshot.exists()); snapshot.putBoolean("exists", dataSnapshot.exists());
snapshot.putBoolean("hasChildren", dataSnapshot.hasChildren()); snapshot.putBoolean("hasChildren", dataSnapshot.hasChildren());
snapshot.putDouble("childrenCount", dataSnapshot.getChildrenCount()); snapshot.putDouble("childrenCount", dataSnapshot.getChildrenCount());
snapshot.putArray("childKeys", Utils.getChildKeys(dataSnapshot));
mapPutValue("priority", dataSnapshot.getPriority(), snapshot);
if (!dataSnapshot.hasChildren()) { if (!dataSnapshot.hasChildren()) {
mapPutValue("value", dataSnapshot.getValue(), snapshot); mapPutValue("value", dataSnapshot.getValue(), snapshot);
@ -103,16 +104,50 @@ public class Utils {
} }
} }
snapshot.putArray("childKeys", Utils.getChildKeys(dataSnapshot));
mapPutValue("priority", dataSnapshot.getPriority(), snapshot);
eventMap.putInt("refId", refId); result.putMap("snapshot", snapshot);
eventMap.putString("path", path); result.putString("previousChildName", previousChildName);
eventMap.putMap("snapshot", snapshot); return result;
eventMap.putString("eventName", name); }
eventMap.putString("previousChildName", previousChildName);
return eventMap; /**
*
* @param map
* @return
*/
public static WritableMap readableMapToWritableMap(ReadableMap map) {
WritableMap writableMap = Arguments.createMap();
ReadableMapKeySetIterator iterator = map.keySetIterator();
while (iterator.hasNextKey()) {
String key = iterator.nextKey();
ReadableType type = map.getType(key);
switch (type) {
case Null:
writableMap.putNull(key);
break;
case Boolean:
writableMap.putBoolean(key, map.getBoolean(key));
break;
case Number:
writableMap.putDouble(key, map.getDouble(key));
break;
case String:
writableMap.putString(key, map.getString(key));
break;
case Map:
writableMap.putMap(key, readableMapToWritableMap(map.getMap(key)));
break;
case Array:
// TODO writableMap.putArray(key, readableArrayToWritableArray(map.getArray(key)));
break;
default:
throw new IllegalArgumentException("Could not convert object with key: " + key + ".");
}
}
return writableMap;
} }
/** /**

View File

@ -35,7 +35,7 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule {
private static final String TAG = "RNFirebaseDatabase"; private static final String TAG = "RNFirebaseDatabase";
private HashMap<String, ChildEventListener> childEventListeners; private HashMap<String, ChildEventListener> childEventListeners;
private HashMap<String, ValueEventListener> valueEventListeners; private HashMap<String, ValueEventListener> valueEventListeners;
private SparseArray<RNFirebaseDatabaseReference> references = new SparseArray<>(); private HashMap<String, RNFirebaseDatabaseReference> references = new HashMap<String, RNFirebaseDatabaseReference>();
private SparseArray<RNFirebaseTransactionHandler> transactionHandlers = new SparseArray<>(); private SparseArray<RNFirebaseTransactionHandler> transactionHandlers = new SparseArray<>();
RNFirebaseDatabase(ReactApplicationContext reactContext) { RNFirebaseDatabase(ReactApplicationContext reactContext) {
@ -103,10 +103,8 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule {
* @param state * @param state
*/ */
@ReactMethod @ReactMethod
public void keepSynced(String appName, int id, String path, ReadableArray modifiers, Boolean state) { public void keepSynced(String appName, String key, String path, ReadableArray modifiers, Boolean state) {
getInternalReferenceForApp(appName, id, path, modifiers, false) getInternalReferenceForApp(appName, key, path, modifiers).getQuery().keepSynced(state);
.getQuery()
.keepSynced(state);
} }
@ -410,34 +408,36 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule {
* Subscribe once to a firebase reference. * Subscribe once to a firebase reference.
* *
* @param appName * @param appName
* @param refId * @param key
* @param path * @param path
* @param modifiers * @param modifiers
* @param eventName * @param eventName
* @param promise * @param promise
*/ */
@ReactMethod @ReactMethod
public void once(String appName, int refId, String path, ReadableArray modifiers, String eventName, Promise promise) { public void once(String appName, String key, String path, ReadableArray modifiers, String eventName, Promise promise) {
getInternalReferenceForApp(appName, refId, path, modifiers, false).once(eventName, promise); getInternalReferenceForApp(appName, key, path, modifiers).once(eventName, promise);
} }
/** /**
* Subscribe to real time events for the specified database path + modifiers * Subscribe to real time events for the specified database path + modifiers
* *
* @param appName String * @param appName String
* @param args ReadableMap * @param props ReadableMap
*/ */
@ReactMethod @ReactMethod
public void on(String appName, ReadableMap args) { public void on(String appName, ReadableMap props) {
RNFirebaseDatabaseReference ref = getInternalReferenceForApp( getInternalReferenceForApp(appName, props).on(
appName, this,
args.getInt("id"), props.getString("eventType"),
args.getString("path"), props.getMap("registration")
args.getArray("modifiers"),
true
); );
}
@ReactMethod
public void off(String appName, ReadableMap args) {
ref.on(this, args.getString("eventType"), args.getString("eventQueryKey"));
} }
/* /*
@ -511,25 +511,49 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule {
* Return an existing or create a new RNFirebaseDatabaseReference instance. * Return an existing or create a new RNFirebaseDatabaseReference instance.
* *
* @param appName * @param appName
* @param refId * @param key
* @param path * @param path
* @param modifiers * @param modifiers
* @param keep
* @return * @return
*/ */
private RNFirebaseDatabaseReference getInternalReferenceForApp(String appName, int refId, String path, ReadableArray modifiers, Boolean keep) { private RNFirebaseDatabaseReference getInternalReferenceForApp(String appName, String key, String path, ReadableArray modifiers) {
RNFirebaseDatabaseReference existingRef = references.get(refId); RNFirebaseDatabaseReference existingRef = references.get(key);
if (existingRef == null) { if (existingRef == null) {
existingRef = new RNFirebaseDatabaseReference( existingRef = new RNFirebaseDatabaseReference(
getReactApplicationContext(), getReactApplicationContext(),
appName, appName,
refId, key,
path,
modifiers
);
}
return existingRef;
}
/**
* @param appName
* @param props
* @return
*/
private RNFirebaseDatabaseReference getInternalReferenceForApp(String appName, ReadableMap props) {
String key = props.getString("key");
String path = props.getString("path");
ReadableArray modifiers = props.getArray("modifiers");
RNFirebaseDatabaseReference existingRef = references.get(key);
if (existingRef == null) {
existingRef = new RNFirebaseDatabaseReference(
getReactApplicationContext(),
appName,
key,
path, path,
modifiers modifiers
); );
if (keep) references.put(refId, existingRef); references.put(key, existingRef);
} }
return existingRef; return existingRef;

View File

@ -1,16 +1,14 @@
package io.invertase.firebase.database; package io.invertase.firebase.database;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.List; import java.util.List;
import android.util.Log; import android.util.Log;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.util.SparseArray;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableArray;
@ -24,10 +22,10 @@ import com.google.firebase.database.ValueEventListener;
import io.invertase.firebase.Utils; import io.invertase.firebase.Utils;
public class RNFirebaseDatabaseReference { class RNFirebaseDatabaseReference {
private static final String TAG = "RNFirebaseDBReference"; private static final String TAG = "RNFirebaseDBReference";
private int refId; private String key;
private Query query; private Query query;
private String path; private String path;
private String appName; private String appName;
@ -41,12 +39,12 @@ public class RNFirebaseDatabaseReference {
/** /**
* @param context * @param context
* @param app * @param app
* @param id * @param refKey
* @param refPath * @param refPath
* @param modifiersArray * @param modifiersArray
*/ */
RNFirebaseDatabaseReference(ReactContext context, String app, int id, String refPath, ReadableArray modifiersArray) { RNFirebaseDatabaseReference(ReactContext context, String app, String refKey, String refPath, ReadableArray modifiersArray) {
refId = id; key = refKey;
appName = app; appName = app;
path = refPath; path = refPath;
reactContext = context; reactContext = context;
@ -62,7 +60,7 @@ public class RNFirebaseDatabaseReference {
ValueEventListener onceValueEventListener = new ValueEventListener() { ValueEventListener onceValueEventListener = new ValueEventListener() {
@Override @Override
public void onDataChange(DataSnapshot dataSnapshot) { public void onDataChange(DataSnapshot dataSnapshot) {
WritableMap data = Utils.snapshotToMap("value", path, dataSnapshot, null, refId); WritableMap data = Utils.snapshotToMap(dataSnapshot, null);
promise.resolve(data); promise.resolve(data);
} }
@ -74,7 +72,7 @@ public class RNFirebaseDatabaseReference {
query.addListenerForSingleValueEvent(onceValueEventListener); query.addListenerForSingleValueEvent(onceValueEventListener);
Log.d(TAG, "Added OnceValueEventListener for refId: " + refId); Log.d(TAG, "Added OnceValueEventListener for key: " + key);
} }
/** /**
@ -89,7 +87,7 @@ public class RNFirebaseDatabaseReference {
public void onChildAdded(DataSnapshot dataSnapshot, String previousChildName) { public void onChildAdded(DataSnapshot dataSnapshot, String previousChildName) {
if ("child_added".equals(eventName)) { if ("child_added".equals(eventName)) {
query.removeEventListener(this); query.removeEventListener(this);
WritableMap data = Utils.snapshotToMap("child_added", path, dataSnapshot, previousChildName, refId); WritableMap data = Utils.snapshotToMap(dataSnapshot, previousChildName);
promise.resolve(data); promise.resolve(data);
} }
} }
@ -98,7 +96,7 @@ public class RNFirebaseDatabaseReference {
public void onChildChanged(DataSnapshot dataSnapshot, String previousChildName) { public void onChildChanged(DataSnapshot dataSnapshot, String previousChildName) {
if ("child_changed".equals(eventName)) { if ("child_changed".equals(eventName)) {
query.removeEventListener(this); query.removeEventListener(this);
WritableMap data = Utils.snapshotToMap("child_changed", path, dataSnapshot, previousChildName, refId); WritableMap data = Utils.snapshotToMap(dataSnapshot, previousChildName);
promise.resolve(data); promise.resolve(data);
} }
} }
@ -107,7 +105,7 @@ public class RNFirebaseDatabaseReference {
public void onChildRemoved(DataSnapshot dataSnapshot) { public void onChildRemoved(DataSnapshot dataSnapshot) {
if ("child_removed".equals(eventName)) { if ("child_removed".equals(eventName)) {
query.removeEventListener(this); query.removeEventListener(this);
WritableMap data = Utils.snapshotToMap("child_removed", path, dataSnapshot, null, refId); WritableMap data = Utils.snapshotToMap(dataSnapshot, null);
promise.resolve(data); promise.resolve(data);
} }
} }
@ -116,7 +114,7 @@ public class RNFirebaseDatabaseReference {
public void onChildMoved(DataSnapshot dataSnapshot, String previousChildName) { public void onChildMoved(DataSnapshot dataSnapshot, String previousChildName) {
if ("child_moved".equals(eventName)) { if ("child_moved".equals(eventName)) {
query.removeEventListener(this); query.removeEventListener(this);
WritableMap data = Utils.snapshotToMap("child_moved", path, dataSnapshot, previousChildName, refId); WritableMap data = Utils.snapshotToMap(dataSnapshot, previousChildName);
promise.resolve(data); promise.resolve(data);
} }
} }
@ -135,13 +133,14 @@ public class RNFirebaseDatabaseReference {
/** /**
* Handles a React Native JS 'on' request and initializes listeners. * Handles a React Native JS 'on' request and initializes listeners.
* *
* @param eventName * @param database
* @param registration
*/ */
void on(RNFirebaseDatabase database, String eventName, String queryKey) { void on(RNFirebaseDatabase database, String eventType, ReadableMap registration) {
if (eventName.equals("value")) { if (eventType.equals("value")) {
addValueEventListener(queryKey, database); addValueEventListener(registration, database);
} else { } else {
addChildEventListener(queryKey, database, eventName); addChildEventListener(registration, eventType, database);
} }
} }
@ -161,94 +160,100 @@ public class RNFirebaseDatabaseReference {
/** /**
* @param queryKey * @param registration
* @param eventName * @param eventType
* @param database
*/ */
private void addChildEventListener(final String queryKey, final RNFirebaseDatabase database, final String eventName) { private void addChildEventListener(final ReadableMap registration, final String eventType, final RNFirebaseDatabase database) {
if (!database.hasChildEventListener(queryKey)) { final String eventRegistrationKey = registration.getString("eventRegistrationKey");
final String registrationCancellationKey = registration.getString("registrationCancellationKey");
if (!database.hasChildEventListener(eventRegistrationKey)) {
ChildEventListener childEventListener = new ChildEventListener() { ChildEventListener childEventListener = new ChildEventListener() {
@Override @Override
public void onChildAdded(DataSnapshot dataSnapshot, String previousChildName) { public void onChildAdded(DataSnapshot dataSnapshot, String previousChildName) {
if ("child_added".equals(eventName)) { if ("child_added".equals(eventType)) {
handleDatabaseEvent("child_added", queryKey, dataSnapshot, previousChildName); handleDatabaseEvent("child_added", registration, dataSnapshot, previousChildName);
} }
} }
@Override @Override
public void onChildChanged(DataSnapshot dataSnapshot, String previousChildName) { public void onChildChanged(DataSnapshot dataSnapshot, String previousChildName) {
if ("child_changed".equals(eventName)) { if ("child_changed".equals(eventType)) {
handleDatabaseEvent("child_changed", queryKey, dataSnapshot, previousChildName); handleDatabaseEvent("child_changed", registration, dataSnapshot, previousChildName);
} }
} }
@Override @Override
public void onChildRemoved(DataSnapshot dataSnapshot) { public void onChildRemoved(DataSnapshot dataSnapshot) {
if ("child_removed".equals(eventName)) { if ("child_removed".equals(eventType)) {
handleDatabaseEvent("child_removed", queryKey, dataSnapshot, null); handleDatabaseEvent("child_removed", registration, dataSnapshot, null);
} }
} }
@Override @Override
public void onChildMoved(DataSnapshot dataSnapshot, String previousChildName) { public void onChildMoved(DataSnapshot dataSnapshot, String previousChildName) {
if ("child_moved".equals(eventName)) { if ("child_moved".equals(eventType)) {
handleDatabaseEvent("child_moved", queryKey, dataSnapshot, previousChildName); handleDatabaseEvent("child_moved", registration, dataSnapshot, previousChildName);
} }
} }
@Override @Override
public void onCancelled(DatabaseError error) { public void onCancelled(DatabaseError error) {
query.removeEventListener(this); query.removeEventListener(this);
database.removeChildEventListener(queryKey); database.removeChildEventListener(eventRegistrationKey);
handleDatabaseError(queryKey, error); handleDatabaseError(registration, error);
} }
}; };
database.addChildEventListener(queryKey, childEventListener); database.addChildEventListener(eventRegistrationKey, childEventListener);
query.addChildEventListener(childEventListener); query.addChildEventListener(childEventListener);
} }
} }
/** /**
* @param queryKey * @param registration
*/ */
private void addValueEventListener(final String queryKey, final RNFirebaseDatabase database) { private void addValueEventListener(final ReadableMap registration, final RNFirebaseDatabase database) {
if (!database.hasValueEventListener(queryKey)) { final String eventRegistrationKey = registration.getString("eventRegistrationKey");
final String registrationCancellationKey = registration.getString("registrationCancellationKey");
if (!database.hasValueEventListener(eventRegistrationKey)) {
ValueEventListener valueEventListener = new ValueEventListener() { ValueEventListener valueEventListener = new ValueEventListener() {
@Override @Override
public void onDataChange(DataSnapshot dataSnapshot) { public void onDataChange(DataSnapshot dataSnapshot) {
handleDatabaseEvent("value", queryKey, dataSnapshot, null); handleDatabaseEvent("value", registration, dataSnapshot, null);
} }
@Override @Override
public void onCancelled(DatabaseError error) { public void onCancelled(DatabaseError error) {
query.removeEventListener(this); query.removeEventListener(this);
database.removeValueEventListener(queryKey); database.removeValueEventListener(eventRegistrationKey);
handleDatabaseError(queryKey, error); handleDatabaseError(registration, error);
} }
}; };
database.addValueEventListener(queryKey, valueEventListener); database.addValueEventListener(eventRegistrationKey, valueEventListener);
query.addValueEventListener(valueEventListener); query.addValueEventListener(valueEventListener);
} }
} }
/** /**
* @param name * @param eventType
* @param dataSnapshot * @param dataSnapshot
* @param previousChildName * @param previousChildName
*/ */
private void handleDatabaseEvent(final String name, String queryKey, final DataSnapshot dataSnapshot, @Nullable String previousChildName) { private void handleDatabaseEvent(String eventType, ReadableMap registration, DataSnapshot dataSnapshot, @Nullable String previousChildName) {
WritableMap evt = Arguments.createMap(); WritableMap event = Arguments.createMap();
WritableMap data = Utils.snapshotToMap(name, path, dataSnapshot, previousChildName, refId); WritableMap data = Utils.snapshotToMap(dataSnapshot, previousChildName);
evt.putMap("body", data); event.putMap("data", data);
evt.putInt("refId", refId); event.putString("key", key);
evt.putString("eventName", name); event.putString("eventType", eventType);
evt.putString("appName", appName); event.putMap("registration", Utils.readableMapToWritableMap(registration));
evt.putString("queryKey", queryKey);
Utils.sendEvent(reactContext, "database_on_event", evt); Utils.sendEvent(reactContext, "database_sync_event", event);
} }
/** /**
@ -256,13 +261,14 @@ public class RNFirebaseDatabaseReference {
* *
* @param error * @param error
*/ */
private void handleDatabaseError(String queryKey, DatabaseError error) { private void handleDatabaseError(ReadableMap registration, DatabaseError error) {
WritableMap errMap = RNFirebaseDatabase.getJSError(error); WritableMap event = Arguments.createMap();
errMap.putInt("refId", refId);
errMap.putString("path", path); event.putString("key", key);
errMap.putString("appName", appName); event.putMap("error", RNFirebaseDatabase.getJSError(error));
errMap.putString("queryKey", queryKey); event.putMap("registration", Utils.readableMapToWritableMap(registration));
Utils.sendEvent(reactContext, "database_cancel_event", errMap);
Utils.sendEvent(reactContext, "database_sync_event", event);
} }

View File

@ -131,6 +131,7 @@ export default class FirebaseApp {
Object.assign(getInstance, statics, { Object.assign(getInstance, statics, {
nativeModuleExists: !!NativeModules[`RNFirebase${capitalizeFirstLetter(name)}`], nativeModuleExists: !!NativeModules[`RNFirebase${capitalizeFirstLetter(name)}`],
}); });
return getInstance; return getInstance;
} }
} }

View File

@ -1,5 +1,5 @@
import EventEmitter from 'EventEmitter'; import EventEmitter from 'EventEmitter';
import { Platform } from 'react-native'; import { Platform, NativeModules } from 'react-native';
const DEFAULT_APP_NAME = Platform.OS === 'ios' ? '__FIRAPP_DEFAULT' : '[DEFAULT]'; const DEFAULT_APP_NAME = Platform.OS === 'ios' ? '__FIRAPP_DEFAULT' : '[DEFAULT]';
@ -54,7 +54,7 @@ export default {
SharedEventEmitter: new EventEmitter(), SharedEventEmitter: new EventEmitter(),
SyncTree: NativeModules.RNFirebaseDatabase ? new SyncTree(NativeModules.RNFirebaseDatabase) : null,
// internal utils // internal utils
deleteApp(name: String) { deleteApp(name: String) {

View File

@ -5,10 +5,8 @@
import { NativeModules } from 'react-native'; import { NativeModules } from 'react-native';
import Reference from './reference'; import Reference from './reference';
import Snapshot from './snapshot';
import TransactionHandler from './transaction'; import TransactionHandler from './transaction';
import ModuleBase from './../../utils/ModuleBase'; import ModuleBase from './../../utils/ModuleBase';
import { nativeToJSError } from './../../utils';
/** /**
* @class Database * @class Database
@ -16,8 +14,6 @@ import { nativeToJSError } from './../../utils';
export default class Database extends ModuleBase { export default class Database extends ModuleBase {
constructor(firebaseApp: Object, options: Object = {}) { constructor(firebaseApp: Object, options: Object = {}) {
super(firebaseApp, options, 'Database', true); super(firebaseApp, options, 'Database', true);
this._references = {};
this._serverTimeOffset = 0;
this._transactionHandler = new TransactionHandler(this); this._transactionHandler = new TransactionHandler(this);
if (this._options.persistence) { if (this._options.persistence) {
@ -25,68 +21,7 @@ export default class Database extends ModuleBase {
} }
// todo serverTimeOffset event/listener - make ref natively and switch to events // todo serverTimeOffset event/listener - make ref natively and switch to events
// todo use nativeToJSError for on/off error events this._serverTimeOffset = 0; // TODO ----^
this.addListener(
this._getAppEventName('database_cancel_event'),
this._handleCancelEvent.bind(this),
);
this.addListener(
this._getAppEventName('database_on_event'),
this._handleOnEvent.bind(this),
);
}
/**
* Routes native database 'on' events to their js equivalent counterpart.
* If there is no longer any listeners remaining for this event we internally
* call the native unsub method to prevent further events coming through.
*
* @param event
* @private
*/
_handleOnEvent(event) {
const { queryKey, body, refId } = event;
const { snapshot, previousChildName } = body;
const remainingListeners = this.listeners(queryKey);
if (!remainingListeners || !remainingListeners.length) {
this._database._native.off(
_refId,
queryKey,
);
delete this._references[refId];
} else {
const ref = this._references[refId];
if (!ref) {
this._database._native.off(
_refId,
queryKey,
);
} else {
this.emit(queryKey, new Snapshot(ref, snapshot), previousChildName);
}
}
}
/**
* Routes native database query listener cancellation events to their js counterparts.
*
* @param event
* @private
*/
_handleCancelEvent(event) {
const { queryKey, code, message, path, refId, appName } = event;
const remainingListeners = this.listeners(`${queryKey}:cancelled`);
if (remainingListeners && remainingListeners.length) {
const error = nativeToJSError(code, message, { path, queryKey, refId, appName });
this.emit(`${queryKey}:cancelled`, error);
}
} }
/** /**

View File

@ -5,8 +5,8 @@
import Query from './query.js'; import Query from './query.js';
import Snapshot from './snapshot'; import Snapshot from './snapshot';
import Disconnect from './disconnect'; import Disconnect from './disconnect';
import INTERNALS from './../../internals';
import ReferenceBase from './../../utils/ReferenceBase'; import ReferenceBase from './../../utils/ReferenceBase';
import { import {
promiseOrCallback, promiseOrCallback,
isFunction, isFunction,
@ -17,8 +17,9 @@ import {
generatePushID, generatePushID,
} from './../../utils'; } from './../../utils';
// Unique Reference ID for native events import INTERNALS from './../../internals';
let refId = 1;
// track all event registrations by path
/** /**
* Enum for event types * Enum for event types
@ -62,7 +63,6 @@ const ReferenceEventTypes = {
*/ */
export default class Reference extends ReferenceBase { export default class Reference extends ReferenceBase {
_refId: number;
_refListeners: { [listenerId: number]: DatabaseListener }; _refListeners: { [listenerId: number]: DatabaseListener };
_database: Object; _database: Object;
_query: Query; _query: Query;
@ -70,13 +70,12 @@ export default class Reference extends ReferenceBase {
constructor(database: Object, path: string, existingModifiers?: Array<DatabaseModifier>) { constructor(database: Object, path: string, existingModifiers?: Array<DatabaseModifier>) {
super(path, database); super(path, database);
this._promise = null; this._promise = null;
this._refId = refId++;
this._listeners = 0; this._listeners = 0;
this._refListeners = {}; this._refListeners = {};
this._database = database; this._database = database;
this._query = new Query(this, path, existingModifiers); this._query = new Query(this, path, existingModifiers);
this.log = this._database.log; this.log = this._database.log;
this.log.debug('Created new Reference', this._refId, this.path); this.log.debug('Created new Reference', this._getRefKey());
} }
/** /**
@ -90,7 +89,7 @@ export default class Reference extends ReferenceBase {
* @returns {*} * @returns {*}
*/ */
keepSynced(bool: boolean) { keepSynced(bool: boolean) {
return this._database._native.keepSynced(this._refId, this.path, this._query.getModifiers(), bool); return this._database._native.keepSynced(this._getRefKey(), this.path, this._query.getModifiers(), bool);
} }
/** /**
@ -225,7 +224,7 @@ export default class Reference extends ReferenceBase {
cancelOrContext: (error: FirebaseError) => void, cancelOrContext: (error: FirebaseError) => void,
context?: Object, context?: Object,
) { ) {
return this._database._native.once(this._refId, this.path, this._query.getModifiers(), eventName) return this._database._native.once(this._getRefKey(), this.path, this._query.getModifiers(), eventName)
.then(({ snapshot }) => { .then(({ snapshot }) => {
const _snapshot = new Snapshot(this, snapshot); const _snapshot = new Snapshot(this, snapshot);
@ -513,11 +512,23 @@ export default class Reference extends ReferenceBase {
*/ */
/** /**
* Generate a unique key based on this refs path and query modifiers * Generate a unique registration key.
*
* @return {string} * @return {string}
*/ */
makeQueryKey() { _getRegistrationKey(eventType) {
return `$${this.path}$${this._query.queryIdentifier()}$${this._refId}$${this._listeners}`; return `$${this._database._appName}$${this.path}$${this._query.queryIdentifier()}$${this._listeners}$${eventType}`;
}
/**
* Generate a string that uniquely identifies this
* combination of path and query modifiers
*
* @return {string}
* @private
*/
_getRefKey() {
return `$${this._database._appName}$${this.path}$${this._query.queryIdentifier()}`;
} }
/** /**
@ -571,17 +582,32 @@ export default class Reference extends ReferenceBase {
}; };
} }
/** /**
* Register a listener for data changes at the current ref's location.
* The primary method of reading data from a Database.
* *
* @param eventType * Listeners can be unbound using {@link off}.
* @param callback *
* @param cancelCallbackOrContext * Event Types:
* @param context *
* @return {*} * - value: {@link callback}.
* - child_added: {@link callback}
* - child_removed: {@link callback}
* - child_changed: {@link callback}
* - child_moved: {@link callback}
*
* @param {ReferenceEventType} eventType - Type of event to attach a callback for.
* @param {ReferenceEventCallback} callback - Function that will be called
* when the event occurs with the new data.
* @param {cancelCallbackOrContext=} cancelCallbackOrContext - Optional callback that is called
* if the event subscription fails. {@link cancelCallbackOrContext}
* @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}
*/ */
// todo:context shouldn't be needed - confirm
// todo refId should no longer be required - update native to work without it then remove from js internals
on(eventType: string, callback: () => any, cancelCallbackOrContext?: () => any, context?: Object): Function { on(eventType: string, callback: () => any, cancelCallbackOrContext?: () => any, context?: Object): Function {
if (!eventType) { if (!eventType) {
throw new Error('Query.on failed: Function called with 0 arguments. Expects at least 2.'); throw new Error('Query.on failed: Function called with 0 arguments. Expects at least 2.');
@ -607,38 +633,88 @@ export default class Reference extends ReferenceBase {
throw new Error('Query.on failed: Function called with 4 arguments, but third optional argument `cancelCallbackOrContext` was not a function.'); throw new Error('Query.on failed: Function called with 4 arguments, but third optional argument `cancelCallbackOrContext` was not a function.');
} }
const eventQueryKey = `${this.makeQueryKey()}$${eventType}`; const eventRegistrationKey = this._getRegistrationKey(eventType);
const registrationCancellationKey = `${eventRegistrationKey}$cancelled`;
const _context = (cancelCallbackOrContext && !isFunction(cancelCallbackOrContext)) ? cancelCallbackOrContext : context; const _context = (cancelCallbackOrContext && !isFunction(cancelCallbackOrContext)) ? cancelCallbackOrContext : context;
INTERNALS.SharedEventEmitter.addListener(eventQueryKey, _context ? callback.bind(_context) : callback); this._syncTree.addRegistration(
{
eventType,
ref: this,
path: this.path,
key: this._getRefKey(),
appName: this._database._appName,
registration: eventRegistrationKey,
},
_context ? callback.bind(_context) : callback,
);
if (isFunction(cancelCallbackOrContext)) { if (isFunction(cancelCallbackOrContext)) {
INTERNALS.SharedEventEmitter.once(`${eventQueryKey}:cancelled`, _context ? cancelCallbackOrContext.bind(_context) : cancelCallbackOrContext); // cancellations have their own separate registration
// as these are one off events, and they're not guaranteed
// to occur either, only happens on failure to register on native
this._syncTree.addRegistration(
{
ref: this,
once: true,
path: this.path,
key: this._getRefKey(),
appName: this._database._appName,
eventType: `${eventType}$cancelled`,
registration: registrationCancellationKey,
},
_context ? cancelCallbackOrContext.bind(_context) : cancelCallbackOrContext,
);
} }
// initialise the native listener if not already listening // initialise the native listener if not already listening
this._database._native.on({ this._database._native.on({
eventType, eventType,
eventQueryKey,
id: this._refId, // todo remove
path: this.path, path: this.path,
key: this._getRefKey(),
appName: this._database._appName,
modifiers: this._query.getModifiers(), modifiers: this._query.getModifiers(),
registration: {
eventRegistrationKey,
registrationCancellationKey,
hasCancellationCallback: isFunction(cancelCallbackOrContext),
},
}); });
if (!this._database._references[this._refId]) { // increment number of listeners - just s short way of making
this._database._references[this._refId] = this; // every registration unique per .on() call
}
this._listeners = this._listeners + 1; this._listeners = this._listeners + 1;
// return original unbound successCallback for
// the purposes of calling .off(eventType, callback) at a later date
return callback; return callback;
} }
/** /**
* Detaches a callback previously attached with on().
* *
* Detach a callback previously attached with on(). Note that if on() was called
* multiple times with the same eventType and callback, the callback will be called
* multiple times for each event, and off() must be called multiple times to
* remove the callback. Calling off() on a parent listener will not automatically
* remove listeners registered on child nodes, off() must also be called on any
* child listeners to remove the callback.
*
* If a callback is not specified, all callbacks for the specified eventType will be removed.
* Similarly, if no eventType or callback is specified, all callbacks for the Reference will be removed.
* @param eventType * @param eventType
* @param originalCallback * @param originalCallback
*/ */
off(eventType?: string = '', originalCallback?: () => any) { off(eventType?: string = '', originalCallback?: () => any) {
if (!arguments.length) {
// Firebase Docs:
// if no eventType or callback is specified, all callbacks for the Reference will be removed.
return this._syncTree.removeListenersForRegistrations(this._syncTree.getRegistrationsByPath(this.path));
}
/*
* VALIDATE ARGS
*/
if (eventType && (!isString(eventType) || !ReferenceEventTypes[eventType])) { if (eventType && (!isString(eventType) || !ReferenceEventTypes[eventType])) {
throw new Error(`Query.off failed: First argument must be a valid string event type: "${Object.keys(ReferenceEventTypes).join(', ')}"`); throw new Error(`Query.off failed: First argument must be a valid string event type: "${Object.keys(ReferenceEventTypes).join(', ')}"`);
} }
@ -647,34 +723,36 @@ export default class Reference extends ReferenceBase {
throw new Error('Query.off failed: Function called with 2 arguments, but second optional argument was not a function.'); throw new Error('Query.off failed: Function called with 2 arguments, but second optional argument was not a function.');
} }
if (eventType) { // Firebase Docs:
const eventQueryKey = `${this.makeQueryKey()}$${eventType}`; // Note that if on() was called
// multiple times with the same eventType and callback, the callback will be called
// multiple times for each event, and off() must be called multiple times to
// remove the callback.
// Remove only a single registration
if (eventType && originalCallback) {
const registrations = this._syncTree.getRegistrationsByPathEvent(this.path, eventType);
if (originalCallback) { // remove the paired cancellation registration if any exist
INTERNALS.SharedEventEmitter.removeListener(eventQueryKey, originalCallback); this._syncTree.removeListenersForRegistrations([`${registrations[0]}$cancelled`]);
} else {
INTERNALS.SharedEventEmitter.removeAllListeners(eventQueryKey); // remove only the first registration to match firebase web sdk
INTERNALS.SharedEventEmitter.removeAllListeners(`${this.makeQueryKey()}:cancelled`); // call multiple times to remove multiple registrations
return this._syncTree.removeListenerRegistrations(originalCallback, [registrations[0]]);
} }
// check if there's any listeners remaining in the js thread // Firebase Docs:
// if there's isn't then call the native .off method which // If a callback is not specified, all callbacks for the specified eventType will be removed.
// will unsubscribe from the native firebase listeners const registrations = this._syncTree.getRegistrationsByPathEvent(this.path, eventType);
const remainingListeners = INTERNALS.SharedEventEmitter.listeners(eventQueryKey);
if (!remainingListeners || !remainingListeners.length) { this._syncTree.removeListenersForRegistrations(
this._database._native.off( this._syncTree.getRegistrationsByPathEvent(this.path, `${eventType}$cancelled`),
this._refId, // todo remove
eventQueryKey,
); );
// remove straggling cancellation listeners return this._syncTree.removeListenersForRegistrations(registrations);
INTERNALS.SharedEventEmitter.removeAllListeners(`${this.makeQueryKey()}:cancelled`);
} }
} else {
// todo remove all associated event subs if no event type && no orignalCb get _syncTree() {
return INTERNALS.SyncTree;
} }
} }
}

View File

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

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

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

View File

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

30
tests/src/playground.js Normal file
View File

@ -0,0 +1,30 @@
import React, { Component } from 'react';
import { View, Text, Button } from 'react-native';
export default class HomeScreen extends Component {
constructor(props) {
super(props);
this.state = {
bgColor: '#cb2600',
};
}
clickMe = () => {
if (this.state.bgColor === '#a8139f') {
this.setState({ bgColor: '#cb2600' });
} else {
this.setState({ bgColor: '#a8139f' });
}
};
render() {
return (
<View style={{ backgroundColor: this.state.bgColor }}>
<Text style={{ color: '#fff' }}>Hello</Text>
<Text style={{ color: '#22ff31' }}>World</Text>
<Button title="Change to pink" onPress={this.clickMe} />
</View>
);
}
}

View File

@ -26,8 +26,8 @@ const testGroups = [
pushTests, onTests, onValueTests, onChildAddedTests, onceTests, updateTests, pushTests, onTests, onValueTests, onChildAddedTests, onceTests, updateTests,
removeTests, setTests, transactionTests, queryTests, refTests, isEqualTests, removeTests, setTests, transactionTests, queryTests, refTests, isEqualTests,
priorityTests, priorityTests,
onValueTests, onChildAddedTests, // offTests, // TODO remove for now, until i can fix, want to see the others working first onValueTests, onChildAddedTests,
// onTests, offTests,
]; ];
function registerTestSuite(testSuite) { function registerTestSuite(testSuite) {

View File

@ -89,7 +89,13 @@ function offTests({ describe, it, xcontext, context, firebase }) {
// Check childAddedCallback is really attached // Check childAddedCallback is really attached
await ref.push(DatabaseContents.DEFAULT.number); await ref.push(DatabaseContents.DEFAULT.number);
valueCallback.should.be.callCount(2);
// stinky test fix - it's all async now so it's not returned within same event loop
await new Promise((resolve) => {
setTimeout(() => resolve(), 15);
});
valueCallback.should.be.calledTwice();
childAddedCallback.should.be.callCount(arrayLength + 1); childAddedCallback.should.be.callCount(arrayLength + 1);
// Returns nothing // Returns nothing
@ -117,10 +123,9 @@ function offTests({ describe, it, xcontext, context, firebase }) {
}); });
context('that is invalid', () => { context('that is invalid', () => {
it('does nothing', () => { it('throws an exception', () => {
const ref = firebase.native.database().ref('tests/types/array'); const ref = firebase.native.database().ref('tests/types/array');
(() => ref.off('invalid')).should.throw('Query.off failed: First argument must be a valid string event type: "value, child_added, child_removed, child_changed, child_moved"');
should(ref.off('invalid'), undefined);
}); });
}); });
@ -225,7 +230,7 @@ function offTests({ describe, it, xcontext, context, firebase }) {
}); });
context('that has been added multiple times', () => { context('that has been added multiple times', () => {
it('must be called as many times completely remove', async () => { it('must be called as many times to completely remove', async () => {
// Setup // Setup
const spyA = sinon.spy(); const spyA = sinon.spy();
@ -249,7 +254,7 @@ function offTests({ describe, it, xcontext, context, firebase }) {
await new Promise((resolve) => { await new Promise((resolve) => {
setTimeout(() => { setTimeout(() => {
resolve(); resolve();
}, 1000); }, 15);
}); });
spyA.should.be.calledTwice(); spyA.should.be.calledTwice();
@ -261,9 +266,16 @@ function offTests({ describe, it, xcontext, context, firebase }) {
// Trigger the event the callback is listening to // Trigger the event the callback is listening to
await ref.set(DatabaseContents.DEFAULT.number); await ref.set(DatabaseContents.DEFAULT.number);
// Add a delay to ensure that the .set() has had time to be registered
await new Promise((resolve) => {
setTimeout(() => {
resolve();
}, 15);
});
// Callback should have been called only once because one of the attachments // Callback should have been called only once because one of the attachments
// has been removed // has been removed
spyA.should.be.callCount(3); spyA.should.be.calledThrice();
// Undo the second attachment // Undo the second attachment
const resp2 = await ref.off('value', callbackA); const resp2 = await ref.off('value', callbackA);
@ -272,8 +284,15 @@ function offTests({ describe, it, xcontext, context, firebase }) {
// Trigger the event the callback is listening to // Trigger the event the callback is listening to
await ref.set(DatabaseContents.DEFAULT.number); await ref.set(DatabaseContents.DEFAULT.number);
// Add a delay to ensure that the .set() has had time to be registered
await new Promise((resolve) => {
setTimeout(() => {
resolve();
}, 15);
});
// Callback should not have been called any more times // Callback should not have been called any more times
spyA.should.be.callCount(3); spyA.should.be.calledThrice();
}); });
}); });
}); });

View File

@ -60,23 +60,19 @@ function pushTests({ describe, it, firebase }) {
return ref.once('value') return ref.once('value')
.then((snapshot) => { .then((snapshot) => {
console.log('first once');
originalListValue = snapshot.val(); originalListValue = snapshot.val();
return ref.push(valueToAddToList); return ref.push(valueToAddToList);
}) })
.then((pushRef) => { .then((pushRef) => {
console.log('after push');
newItemRef = pushRef; newItemRef = pushRef;
return newItemRef.once('value'); return newItemRef.once('value');
}) })
.then((snapshot) => { .then((snapshot) => {
console.log('second once');
newItemValue = snapshot.val(); newItemValue = snapshot.val();
newItemValue.should.eql(valueToAddToList); newItemValue.should.eql(valueToAddToList);
return firebase.native.database().ref('tests/types/array').once('value'); return firebase.native.database().ref('tests/types/array').once('value');
}) })
.then((snapshot) => { .then((snapshot) => {
console.log('third once');
newListValue = snapshot.val(); newListValue = snapshot.val();
const originalListAsObject = originalListValue.reduce((memo, value, index) => { const originalListAsObject = originalListValue.reduce((memo, value, index) => {
memo[index] = value; memo[index] = value;