[js][android] database sync tree implementation with adjusted tests
This commit is contained in:
parent
67985f8e90
commit
e4d27029b9
|
@ -77,20 +77,21 @@ public class Utils {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param name
|
||||
* @param path
|
||||
* @param dataSnapshot
|
||||
* @param refId
|
||||
* @param registration
|
||||
* @param previousChildName
|
||||
* @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 eventMap = Arguments.createMap();
|
||||
|
||||
snapshot.putString("key", dataSnapshot.getKey());
|
||||
snapshot.putBoolean("exists", dataSnapshot.exists());
|
||||
snapshot.putBoolean("hasChildren", dataSnapshot.hasChildren());
|
||||
snapshot.putDouble("childrenCount", dataSnapshot.getChildrenCount());
|
||||
snapshot.putArray("childKeys", Utils.getChildKeys(dataSnapshot));
|
||||
mapPutValue("priority", dataSnapshot.getPriority(), snapshot);
|
||||
|
||||
if (!dataSnapshot.hasChildren()) {
|
||||
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);
|
||||
eventMap.putString("path", path);
|
||||
eventMap.putMap("snapshot", snapshot);
|
||||
eventMap.putString("eventName", name);
|
||||
eventMap.putString("previousChildName", previousChildName);
|
||||
result.putMap("snapshot", snapshot);
|
||||
result.putString("previousChildName", previousChildName);
|
||||
return result;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -35,7 +35,7 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule {
|
|||
private static final String TAG = "RNFirebaseDatabase";
|
||||
private HashMap<String, ChildEventListener> childEventListeners;
|
||||
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<>();
|
||||
|
||||
RNFirebaseDatabase(ReactApplicationContext reactContext) {
|
||||
|
@ -103,10 +103,8 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule {
|
|||
* @param state
|
||||
*/
|
||||
@ReactMethod
|
||||
public void keepSynced(String appName, int id, String path, ReadableArray modifiers, Boolean state) {
|
||||
getInternalReferenceForApp(appName, id, path, modifiers, false)
|
||||
.getQuery()
|
||||
.keepSynced(state);
|
||||
public void keepSynced(String appName, String key, String path, ReadableArray modifiers, Boolean state) {
|
||||
getInternalReferenceForApp(appName, key, path, modifiers).getQuery().keepSynced(state);
|
||||
}
|
||||
|
||||
|
||||
|
@ -410,34 +408,36 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule {
|
|||
* Subscribe once to a firebase reference.
|
||||
*
|
||||
* @param appName
|
||||
* @param refId
|
||||
* @param key
|
||||
* @param path
|
||||
* @param modifiers
|
||||
* @param eventName
|
||||
* @param promise
|
||||
*/
|
||||
@ReactMethod
|
||||
public void once(String appName, int refId, String path, ReadableArray modifiers, String eventName, Promise promise) {
|
||||
getInternalReferenceForApp(appName, refId, path, modifiers, false).once(eventName, promise);
|
||||
public void once(String appName, String key, String path, ReadableArray modifiers, String eventName, Promise promise) {
|
||||
getInternalReferenceForApp(appName, key, path, modifiers).once(eventName, promise);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to real time events for the specified database path + modifiers
|
||||
*
|
||||
* @param appName String
|
||||
* @param args ReadableMap
|
||||
* @param props ReadableMap
|
||||
*/
|
||||
@ReactMethod
|
||||
public void on(String appName, ReadableMap args) {
|
||||
RNFirebaseDatabaseReference ref = getInternalReferenceForApp(
|
||||
appName,
|
||||
args.getInt("id"),
|
||||
args.getString("path"),
|
||||
args.getArray("modifiers"),
|
||||
true
|
||||
public void on(String appName, ReadableMap props) {
|
||||
getInternalReferenceForApp(appName, props).on(
|
||||
this,
|
||||
props.getString("eventType"),
|
||||
props.getMap("registration")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@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.
|
||||
*
|
||||
* @param appName
|
||||
* @param refId
|
||||
* @param key
|
||||
* @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);
|
||||
private RNFirebaseDatabaseReference getInternalReferenceForApp(String appName, String key, String path, ReadableArray modifiers) {
|
||||
RNFirebaseDatabaseReference existingRef = references.get(key);
|
||||
|
||||
if (existingRef == null) {
|
||||
existingRef = new RNFirebaseDatabaseReference(
|
||||
getReactApplicationContext(),
|
||||
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,
|
||||
modifiers
|
||||
);
|
||||
|
||||
if (keep) references.put(refId, existingRef);
|
||||
references.put(key, existingRef);
|
||||
}
|
||||
|
||||
return existingRef;
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
package io.invertase.firebase.database;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
|
||||
import android.util.Log;
|
||||
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.WritableArray;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
|
@ -24,10 +22,10 @@ import com.google.firebase.database.ValueEventListener;
|
|||
|
||||
import io.invertase.firebase.Utils;
|
||||
|
||||
public class RNFirebaseDatabaseReference {
|
||||
class RNFirebaseDatabaseReference {
|
||||
private static final String TAG = "RNFirebaseDBReference";
|
||||
|
||||
private int refId;
|
||||
private String key;
|
||||
private Query query;
|
||||
private String path;
|
||||
private String appName;
|
||||
|
@ -41,12 +39,12 @@ public class RNFirebaseDatabaseReference {
|
|||
/**
|
||||
* @param context
|
||||
* @param app
|
||||
* @param id
|
||||
* @param refKey
|
||||
* @param refPath
|
||||
* @param modifiersArray
|
||||
*/
|
||||
RNFirebaseDatabaseReference(ReactContext context, String app, int id, String refPath, ReadableArray modifiersArray) {
|
||||
refId = id;
|
||||
RNFirebaseDatabaseReference(ReactContext context, String app, String refKey, String refPath, ReadableArray modifiersArray) {
|
||||
key = refKey;
|
||||
appName = app;
|
||||
path = refPath;
|
||||
reactContext = context;
|
||||
|
@ -62,7 +60,7 @@ public class RNFirebaseDatabaseReference {
|
|||
ValueEventListener onceValueEventListener = new ValueEventListener() {
|
||||
@Override
|
||||
public void onDataChange(DataSnapshot dataSnapshot) {
|
||||
WritableMap data = Utils.snapshotToMap("value", path, dataSnapshot, null, refId);
|
||||
WritableMap data = Utils.snapshotToMap(dataSnapshot, null);
|
||||
promise.resolve(data);
|
||||
}
|
||||
|
||||
|
@ -74,7 +72,7 @@ public class RNFirebaseDatabaseReference {
|
|||
|
||||
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) {
|
||||
if ("child_added".equals(eventName)) {
|
||||
query.removeEventListener(this);
|
||||
WritableMap data = Utils.snapshotToMap("child_added", path, dataSnapshot, previousChildName, refId);
|
||||
WritableMap data = Utils.snapshotToMap(dataSnapshot, previousChildName);
|
||||
promise.resolve(data);
|
||||
}
|
||||
}
|
||||
|
@ -98,7 +96,7 @@ public class RNFirebaseDatabaseReference {
|
|||
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);
|
||||
WritableMap data = Utils.snapshotToMap(dataSnapshot, previousChildName);
|
||||
promise.resolve(data);
|
||||
}
|
||||
}
|
||||
|
@ -107,7 +105,7 @@ public class RNFirebaseDatabaseReference {
|
|||
public void onChildRemoved(DataSnapshot dataSnapshot) {
|
||||
if ("child_removed".equals(eventName)) {
|
||||
query.removeEventListener(this);
|
||||
WritableMap data = Utils.snapshotToMap("child_removed", path, dataSnapshot, null, refId);
|
||||
WritableMap data = Utils.snapshotToMap(dataSnapshot, null);
|
||||
promise.resolve(data);
|
||||
}
|
||||
}
|
||||
|
@ -116,7 +114,7 @@ public class RNFirebaseDatabaseReference {
|
|||
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);
|
||||
WritableMap data = Utils.snapshotToMap(dataSnapshot, previousChildName);
|
||||
promise.resolve(data);
|
||||
}
|
||||
}
|
||||
|
@ -135,13 +133,14 @@ public class RNFirebaseDatabaseReference {
|
|||
/**
|
||||
* Handles a React Native JS 'on' request and initializes listeners.
|
||||
*
|
||||
* @param eventName
|
||||
* @param database
|
||||
* @param registration
|
||||
*/
|
||||
void on(RNFirebaseDatabase database, String eventName, String queryKey) {
|
||||
if (eventName.equals("value")) {
|
||||
addValueEventListener(queryKey, database);
|
||||
void on(RNFirebaseDatabase database, String eventType, ReadableMap registration) {
|
||||
if (eventType.equals("value")) {
|
||||
addValueEventListener(registration, database);
|
||||
} else {
|
||||
addChildEventListener(queryKey, database, eventName);
|
||||
addChildEventListener(registration, eventType, database);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -161,94 +160,100 @@ public class RNFirebaseDatabaseReference {
|
|||
|
||||
|
||||
/**
|
||||
* @param queryKey
|
||||
* @param eventName
|
||||
* @param registration
|
||||
* @param eventType
|
||||
* @param database
|
||||
*/
|
||||
private void addChildEventListener(final String queryKey, final RNFirebaseDatabase database, final String eventName) {
|
||||
if (!database.hasChildEventListener(queryKey)) {
|
||||
private void addChildEventListener(final ReadableMap registration, final String eventType, final RNFirebaseDatabase database) {
|
||||
final String eventRegistrationKey = registration.getString("eventRegistrationKey");
|
||||
final String registrationCancellationKey = registration.getString("registrationCancellationKey");
|
||||
|
||||
if (!database.hasChildEventListener(eventRegistrationKey)) {
|
||||
ChildEventListener childEventListener = new ChildEventListener() {
|
||||
@Override
|
||||
public void onChildAdded(DataSnapshot dataSnapshot, String previousChildName) {
|
||||
if ("child_added".equals(eventName)) {
|
||||
handleDatabaseEvent("child_added", queryKey, dataSnapshot, previousChildName);
|
||||
if ("child_added".equals(eventType)) {
|
||||
handleDatabaseEvent("child_added", registration, dataSnapshot, previousChildName);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChildChanged(DataSnapshot dataSnapshot, String previousChildName) {
|
||||
if ("child_changed".equals(eventName)) {
|
||||
handleDatabaseEvent("child_changed", queryKey, dataSnapshot, previousChildName);
|
||||
if ("child_changed".equals(eventType)) {
|
||||
handleDatabaseEvent("child_changed", registration, dataSnapshot, previousChildName);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChildRemoved(DataSnapshot dataSnapshot) {
|
||||
if ("child_removed".equals(eventName)) {
|
||||
handleDatabaseEvent("child_removed", queryKey, dataSnapshot, null);
|
||||
if ("child_removed".equals(eventType)) {
|
||||
handleDatabaseEvent("child_removed", registration, dataSnapshot, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChildMoved(DataSnapshot dataSnapshot, String previousChildName) {
|
||||
if ("child_moved".equals(eventName)) {
|
||||
handleDatabaseEvent("child_moved", queryKey, dataSnapshot, previousChildName);
|
||||
if ("child_moved".equals(eventType)) {
|
||||
handleDatabaseEvent("child_moved", registration, dataSnapshot, previousChildName);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancelled(DatabaseError error) {
|
||||
query.removeEventListener(this);
|
||||
database.removeChildEventListener(queryKey);
|
||||
handleDatabaseError(queryKey, error);
|
||||
database.removeChildEventListener(eventRegistrationKey);
|
||||
handleDatabaseError(registration, error);
|
||||
}
|
||||
};
|
||||
|
||||
database.addChildEventListener(queryKey, childEventListener);
|
||||
database.addChildEventListener(eventRegistrationKey, childEventListener);
|
||||
query.addChildEventListener(childEventListener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param queryKey
|
||||
* @param registration
|
||||
*/
|
||||
private void addValueEventListener(final String queryKey, final RNFirebaseDatabase database) {
|
||||
if (!database.hasValueEventListener(queryKey)) {
|
||||
private void addValueEventListener(final ReadableMap registration, final RNFirebaseDatabase database) {
|
||||
final String eventRegistrationKey = registration.getString("eventRegistrationKey");
|
||||
final String registrationCancellationKey = registration.getString("registrationCancellationKey");
|
||||
|
||||
if (!database.hasValueEventListener(eventRegistrationKey)) {
|
||||
ValueEventListener valueEventListener = new ValueEventListener() {
|
||||
@Override
|
||||
public void onDataChange(DataSnapshot dataSnapshot) {
|
||||
handleDatabaseEvent("value", queryKey, dataSnapshot, null);
|
||||
handleDatabaseEvent("value", registration, dataSnapshot, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancelled(DatabaseError error) {
|
||||
query.removeEventListener(this);
|
||||
database.removeValueEventListener(queryKey);
|
||||
handleDatabaseError(queryKey, error);
|
||||
database.removeValueEventListener(eventRegistrationKey);
|
||||
handleDatabaseError(registration, error);
|
||||
}
|
||||
};
|
||||
|
||||
database.addValueEventListener(queryKey, valueEventListener);
|
||||
database.addValueEventListener(eventRegistrationKey, valueEventListener);
|
||||
query.addValueEventListener(valueEventListener);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param name
|
||||
* @param eventType
|
||||
* @param dataSnapshot
|
||||
* @param previousChildName
|
||||
*/
|
||||
private void handleDatabaseEvent(final String name, String queryKey, final DataSnapshot dataSnapshot, @Nullable String previousChildName) {
|
||||
WritableMap evt = Arguments.createMap();
|
||||
WritableMap data = Utils.snapshotToMap(name, path, dataSnapshot, previousChildName, refId);
|
||||
private void handleDatabaseEvent(String eventType, ReadableMap registration, DataSnapshot dataSnapshot, @Nullable String previousChildName) {
|
||||
WritableMap event = Arguments.createMap();
|
||||
WritableMap data = Utils.snapshotToMap(dataSnapshot, previousChildName);
|
||||
|
||||
evt.putMap("body", data);
|
||||
evt.putInt("refId", refId);
|
||||
evt.putString("eventName", name);
|
||||
evt.putString("appName", appName);
|
||||
evt.putString("queryKey", queryKey);
|
||||
event.putMap("data", data);
|
||||
event.putString("key", key);
|
||||
event.putString("eventType", eventType);
|
||||
event.putMap("registration", Utils.readableMapToWritableMap(registration));
|
||||
|
||||
Utils.sendEvent(reactContext, "database_on_event", evt);
|
||||
Utils.sendEvent(reactContext, "database_sync_event", event);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -256,13 +261,14 @@ public class RNFirebaseDatabaseReference {
|
|||
*
|
||||
* @param error
|
||||
*/
|
||||
private void handleDatabaseError(String queryKey, DatabaseError error) {
|
||||
WritableMap errMap = RNFirebaseDatabase.getJSError(error);
|
||||
errMap.putInt("refId", refId);
|
||||
errMap.putString("path", path);
|
||||
errMap.putString("appName", appName);
|
||||
errMap.putString("queryKey", queryKey);
|
||||
Utils.sendEvent(reactContext, "database_cancel_event", errMap);
|
||||
private void handleDatabaseError(ReadableMap registration, DatabaseError error) {
|
||||
WritableMap event = Arguments.createMap();
|
||||
|
||||
event.putString("key", key);
|
||||
event.putMap("error", RNFirebaseDatabase.getJSError(error));
|
||||
event.putMap("registration", Utils.readableMapToWritableMap(registration));
|
||||
|
||||
Utils.sendEvent(reactContext, "database_sync_event", event);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -131,6 +131,7 @@ export default class FirebaseApp {
|
|||
Object.assign(getInstance, statics, {
|
||||
nativeModuleExists: !!NativeModules[`RNFirebase${capitalizeFirstLetter(name)}`],
|
||||
});
|
||||
|
||||
return getInstance;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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]';
|
||||
|
||||
|
@ -54,7 +54,7 @@ export default {
|
|||
|
||||
|
||||
SharedEventEmitter: new EventEmitter(),
|
||||
|
||||
SyncTree: NativeModules.RNFirebaseDatabase ? new SyncTree(NativeModules.RNFirebaseDatabase) : null,
|
||||
|
||||
// internal utils
|
||||
deleteApp(name: String) {
|
||||
|
|
|
@ -5,10 +5,8 @@
|
|||
import { NativeModules } from 'react-native';
|
||||
|
||||
import Reference from './reference';
|
||||
import Snapshot from './snapshot';
|
||||
import TransactionHandler from './transaction';
|
||||
import ModuleBase from './../../utils/ModuleBase';
|
||||
import { nativeToJSError } from './../../utils';
|
||||
|
||||
/**
|
||||
* @class Database
|
||||
|
@ -16,8 +14,6 @@ import { nativeToJSError } from './../../utils';
|
|||
export default class Database extends ModuleBase {
|
||||
constructor(firebaseApp: Object, options: Object = {}) {
|
||||
super(firebaseApp, options, 'Database', true);
|
||||
this._references = {};
|
||||
this._serverTimeOffset = 0;
|
||||
this._transactionHandler = new TransactionHandler(this);
|
||||
|
||||
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 use nativeToJSError for on/off error events
|
||||
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);
|
||||
}
|
||||
this._serverTimeOffset = 0; // TODO ----^
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
import Query from './query.js';
|
||||
import Snapshot from './snapshot';
|
||||
import Disconnect from './disconnect';
|
||||
import INTERNALS from './../../internals';
|
||||
import ReferenceBase from './../../utils/ReferenceBase';
|
||||
|
||||
import {
|
||||
promiseOrCallback,
|
||||
isFunction,
|
||||
|
@ -17,8 +17,9 @@ import {
|
|||
generatePushID,
|
||||
} from './../../utils';
|
||||
|
||||
// Unique Reference ID for native events
|
||||
let refId = 1;
|
||||
import INTERNALS from './../../internals';
|
||||
|
||||
// track all event registrations by path
|
||||
|
||||
/**
|
||||
* Enum for event types
|
||||
|
@ -62,7 +63,6 @@ const ReferenceEventTypes = {
|
|||
*/
|
||||
export default class Reference extends ReferenceBase {
|
||||
|
||||
_refId: number;
|
||||
_refListeners: { [listenerId: number]: DatabaseListener };
|
||||
_database: Object;
|
||||
_query: Query;
|
||||
|
@ -70,13 +70,12 @@ export default class Reference extends ReferenceBase {
|
|||
constructor(database: Object, path: string, existingModifiers?: Array<DatabaseModifier>) {
|
||||
super(path, database);
|
||||
this._promise = null;
|
||||
this._refId = refId++;
|
||||
this._listeners = 0;
|
||||
this._refListeners = {};
|
||||
this._database = database;
|
||||
this._query = new Query(this, path, existingModifiers);
|
||||
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 {*}
|
||||
*/
|
||||
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,
|
||||
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 }) => {
|
||||
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}
|
||||
*/
|
||||
makeQueryKey() {
|
||||
return `$${this.path}$${this._query.queryIdentifier()}$${this._refId}$${this._listeners}`;
|
||||
_getRegistrationKey(eventType) {
|
||||
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
|
||||
* @param callback
|
||||
* @param cancelCallbackOrContext
|
||||
* @param context
|
||||
* @return {*}
|
||||
* Listeners can be unbound using {@link off}.
|
||||
*
|
||||
* Event Types:
|
||||
*
|
||||
* - 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 {
|
||||
if (!eventType) {
|
||||
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.');
|
||||
}
|
||||
|
||||
const eventQueryKey = `${this.makeQueryKey()}$${eventType}`;
|
||||
const eventRegistrationKey = this._getRegistrationKey(eventType);
|
||||
const registrationCancellationKey = `${eventRegistrationKey}$cancelled`;
|
||||
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)) {
|
||||
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
|
||||
this._database._native.on({
|
||||
eventType,
|
||||
eventQueryKey,
|
||||
id: this._refId, // todo remove
|
||||
path: this.path,
|
||||
key: this._getRefKey(),
|
||||
appName: this._database._appName,
|
||||
modifiers: this._query.getModifiers(),
|
||||
registration: {
|
||||
eventRegistrationKey,
|
||||
registrationCancellationKey,
|
||||
hasCancellationCallback: isFunction(cancelCallbackOrContext),
|
||||
},
|
||||
});
|
||||
|
||||
if (!this._database._references[this._refId]) {
|
||||
this._database._references[this._refId] = this;
|
||||
}
|
||||
|
||||
// increment number of listeners - just s short way of making
|
||||
// every registration unique per .on() call
|
||||
this._listeners = this._listeners + 1;
|
||||
|
||||
// return original unbound successCallback for
|
||||
// the purposes of calling .off(eventType, callback) at a later date
|
||||
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 originalCallback
|
||||
*/
|
||||
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])) {
|
||||
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.');
|
||||
}
|
||||
|
||||
if (eventType) {
|
||||
const eventQueryKey = `${this.makeQueryKey()}$${eventType}`;
|
||||
// Firebase Docs:
|
||||
// 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) {
|
||||
INTERNALS.SharedEventEmitter.removeListener(eventQueryKey, originalCallback);
|
||||
} else {
|
||||
INTERNALS.SharedEventEmitter.removeAllListeners(eventQueryKey);
|
||||
INTERNALS.SharedEventEmitter.removeAllListeners(`${this.makeQueryKey()}:cancelled`);
|
||||
}
|
||||
// remove the paired cancellation registration if any exist
|
||||
this._syncTree.removeListenersForRegistrations([`${registrations[0]}$cancelled`]);
|
||||
|
||||
// check if there's any listeners remaining in the js thread
|
||||
// if there's isn't then call the native .off method which
|
||||
// will unsubscribe from the native firebase listeners
|
||||
const remainingListeners = INTERNALS.SharedEventEmitter.listeners(eventQueryKey);
|
||||
|
||||
if (!remainingListeners || !remainingListeners.length) {
|
||||
this._database._native.off(
|
||||
this._refId, // todo remove
|
||||
eventQueryKey,
|
||||
);
|
||||
|
||||
// remove straggling cancellation listeners
|
||||
INTERNALS.SharedEventEmitter.removeAllListeners(`${this.makeQueryKey()}:cancelled`);
|
||||
}
|
||||
} else {
|
||||
// todo remove all associated event subs if no event type && no orignalCb
|
||||
// remove only the first registration to match firebase web sdk
|
||||
// call multiple times to remove multiple registrations
|
||||
return this._syncTree.removeListenerRegistrations(originalCallback, [registrations[0]]);
|
||||
}
|
||||
|
||||
// Firebase Docs:
|
||||
// If a callback is not specified, all callbacks for the specified eventType will be removed.
|
||||
const registrations = this._syncTree.getRegistrationsByPathEvent(this.path, eventType);
|
||||
|
||||
this._syncTree.removeListenersForRegistrations(
|
||||
this._syncTree.getRegistrationsByPathEvent(this.path, `${eventType}$cancelled`),
|
||||
);
|
||||
|
||||
return this._syncTree.removeListenersForRegistrations(registrations);
|
||||
}
|
||||
|
||||
|
||||
get _syncTree() {
|
||||
return INTERNALS.SyncTree;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -25,8 +25,8 @@ const NATIVE_MODULE_EVENTS = {
|
|||
'onAuthStateChanged',
|
||||
],
|
||||
Database: [
|
||||
'database_on_event',
|
||||
'database_cancel_event',
|
||||
// 'database_on_event',
|
||||
// 'database_cancel_event',
|
||||
'database_transaction_event',
|
||||
// 'database_server_offset', // TODO
|
||||
],
|
||||
|
@ -47,11 +47,11 @@ export default class ModuleBase {
|
|||
* @param withEventEmitter
|
||||
*/
|
||||
constructor(firebaseApp, options, moduleName, withEventEmitter = false) {
|
||||
this._options = Object.assign({}, DEFAULTS[moduleName] || {}, options);
|
||||
this._module = moduleName;
|
||||
this._firebaseApp = firebaseApp;
|
||||
this._appName = firebaseApp._name;
|
||||
this._namespace = `${this._appName}:${this._module}`;
|
||||
this._options = Object.assign({}, DEFAULTS[moduleName] || {}, options);
|
||||
|
||||
// check if native module exists as all native
|
||||
// modules are now optionally part of build
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -231,6 +231,14 @@ export function map(
|
|||
}, () => 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.
|
||||
let lastPushTime = 0;
|
||||
|
@ -319,6 +327,11 @@ export function nativeWithApp(appName, NativeModule) {
|
|||
return native;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param object
|
||||
* @return {string}
|
||||
*/
|
||||
export function objectToUniqueId(object: Object): String {
|
||||
if (!isObject(object) || object === null) return JSON.stringify(object);
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -26,8 +26,8 @@ const testGroups = [
|
|||
pushTests, onTests, onValueTests, onChildAddedTests, onceTests, updateTests,
|
||||
removeTests, setTests, transactionTests, queryTests, refTests, isEqualTests,
|
||||
priorityTests,
|
||||
onValueTests, onChildAddedTests, // offTests, // TODO remove for now, until i can fix, want to see the others working first
|
||||
// onTests,
|
||||
onValueTests, onChildAddedTests,
|
||||
offTests,
|
||||
];
|
||||
|
||||
function registerTestSuite(testSuite) {
|
||||
|
|
|
@ -89,7 +89,13 @@ function offTests({ describe, it, xcontext, context, firebase }) {
|
|||
|
||||
// Check childAddedCallback is really attached
|
||||
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);
|
||||
|
||||
// Returns nothing
|
||||
|
@ -117,10 +123,9 @@ function offTests({ describe, it, xcontext, context, firebase }) {
|
|||
});
|
||||
|
||||
context('that is invalid', () => {
|
||||
it('does nothing', () => {
|
||||
it('throws an exception', () => {
|
||||
const ref = firebase.native.database().ref('tests/types/array');
|
||||
|
||||
should(ref.off('invalid'), undefined);
|
||||
(() => 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"');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -225,7 +230,7 @@ function offTests({ describe, it, xcontext, context, firebase }) {
|
|||
});
|
||||
|
||||
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
|
||||
|
||||
const spyA = sinon.spy();
|
||||
|
@ -249,7 +254,7 @@ function offTests({ describe, it, xcontext, context, firebase }) {
|
|||
await new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 1000);
|
||||
}, 15);
|
||||
});
|
||||
|
||||
spyA.should.be.calledTwice();
|
||||
|
@ -261,9 +266,16 @@ function offTests({ describe, it, xcontext, context, firebase }) {
|
|||
// Trigger the event the callback is listening to
|
||||
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
|
||||
// has been removed
|
||||
spyA.should.be.callCount(3);
|
||||
spyA.should.be.calledThrice();
|
||||
|
||||
// Undo the second attachment
|
||||
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
|
||||
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
|
||||
spyA.should.be.callCount(3);
|
||||
spyA.should.be.calledThrice();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -60,23 +60,19 @@ function pushTests({ describe, it, firebase }) {
|
|||
|
||||
return ref.once('value')
|
||||
.then((snapshot) => {
|
||||
console.log('first once');
|
||||
originalListValue = snapshot.val();
|
||||
return ref.push(valueToAddToList);
|
||||
})
|
||||
.then((pushRef) => {
|
||||
console.log('after push');
|
||||
newItemRef = pushRef;
|
||||
return newItemRef.once('value');
|
||||
})
|
||||
.then((snapshot) => {
|
||||
console.log('second once');
|
||||
newItemValue = snapshot.val();
|
||||
newItemValue.should.eql(valueToAddToList);
|
||||
return firebase.native.database().ref('tests/types/array').once('value');
|
||||
})
|
||||
.then((snapshot) => {
|
||||
console.log('third once');
|
||||
newListValue = snapshot.val();
|
||||
const originalListAsObject = originalListValue.reduce((memo, value, index) => {
|
||||
memo[index] = value;
|
||||
|
|
Loading…
Reference in New Issue