[database][wip] on/off logic refactor - heavily still wip so things still to change

This commit is contained in:
Salakar 2017-08-14 11:05:49 +01:00
parent 0675aa076d
commit f1709970e9
13 changed files with 574 additions and 459 deletions

View File

@ -81,10 +81,9 @@ public class Utils {
* @param path
* @param dataSnapshot
* @param refId
* @param listenerId
* @return
*/
public static WritableMap snapshotToMap(String name, String path, DataSnapshot dataSnapshot, @Nullable String previousChildName, int refId, int listenerId) {
public static WritableMap snapshotToMap(String name, String path, DataSnapshot dataSnapshot, @Nullable String previousChildName, int refId) {
WritableMap snapshot = Arguments.createMap();
WritableMap eventMap = Arguments.createMap();
@ -111,7 +110,6 @@ public class Utils {
eventMap.putString("path", path);
eventMap.putMap("snapshot", snapshot);
eventMap.putString("eventName", name);
eventMap.putInt("listenerId", listenerId);
eventMap.putString("previousChildName", previousChildName);
return eventMap;

View File

@ -13,6 +13,7 @@ import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.google.firebase.FirebaseApp;
import com.google.firebase.database.ChildEventListener;
import com.google.firebase.database.MutableData;
import com.google.firebase.database.OnDisconnect;
import com.google.firebase.database.ServerValue;
@ -21,6 +22,7 @@ import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.ValueEventListener;
import java.util.HashMap;
import java.util.List;
@ -31,11 +33,39 @@ import io.invertase.firebase.Utils;
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 SparseArray<RNFirebaseTransactionHandler> transactionHandlers = new SparseArray<>();
RNFirebaseDatabase(ReactApplicationContext reactContext) {
super(reactContext);
childEventListeners = new HashMap<String, ChildEventListener>();
valueEventListeners = new HashMap<String, ValueEventListener>();
}
Boolean hasValueEventListener(String queryKey) {
return valueEventListeners.containsKey(queryKey);
}
Boolean hasChildEventListener(String queryKey) {
return childEventListeners.containsKey(queryKey);
}
void addValueEventListener(String queryKey, ValueEventListener listener) {
valueEventListeners.put(queryKey, listener);
}
void addChildEventListener(String queryKey, ChildEventListener listener) {
childEventListeners.put(queryKey, listener);
}
void removeValueEventListener(String queryKey) {
valueEventListeners.remove(queryKey);
}
void removeChildEventListener(String queryKey) {
childEventListeners.remove(queryKey);
}
/*
@ -394,17 +424,20 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule {
/**
* Subscribe to real time events for the specified database path + modifiers
*
* @param appName
* @param refId
* @param path
* @param modifiers
* @param eventName
* @param promise
* @param appName String
* @param args ReadableMap
*/
@ReactMethod
public void on(String appName, int refId, String path, ReadableArray modifiers, String eventName, Promise promise) {
getInternalReferenceForApp(appName, refId, path, modifiers, true).on(eventName);
public void on(String appName, ReadableMap args) {
RNFirebaseDatabaseReference ref = getInternalReferenceForApp(
appName,
args.getInt("id"),
args.getString("path"),
args.getArray("modifiers"),
true
);
ref.on(this, args.getString("eventType"), args.getString("eventQueryKey"));
}
/*

View File

@ -1,5 +1,6 @@
package io.invertase.firebase.database;
import java.util.HashMap;
import java.util.Map;
import java.util.List;
@ -9,6 +10,7 @@ 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.WritableMap;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReadableArray;
@ -30,8 +32,7 @@ public class RNFirebaseDatabaseReference {
private String path;
private String appName;
private ReactContext reactContext;
private SparseArray<ChildEventListener> childEventListeners;
private SparseArray<ValueEventListener> valueEventListeners;
Query getQuery() {
return query;
@ -49,8 +50,6 @@ public class RNFirebaseDatabaseReference {
appName = app;
path = refPath;
reactContext = context;
childEventListeners = new SparseArray<ChildEventListener>();
valueEventListeners = new SparseArray<ValueEventListener>();
query = buildDatabaseQueryAtPathAndModifiers(path, modifiersArray);
}
@ -63,7 +62,7 @@ public class RNFirebaseDatabaseReference {
ValueEventListener onceValueEventListener = new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
WritableMap data = Utils.snapshotToMap("value", path, dataSnapshot, null, refId, 0);
WritableMap data = Utils.snapshotToMap("value", path, dataSnapshot, null, refId);
promise.resolve(data);
}
@ -90,7 +89,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, 0);
WritableMap data = Utils.snapshotToMap("child_added", path, dataSnapshot, previousChildName, refId);
promise.resolve(data);
}
}
@ -99,7 +98,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, 0);
WritableMap data = Utils.snapshotToMap("child_changed", path, dataSnapshot, previousChildName, refId);
promise.resolve(data);
}
}
@ -108,7 +107,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, 0);
WritableMap data = Utils.snapshotToMap("child_removed", path, dataSnapshot, null, refId);
promise.resolve(data);
}
}
@ -117,7 +116,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, 0);
WritableMap data = Utils.snapshotToMap("child_moved", path, dataSnapshot, previousChildName, refId);
promise.resolve(data);
}
}
@ -135,14 +134,20 @@ public class RNFirebaseDatabaseReference {
/**
* Handles a React Native JS 'on' request and initializes listeners.
*
* @param eventName
*/
void on(String eventName) {
void on(RNFirebaseDatabase database, String eventName, String queryKey) {
if (eventName.equals("value")) {
addValueEventListener(queryKey, database);
} else {
addChildEventListener(queryKey, database, eventName);
}
}
/**
* Handles a React Native JS 'once' request.
*
* @param eventName
* @param promise
*/
@ -155,136 +160,153 @@ public class RNFirebaseDatabaseReference {
}
// todo cleanup all below
void addChildEventListener(final int listenerId, final String eventName) {
if (childEventListeners.get(listenerId) != null) {
/**
* @param queryKey
* @param eventName
*/
private void addChildEventListener(final String queryKey, final RNFirebaseDatabase database, final String eventName) {
if (!database.hasChildEventListener(queryKey)) {
ChildEventListener childEventListener = new ChildEventListener() {
@Override
public void onChildAdded(DataSnapshot dataSnapshot, String previousChildName) {
if ("child_added".equals(eventName)) {
handleDatabaseEvent("child_added", listenerId, dataSnapshot, previousChildName);
handleDatabaseEvent("child_added", queryKey, dataSnapshot, previousChildName);
}
}
@Override
public void onChildChanged(DataSnapshot dataSnapshot, String previousChildName) {
if ("child_changed".equals(eventName)) {
handleDatabaseEvent("child_changed", listenerId, dataSnapshot, previousChildName);
handleDatabaseEvent("child_changed", queryKey, dataSnapshot, previousChildName);
}
}
@Override
public void onChildRemoved(DataSnapshot dataSnapshot) {
if ("child_removed".equals(eventName)) {
handleDatabaseEvent("child_removed", listenerId, dataSnapshot, null);
handleDatabaseEvent("child_removed", queryKey, dataSnapshot, null);
}
}
@Override
public void onChildMoved(DataSnapshot dataSnapshot, String previousChildName) {
if ("child_moved".equals(eventName)) {
handleDatabaseEvent("child_moved", listenerId, dataSnapshot, previousChildName);
handleDatabaseEvent("child_moved", queryKey, dataSnapshot, previousChildName);
}
}
@Override
public void onCancelled(DatabaseError error) {
removeChildEventListener(listenerId);
handleDatabaseError(listenerId, error);
query.removeEventListener(this);
database.removeChildEventListener(queryKey);
handleDatabaseError(queryKey, error);
}
};
childEventListeners.put(listenerId, childEventListener);
database.addChildEventListener(queryKey, childEventListener);
query.addChildEventListener(childEventListener);
Log.d(TAG, "Added ChildEventListener for refId: " + refId + " listenerId: " + listenerId);
} else {
Log.d(TAG, "ChildEventListener for refId: " + refId + " listenerId: " + listenerId + " already exists");
}
}
void addValueEventListener(final int listenerId) {
if (valueEventListeners.get(listenerId) != null) {
/**
* @param queryKey
*/
private void addValueEventListener(final String queryKey, final RNFirebaseDatabase database) {
if (!database.hasValueEventListener(queryKey)) {
ValueEventListener valueEventListener = new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
handleDatabaseEvent("value", listenerId, dataSnapshot, null);
handleDatabaseEvent("value", queryKey, dataSnapshot, null);
}
@Override
public void onCancelled(DatabaseError error) {
removeValueEventListener(listenerId);
handleDatabaseError(listenerId, error);
query.removeEventListener(this);
database.removeValueEventListener(queryKey);
handleDatabaseError(queryKey, error);
}
};
valueEventListeners.put(listenerId, valueEventListener);
database.addValueEventListener(queryKey, valueEventListener);
query.addValueEventListener(valueEventListener);
Log.d(TAG, "Added ValueEventListener for refId: " + refId + " listenerId: " + listenerId);
} else {
Log.d(TAG, "ValueEventListener for refId: " + refId + " listenerId: " + listenerId + " already exists");
}
}
void removeEventListener(int listenerId, String eventName) {
if ("value".equals(eventName)) {
this.removeValueEventListener(listenerId);
} else {
this.removeChildEventListener(listenerId);
}
}
boolean hasListeners() {
return childEventListeners.size() > 0 || valueEventListeners.size() > 0;
}
public void cleanup() {
Log.d(TAG, "cleaning up database reference " + this);
this.removeChildEventListener(null);
this.removeValueEventListener(null);
}
private void removeChildEventListener(Integer listenerId) {
ChildEventListener listener = childEventListeners.get(listenerId);
if (listener != null) {
query.removeEventListener(listener);
childEventListeners.delete(listenerId);
}
}
private void removeValueEventListener(Integer listenerId) {
ValueEventListener listener = valueEventListeners.get(listenerId);
if (listener != null) {
query.removeEventListener(listener);
valueEventListeners.delete(listenerId);
}
}
private void handleDatabaseEvent(final String name, final Integer listenerId, final DataSnapshot dataSnapshot, @Nullable String previousChildName) {
WritableMap data = Utils.snapshotToMap(name, path, dataSnapshot, previousChildName, refId, listenerId);
/**
* @param name
* @param dataSnapshot
* @param previousChildName
*/
private void handleDatabaseEvent(final String name, String queryKey, final DataSnapshot dataSnapshot, @Nullable String previousChildName) {
WritableMap evt = Arguments.createMap();
evt.putString("eventName", name);
evt.putMap("body", data);
WritableMap data = Utils.snapshotToMap(name, path, dataSnapshot, previousChildName, refId);
Utils.sendEvent(reactContext, "database_event", evt);
evt.putMap("body", data);
evt.putInt("refId", refId);
evt.putString("eventName", name);
evt.putString("appName", appName);
evt.putString("queryKey", queryKey);
Utils.sendEvent(reactContext, "database_on_event", evt);
}
private void handleDatabaseError(final Integer listenerId, final DatabaseError error) {
/**
* Handles a database listener cancellation error.
*
* @param error
*/
private void handleDatabaseError(String queryKey, final DatabaseError error) {
WritableMap errMap = Arguments.createMap();
errMap.putInt("refId", refId);
if (listenerId != null) {
errMap.putInt("listenerId", listenerId);
}
errMap.putString("path", path);
errMap.putString("appName", appName);
errMap.putString("queryKey", queryKey);
errMap.putInt("code", error.getCode());
errMap.putString("details", error.getDetails());
errMap.putString("message", error.getMessage());
Utils.sendEvent(reactContext, "database_error", errMap);
Utils.sendEvent(reactContext, "database_cancel_event", errMap);
}
// todo cleanup below
// void removeEventListener(int listenerId, String eventName) {
// if ("value".equals(eventName)) {
// removeValueEventListener(listenerId);
// } else {
// removeChildEventListener(listenerId);
// }
// }
// boolean hasListeners() {
// return childEventListeners.size() > 0 || valueEventListeners.size() > 0;
// }
//
// public void cleanup() {
// Log.d(TAG, "cleaning up database reference " + this);
// this.removeChildEventListener(null);
// this.removeValueEventListener(null);
// }
// private void removeChildEventListener(Integer listenerId) {
// ChildEventListener listener = childEventListeners.get(listenerId);
// if (listener != null) {
// query.removeEventListener(listener);
// childEventListeners.remove(listenerId);
// }
// }
//
// private void removeValueEventListener(Integer listenerId) {
// ValueEventListener listener = valueEventListeners.get(listenerId);
// if (listener != null) {
// query.removeEventListener(listener);
// valueEventListeners.delete(listenerId);
// }
// }
private Query buildDatabaseQueryAtPathAndModifiers(String path, ReadableArray modifiers) {
FirebaseDatabase firebaseDatabase = RNFirebaseDatabase.getDatabaseForApp(appName);

View File

@ -8,8 +8,8 @@ static NSString *const AUTH_ERROR_EVENT = @"authError";
static NSString *const AUTH_ANONYMOUS_ERROR_EVENT = @"authAnonymousError";
// Database
static NSString *const DATABASE_DATA_EVENT = @"database_event";
static NSString *const DATABASE_ERROR_EVENT = @"database_error";
static NSString *const DATABASE_ON_EVENT = @"database_on_event";
static NSString *const DATABASE_CANCEL_EVENT = @"database_cancel_event";
static NSString *const DATABASE_TRANSACTION_EVENT = @"database_transaction_event";
static NSString *const DATABASE_VALUE_EVENT = @"value";

View File

@ -1,12 +1,15 @@
#import "RNFirebaseDatabase.h"
#if __has_include(<FirebaseDatabase/FIRDatabase.h>)
#import "RNFirebaseDatabaseReference.h"
#import "RNFirebaseEvents.h"
@implementation RNFirebaseDatabase
RCT_EXPORT_MODULE();
// TODO document methods
- (id)init {
self = [super init];
if (self != nil) {
@ -17,32 +20,42 @@ RCT_EXPORT_MODULE();
return self;
}
RCT_EXPORT_METHOD(goOnline:(NSString *) appName) {
RCT_EXPORT_METHOD(goOnline:
(NSString *) appName) {
[[RNFirebaseDatabase getDatabaseForApp:appName] goOnline];
}
RCT_EXPORT_METHOD(goOffline:(NSString *) appName) {
RCT_EXPORT_METHOD(goOffline:
(NSString *) appName) {
[[RNFirebaseDatabase getDatabaseForApp:appName] goOffline];
}
RCT_EXPORT_METHOD(setPersistence:(NSString *) appName
state:(BOOL) state) {
RCT_EXPORT_METHOD(setPersistence:
(NSString *) appName
state:
(BOOL) state) {
[RNFirebaseDatabase getDatabaseForApp:appName].persistenceEnabled = state;
}
RCT_EXPORT_METHOD(keepSynced:(NSString *) appName
refId:(nonnull NSNumber *) refId
path:(NSString *) path
modifiers:(NSArray *) modifiers
state:(BOOL) state) {
RCT_EXPORT_METHOD(keepSynced:
(NSString *) appName
refId:
(nonnull
NSNumber *) refId
path:(NSString *) path
modifiers:(NSArray *) modifiers
state:(BOOL) state) {
FIRDatabaseQuery * query = [self getInternalReferenceForApp:appName refId:refId path:path modifiers:modifiers keep:false].query;
FIRDatabaseQuery *query = [self getInternalReferenceForApp:appName refId:refId path:path modifiers:modifiers keep:false].query;
[query keepSynced:state];
}
RCT_EXPORT_METHOD(transactionTryCommit:(NSString *) appName
transactionId:(nonnull NSNumber *) transactionId
updates:(NSDictionary *) updates) {
RCT_EXPORT_METHOD(transactionTryCommit:
(NSString *) appName
transactionId:
(nonnull
NSNumber *) transactionId
updates:(NSDictionary *) updates) {
__block NSMutableDictionary *transactionState;
dispatch_sync(_transactionQueue, ^{
@ -69,10 +82,14 @@ RCT_EXPORT_METHOD(transactionTryCommit:(NSString *) appName
}
RCT_EXPORT_METHOD(transactionStart:(NSString *) appName
path:(NSString *) path
transactionId:(nonnull NSNumber *) transactionId
applyLocally:(BOOL) applyLocally) {
RCT_EXPORT_METHOD(transactionStart:
(NSString *) appName
path:
(NSString *) path
transactionId:
(nonnull
NSNumber *) transactionId
applyLocally:(BOOL) applyLocally) {
dispatch_async(_transactionQueue, ^{
NSMutableDictionary *transactionState = [NSMutableDictionary new];
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
@ -80,148 +97,194 @@ RCT_EXPORT_METHOD(transactionStart:(NSString *) appName
FIRDatabaseReference *ref = [self getReferenceForAppPath:appName path:path];
[ref runTransactionBlock:^FIRTransactionResult * _Nonnull(FIRMutableData *
_Nonnull currentData) {
dispatch_barrier_async(_transactionQueue, ^{
[_transactions setValue:transactionState forKey:transactionId];
NSDictionary *updateMap = [self createTransactionUpdateMap:appName transactionId:transactionId updatesData:currentData];
[self sendEventWithName:DATABASE_TRANSACTION_EVENT body:updateMap];
});
_Nonnull currentData) {
dispatch_barrier_async(_transactionQueue, ^{
[_transactions setValue:transactionState forKey:transactionId];
NSDictionary *updateMap = [self createTransactionUpdateMap:appName transactionId:transactionId updatesData:currentData];
[self sendEventWithName:DATABASE_TRANSACTION_EVENT body:updateMap];
});
// wait for the js event handler to call tryCommitTransaction
// this wait occurs on the Firebase Worker Queue
// so if the tryCommitTransaction fails to signal the semaphore
// no further blocks will be executed by Firebase until the timeout expires
dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC);
BOOL timedout = dispatch_semaphore_wait(sema, delayTime) != 0;
// wait for the js event handler to call tryCommitTransaction
// this wait occurs on the Firebase Worker Queue
// so if the tryCommitTransaction fails to signal the semaphore
// no further blocks will be executed by Firebase until the timeout expires
dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC);
BOOL timedout = dispatch_semaphore_wait(sema, delayTime) != 0;
BOOL abort = [transactionState valueForKey:@"abort"] || timedout;
id value = [transactionState valueForKey:@"value"];
BOOL abort = [transactionState valueForKey:@"abort"] || timedout;
id value = [transactionState valueForKey:@"value"];
dispatch_barrier_async(_transactionQueue, ^{
[_transactions removeObjectForKey:transactionId];
});
dispatch_barrier_async(_transactionQueue, ^{
[_transactions removeObjectForKey:transactionId];
});
if (abort) {
return [FIRTransactionResult abort];
} else {
currentData.value = value;
return [FIRTransactionResult successWithValue:currentData];
}
if (abort) {
return [FIRTransactionResult abort];
} else {
currentData.value = value;
return [FIRTransactionResult successWithValue:currentData];
}
andCompletionBlock:
^(NSError *_Nullable databaseError, BOOL committed, FIRDataSnapshot *_Nullable snapshot) {
NSDictionary *resultMap = [self createTransactionResultMap:appName transactionId:transactionId error:databaseError committed:committed snapshot:snapshot];
[self sendEventWithName:DATABASE_TRANSACTION_EVENT body:resultMap];
}
withLocalEvents:
applyLocally];
}
andCompletionBlock:
^(NSError *_Nullable databaseError, BOOL committed, FIRDataSnapshot *_Nullable snapshot) {
NSDictionary *resultMap = [self createTransactionResultMap:appName transactionId:transactionId error:databaseError committed:committed snapshot:snapshot];
[self sendEventWithName:DATABASE_TRANSACTION_EVENT body:resultMap];
}
withLocalEvents:
applyLocally];
});
}
RCT_EXPORT_METHOD(onDisconnectSet:(NSString *) appName
path:(NSString *) path
props:(NSDictionary *) props
resolver:(RCTPromiseResolveBlock) resolve
rejecter:(RCTPromiseRejectBlock) reject) {
RCT_EXPORT_METHOD(onDisconnectSet:
(NSString *) appName
path:
(NSString *) path
props:
(NSDictionary *) props
resolver:
(RCTPromiseResolveBlock) resolve
rejecter:
(RCTPromiseRejectBlock) reject) {
FIRDatabaseReference *ref = [self getReferenceForAppPath:appName path:path];
[ref onDisconnectSetValue:props[@"value"] withCompletionBlock:^(NSError *_Nullable error, FIRDatabaseReference *_Nonnull _ref) {
[RNFirebaseDatabase handlePromise:resolve rejecter:reject databaseError:error];
}];
}
RCT_EXPORT_METHOD(onDisconnectUpdate:(NSString *) appName
path:(NSString *) path
props:(NSDictionary *) props
resolver:(RCTPromiseResolveBlock) resolve
rejecter:(RCTPromiseRejectBlock) reject) {
RCT_EXPORT_METHOD(onDisconnectUpdate:
(NSString *) appName
path:
(NSString *) path
props:
(NSDictionary *) props
resolver:
(RCTPromiseResolveBlock) resolve
rejecter:
(RCTPromiseRejectBlock) reject) {
FIRDatabaseReference *ref = [self getReferenceForAppPath:appName path:path];
[ref onDisconnectUpdateChildValues:props withCompletionBlock:^(NSError *_Nullable error, FIRDatabaseReference *_Nonnull _ref) {
[RNFirebaseDatabase handlePromise:resolve rejecter:reject databaseError:error];
}];
}
RCT_EXPORT_METHOD(onDisconnectRemove:(NSString *) appName
path:(NSString *) path
resolver:(RCTPromiseResolveBlock) resolve
rejecter:(RCTPromiseRejectBlock) reject) {
RCT_EXPORT_METHOD(onDisconnectRemove:
(NSString *) appName
path:
(NSString *) path
resolver:
(RCTPromiseResolveBlock) resolve
rejecter:
(RCTPromiseRejectBlock) reject) {
FIRDatabaseReference *ref = [self getReferenceForAppPath:appName path:path];
[ref onDisconnectRemoveValueWithCompletionBlock:^(NSError *_Nullable error, FIRDatabaseReference *_Nonnull _ref) {
[RNFirebaseDatabase handlePromise:resolve rejecter:reject databaseError:error];
}];
}
RCT_EXPORT_METHOD(onDisconnectCancel:(NSString *) appName
path:(NSString *) path
resolver:(RCTPromiseResolveBlock) resolve
rejecter:(RCTPromiseRejectBlock) reject) {
RCT_EXPORT_METHOD(onDisconnectCancel:
(NSString *) appName
path:
(NSString *) path
resolver:
(RCTPromiseResolveBlock) resolve
rejecter:
(RCTPromiseRejectBlock) reject) {
FIRDatabaseReference *ref = [self getReferenceForAppPath:appName path:path];
[ref cancelDisconnectOperationsWithCompletionBlock:^(NSError *_Nullable error, FIRDatabaseReference *_Nonnull _ref) {
[RNFirebaseDatabase handlePromise:resolve rejecter:reject databaseError:error];
}];
}
RCT_EXPORT_METHOD(set:(NSString *) appName
path:(NSString *) path
props:(NSDictionary *) props
resolver:(RCTPromiseResolveBlock) resolve
rejecter:(RCTPromiseRejectBlock) reject) {
RCT_EXPORT_METHOD(set:
(NSString *) appName
path:
(NSString *) path
props:
(NSDictionary *) props
resolver:
(RCTPromiseResolveBlock) resolve
rejecter:
(RCTPromiseRejectBlock) reject) {
FIRDatabaseReference *ref = [self getReferenceForAppPath:appName path:path];
[ref setValue:[props valueForKey:@"value"] withCompletionBlock:^(NSError *_Nullable error, FIRDatabaseReference *_Nonnull _ref) {
[RNFirebaseDatabase handlePromise:resolve rejecter:reject databaseError:error];
}];
}
RCT_EXPORT_METHOD(setPriority:(NSString *) appName
path:(NSString *) path
priority:(NSDictionary *) priority
resolver:(RCTPromiseResolveBlock) resolve
rejecter:(RCTPromiseRejectBlock) reject) {
RCT_EXPORT_METHOD(setPriority:
(NSString *) appName
path:
(NSString *) path
priority:
(NSDictionary *) priority
resolver:
(RCTPromiseResolveBlock) resolve
rejecter:
(RCTPromiseRejectBlock) reject) {
FIRDatabaseReference *ref = [self getReferenceForAppPath:appName path:path];
[ref setPriority:[priority valueForKey:@"value"] withCompletionBlock:^(NSError * _Nullable error, FIRDatabaseReference * _Nonnull ref) {
[ref setPriority:[priority valueForKey:@"value"] withCompletionBlock:^(NSError *_Nullable error, FIRDatabaseReference *_Nonnull ref) {
[RNFirebaseDatabase handlePromise:resolve rejecter:reject databaseError:error];
}];
}
RCT_EXPORT_METHOD(setWithPriority:(NSString *) appName
path:(NSString *) path
data:(NSDictionary *) data
priority:(NSDictionary *) priority
resolver:(RCTPromiseResolveBlock) resolve
rejecter:(RCTPromiseRejectBlock) reject) {
RCT_EXPORT_METHOD(setWithPriority:
(NSString *) appName
path:
(NSString *) path
data:
(NSDictionary *) data
priority:
(NSDictionary *) priority
resolver:
(RCTPromiseResolveBlock) resolve
rejecter:
(RCTPromiseRejectBlock) reject) {
FIRDatabaseReference *ref = [self getReferenceForAppPath:appName path:path];
[ref setValue:[data valueForKey:@"value"] andPriority:[priority valueForKey:@"value"] withCompletionBlock:^(NSError * _Nullable error, FIRDatabaseReference * _Nonnull ref) {
[ref setValue:[data valueForKey:@"value"] andPriority:[priority valueForKey:@"value"] withCompletionBlock:^(NSError *_Nullable error, FIRDatabaseReference *_Nonnull ref) {
[RNFirebaseDatabase handlePromise:resolve rejecter:reject databaseError:error];
}];
}
RCT_EXPORT_METHOD(update:(NSString *) appName
path:(NSString *) path
props:(NSDictionary *) props
resolver:(RCTPromiseResolveBlock) resolve
rejecter:(RCTPromiseRejectBlock) reject) {
RCT_EXPORT_METHOD(update:
(NSString *) appName
path:
(NSString *) path
props:
(NSDictionary *) props
resolver:
(RCTPromiseResolveBlock) resolve
rejecter:
(RCTPromiseRejectBlock) reject) {
FIRDatabaseReference *ref = [self getReferenceForAppPath:appName path:path];
[ref updateChildValues:props withCompletionBlock:^(NSError *_Nullable error, FIRDatabaseReference *_Nonnull _ref) {
[RNFirebaseDatabase handlePromise:resolve rejecter:reject databaseError:error];
}];
}
RCT_EXPORT_METHOD(remove:(NSString *) appName
path:(NSString *) path
resolver:(RCTPromiseResolveBlock) resolve
rejecter:(RCTPromiseRejectBlock) reject) {
RCT_EXPORT_METHOD(remove:
(NSString *) appName
path:
(NSString *) path
resolver:
(RCTPromiseResolveBlock) resolve
rejecter:
(RCTPromiseRejectBlock) reject) {
FIRDatabaseReference *ref = [self getReferenceForAppPath:appName path:path];
[ref removeValueWithCompletionBlock:^(NSError *_Nullable error, FIRDatabaseReference *_Nonnull _ref) {
[RNFirebaseDatabase handlePromise:resolve rejecter:reject databaseError:error];
}];
}
RCT_EXPORT_METHOD(once:(NSString *) appName
refId:(nonnull NSNumber *) refId
path:(NSString *) path
modifiers:(NSArray *) modifiers
eventName:(NSString *) eventName
resolver:(RCTPromiseResolveBlock) resolve
rejecter:(RCTPromiseRejectBlock) reject) {
RCT_EXPORT_METHOD(once:
(NSString *) appName
refId:
(nonnull
NSNumber *) refId
path:(NSString *) path
modifiers:(NSArray *) modifiers
eventName:(NSString *) eventName
resolver:(RCTPromiseResolveBlock) resolve
rejecter:(RCTPromiseRejectBlock) reject) {
RNFirebaseDatabaseReference *ref = [self getInternalReferenceForApp:appName refId:refId path:path modifiers:modifiers keep:false];
[ref addSingleEventHandler:eventName resolver:resolve rejecter:reject];
}
@ -229,9 +292,7 @@ RCT_EXPORT_METHOD(once:(NSString *) appName
/*
* INTERNALS/UTILS
*/
+ (void) handlePromise:(RCTPromiseResolveBlock) resolve
rejecter:(RCTPromiseRejectBlock) reject
databaseError:(NSError *) databaseError {
+ (void)handlePromise:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject databaseError:(NSError *)databaseError {
if (databaseError != nil) {
NSDictionary *jsError = [RNFirebaseDatabase getJSError:databaseError];
reject([jsError valueForKey:@"code"], [jsError valueForKey:@"message"], databaseError);
@ -240,21 +301,16 @@ RCT_EXPORT_METHOD(once:(NSString *) appName
}
}
+ (FIRDatabase *) getDatabaseForApp:(NSString *) appName {
+ (FIRDatabase *)getDatabaseForApp:(NSString *)appName {
FIRApp *app = [FIRApp appNamed:appName];
return [FIRDatabase databaseForApp:app];
}
- (FIRDatabaseReference *) getReferenceForAppPath:(NSString *) appName
path:(NSString *) path {
- (FIRDatabaseReference *)getReferenceForAppPath:(NSString *)appName path:(NSString *)path {
return [[RNFirebaseDatabase getDatabaseForApp:appName] referenceWithPath:path];
}
- (RNFirebaseDatabaseReference *) getInternalReferenceForApp:(NSString *) appName
refId:(NSNumber *) refId
path:(NSString *) path
modifiers:(NSArray *) modifiers
keep:(BOOL) keep {
- (RNFirebaseDatabaseReference *)getInternalReferenceForApp:(NSString *)appName refId:(NSNumber *)refId path:(NSString *)path modifiers:(NSArray *)modifiers keep:(BOOL)keep {
RNFirebaseDatabaseReference *ref = _dbReferences[refId];
if (ref == nil) {
@ -268,18 +324,15 @@ RCT_EXPORT_METHOD(once:(NSString *) appName
}
// TODO: Move to error util for use in other modules
+ (NSString *) getMessageWithService:(NSString *) message
service:(NSString *) service
fullCode:(NSString *) fullCode {
+ (NSString *)getMessageWithService:(NSString *)message service:(NSString *)service fullCode:(NSString *)fullCode {
return [NSString stringWithFormat:@"%@: %@ (%@).", service, message, [fullCode lowercaseString]];
}
+ (NSString *) getCodeWithService:(NSString *) service
code:(NSString *) code {
+ (NSString *)getCodeWithService:(NSString *)service code:(NSString *)code {
return [NSString stringWithFormat:@"%@/%@", [service uppercaseString], [code uppercaseString]];
}
+ (NSDictionary *) getJSError:(NSError *) nativeError {
+ (NSDictionary *)getJSError:(NSError *)nativeError {
NSMutableDictionary *errorMap = [[NSMutableDictionary alloc] init];
[errorMap setValue:@(nativeError.code) forKey:@"nativeErrorCode"];
[errorMap setValue:[nativeError localizedDescription] forKey:@"nativeErrorMessage"];
@ -303,7 +356,7 @@ RCT_EXPORT_METHOD(once:(NSString *) appName
message = [RNFirebaseDatabase getMessageWithService:@"The write was canceled by the user." service:service fullCode:code];
break;
// TODO: Missing iOS equivalent codes
// TODO: Missing iOS equivalent codes
case -1:
code = [RNFirebaseDatabase getCodeWithService:service code:@"data-stale"];
message = [RNFirebaseDatabase getMessageWithService:@"The transaction needs to be run again with current data." service:service fullCode:code];
@ -352,9 +405,7 @@ RCT_EXPORT_METHOD(once:(NSString *) appName
return errorMap;
}
- (NSDictionary *) createTransactionUpdateMap:(NSString *) appName
transactionId:(NSNumber *) transactionId
updatesData:(FIRMutableData *) updatesData {
- (NSDictionary *)createTransactionUpdateMap:(NSString *)appName transactionId:(NSNumber *)transactionId updatesData:(FIRMutableData *)updatesData {
NSMutableDictionary *updatesMap = [[NSMutableDictionary alloc] init];
[updatesMap setValue:transactionId forKey:@"id"];
[updatesMap setValue:@"update" forKey:@"type"];
@ -364,11 +415,7 @@ RCT_EXPORT_METHOD(once:(NSString *) appName
return updatesMap;
}
- (NSDictionary *) createTransactionResultMap:(NSString *) appName
transactionId:(NSNumber *) transactionId
error:(NSError *) error
committed:(BOOL) committed
snapshot:(FIRDataSnapshot *) snapshot {
- (NSDictionary *)createTransactionResultMap:(NSString *)appName transactionId:(NSNumber *)transactionId error:(NSError *)error committed:(BOOL)committed snapshot:(FIRDataSnapshot *)snapshot {
NSMutableDictionary *resultMap = [[NSMutableDictionary alloc] init];
[resultMap setValue:transactionId forKey:@"id"];
[resultMap setValue:appName forKey:@"appName"];
@ -388,20 +435,15 @@ RCT_EXPORT_METHOD(once:(NSString *) appName
}
/* TODO
RCT_EXPORT_METHOD(on:
(nonnull
NSNumber *) refId
path:(NSString *) path
modifiers:(NSArray *) modifiers
listenerId:(nonnull NSNumber *) listenerId
name:(NSString *) eventName
callback:(RCTResponseSenderBlock) callback) {
RNFirebaseDatabaseReference *ref = [self getDBHandle:refId path:path modifiers:modifiers];
[ref addEventHandler:listenerId eventName:eventName];
callback(@[[NSNull null], @{@"status": @"success", @"refId": refId, @"handle": path}]);
RCT_EXPORT_METHOD(on:(NSString *) appName args:(NSDictionary *) args) {
// todo
// RNFirebaseDatabaseReference *ref = [self getInternalReferenceForApp:appName refId:refId path:path modifiers:modifiers keep:false];
// TODO query identifier
// [ref addEventHandler:listenerId eventName:eventName];
}
/* TODO
RCT_EXPORT_METHOD(off:
(nonnull
NSNumber *) refId
@ -432,9 +474,9 @@ RCT_EXPORT_METHOD(off:
return ref;
} */
// Not sure how to get away from this... yet
- (NSArray<NSString *> *)supportedEvents {
return @[DATABASE_DATA_EVENT, DATABASE_ERROR_EVENT, DATABASE_TRANSACTION_EVENT];
return @[DATABASE_ON_EVENT, DATABASE_CANCEL_EVENT, DATABASE_TRANSACTION_EVENT];
}
@end

View File

@ -28,7 +28,7 @@
if (!_listeners[listenerId]) {
id andPreviousSiblingKeyWithBlock = ^(FIRDataSnapshot *_Nonnull snapshot, NSString *_Nullable previousChildName) {
NSDictionary *props = [RNFirebaseDatabaseReference snapshotToDict:snapshot];
[self sendJSEvent:DATABASE_DATA_EVENT title:eventName props:@{@"eventName": eventName, @"refId": _refId, @"listenerId": listenerId, @"path": _path, @"snapshot": props, @"previousChildName": previousChildName != nil ? previousChildName : [NSNull null]}];
[self sendJSEvent:DATABASE_ON_EVENT title:eventName props:@{@"eventName": eventName, @"refId": _refId, @"listenerId": listenerId, @"path": _path, @"snapshot": props, @"previousChildName": previousChildName != nil ? previousChildName : [NSNull null]}];
};
id errorBlock = ^(NSError *_Nonnull error) {
NSLog(@"Error onDBEvent: %@", [error debugDescription]);
@ -130,13 +130,13 @@
}
- (NSDictionary *)getAndSendDatabaseError:(NSError *)error listenerId:(NSNumber *)listenerId {
NSDictionary *event = @{@"eventName": DATABASE_ERROR_EVENT, @"path": _path, @"refId": _refId, @"listenerId": listenerId, @"code": @([error code]), @"details": [error debugDescription], @"message": [error localizedDescription], @"description": [error description]};
NSDictionary *event = @{@"eventName": DATABASE_CANCEL_EVENT, @"path": _path, @"refId": _refId, @"listenerId": listenerId, @"code": @([error code]), @"details": [error debugDescription], @"message": [error localizedDescription], @"description": [error description]};
@try {
[_emitter sendEventWithName:DATABASE_ERROR_EVENT body:event];
[_emitter sendEventWithName:DATABASE_CANCEL_EVENT body:event];
} @catch (NSException *err) {
NSLog(@"An error occurred in getAndSendDatabaseError: %@", [err debugDescription]);
NSLog(@"Tried to send: %@ with %@", DATABASE_ERROR_EVENT, event);
NSLog(@"Tried to send: %@ with %@", DATABASE_CANCEL_EVENT, event);
}
return event;

View File

@ -5,6 +5,7 @@
import { NativeModules } from 'react-native';
import Reference from './reference';
import Snapshot from './snapshot';
import TransactionHandler from './transaction';
import ModuleBase from './../../utils/ModuleBase';
@ -14,6 +15,7 @@ import ModuleBase from './../../utils/ModuleBase';
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);
@ -21,9 +23,49 @@ export default class Database extends ModuleBase {
this._native.setPersistence(this._options.persistence);
}
// todo event & error listeners
// 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),
);
}
_handleOnEvent(event) {
console.log('>>>ON-event>>>', 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);
}
}
}
_handleCancelEvent(event) {
console.log('>>>CANCEL-event>>>', event);
}
/**

View File

@ -3,6 +3,9 @@
*/
import Reference from './reference.js';
import { objectToUniqueId } from './../../utils';
// todo doc methods
/**
* @class Query
@ -15,6 +18,12 @@ export default class Query {
this._reference = ref;
}
/**
*
* @param name
* @param key
* @return {Reference|*}
*/
orderBy(name: string, key?: string) {
this.modifiers.push({
type: 'orderBy',
@ -25,6 +34,12 @@ export default class Query {
return this._reference;
}
/**
*
* @param name
* @param limit
* @return {Reference|*}
*/
limit(name: string, limit: number) {
this.modifiers.push({
type: 'limit',
@ -35,6 +50,13 @@ export default class Query {
return this._reference;
}
/**
*
* @param name
* @param value
* @param key
* @return {Reference|*}
*/
filter(name: string, value: any, key?: string) {
this.modifiers.push({
type: 'filter',
@ -47,7 +69,27 @@ export default class Query {
return this._reference;
}
/**
*
* @return {[*]}
*/
getModifiers(): Array<DatabaseModifier> {
return [...this.modifiers];
}
/**
*
* @return {*}
*/
queryIdentifier() {
// convert query modifiers array into an object for generating a unique key
const object = {};
for (let i = 0, len = this.modifiers.length; i < len; i++) {
const { name, type, value } = this.modifiers[i];
object[`${type}-${name}`] = value;
}
return objectToUniqueId(object);
}
}

View File

@ -1,11 +1,21 @@
/**
* @flow
*/
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, isObject, tryJSONParse, tryJSONStringify, generatePushID } from './../../utils';
import {
promiseOrCallback,
isFunction,
isObject,
isString,
tryJSONParse,
tryJSONStringify,
generatePushID,
} from './../../utils';
// Unique Reference ID for native events
let refId = 1;
@ -61,6 +71,7 @@ export default class Reference extends ReferenceBase {
super(path, database);
this._promise = null;
this._refId = refId++;
this._listeners = 0;
this._refListeners = {};
this._database = database;
this._query = new Query(this, path, existingModifiers);
@ -491,6 +502,17 @@ export default class Reference extends ReferenceBase {
* INTERNALS
*/
/**
* Generate a unique key based on this refs path and query modifiers
* @return {string}
*/
makeQueryKey() {
return `$${this.path}$${this._query.queryIdentifier()}$${this._refId}$${this._listeners}`;
}
/**
* Return instance of db logger
*/
get log() {
return this._database.log;
}
@ -540,219 +562,103 @@ export default class Reference extends ReferenceBase {
}
// todo below methods need refactoring
// todo below methods need refactoring
// todo below methods need refactoring
// todo below methods need refactoring
// todo below methods need refactoring
// todo below methods need refactoring
// todo below methods need refactoring
// todo below methods need refactoring
/**
* iOS: Called once with the initial data at the specified location and then once each
* time the data changes. It won't trigger until the entire contents have been
* synchronized.
*
* Android: (@link https://github.com/invertase/react-native-firebase/issues/92)
* - Array & number values: Called once with the initial data at the specified
* location and then twice each time the value changes.
* - Other data types: Called once with the initial data at the specified location
* and once each time the data type changes.
*
* @callback onValueCallback
* @param {!DataSnapshot} dataSnapshot - Snapshot representing data at the location
* specified by the current ref. If location has no data, .val() will return null.
* @param eventType
* @param callback
* @param cancelCallback
* @return {*}
*/
/**
* Called once for each initial child at the specified location and then again
* every time a new child is added.
*
* @callback onChildAddedCallback
* @param {!DataSnapshot} dataSnapshot - Snapshot reflecting the data for the
* relevant child.
* @param {?ReferenceKey} previousChildKey - For ordering purposes, the key
* of the previous sibling child by sort order, or null if it is the first child.
*/
/**
* Called once every time a child is removed.
*
* A child will get removed when either:
* - remove() is explicitly called on a child or one of its ancestors
* - set(null) is called on that child or one of its ancestors
* - a child has all of its children removed
* - there is a query in effect which now filters out the child (because it's sort
* order changed or the max limit was hit)
*
* @callback onChildRemovedCallback
* @param {!DataSnapshot} dataSnapshot - Snapshot reflecting the old data for
* the child that was removed.
*/
/**
* Called when a child (or any of its descendants) changes.
*
* A single child_changed event may represent multiple changes to the child.
*
* @callback onChildChangedCallback
* @param {!DataSnapshot} dataSnapshot - Snapshot reflecting new child contents.
* @param {?ReferenceKey} previousChildKey - For ordering purposes, the key
* of the previous sibling child by sort order, or null if it is the first child.
*/
/**
* Called when a child's sort order changes, i.e. its position relative to its
* siblings changes.
*
* @callback onChildMovedCallback
* @param {!DataSnapshot} dataSnapshot - Snapshot reflecting the data of the moved
* child.
* @param {?ReferenceKey} previousChildKey - For ordering purposes, the key
* of the previous sibling child by sort order, or null if it is the first child.
*/
/**
* @typedef (onValueCallback|onChildAddedCallback|onChildRemovedCallback|onChildChangedCallback|onChildMovedCallback) ReferenceEventCallback
*/
/**
* Called if the event subscription is cancelled because the client does
* not have permission to read this data (or has lost the permission to do so).
*
* @callback onFailureCallback
* @param {Error} error - Object indicating why the failure occurred
*/
/**
* Binds callback handlers to when data changes at the current ref's location.
* The primary method of reading data from a Database.
*
* Callbacks can be unbound using {@link off}.
*
* Event Types:
*
* - value: {@link onValueCallback}.
* - child_added: {@link onChildAddedCallback}
* - child_removed: {@link onChildRemovedCallback}
* - child_changed: {@link onChildChangedCallback}
* - child_moved: {@link onChildMovedCallback}
*
* @param {ReferenceEventType} eventType - Type of event to attach a callback for.
* @param {ReferenceEventCallback} successCallback - Function that will be called
* when the event occurs with the new data.
* @param {onFailureCallback=} failureCallbackOrContext - Optional callback that is called
* if the event subscription fails. {@link onFailureCallback}
* @param {*=} context - Optional object to bind the callbacks to when calling them.
* @returns {ReferenceEventCallback} callback function, unmodified (unbound), for
* convenience if you want to pass an inline function to on() and store it later for
* removing using off().
*
* {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#on}
*/
on(eventType: string, successCallback: () => any, failureCallbackOrContext: () => any, context: any) {
if (!eventType) throw new Error('Error: Query on failed: Was called with 0 arguments. Expects at least 2');
if (!ReferenceEventTypes[eventType]) throw new Error('Query.on failed: First argument must be a valid event type: "value", "child_added", "child_removed", "child_changed", or "child_moved".');
if (!successCallback) throw new Error('Query.on failed: Was called with 1 argument. Expects at least 2.');
if (!isFunction(successCallback)) throw new Error('Query.on failed: Second argument must be a valid function.');
if (arguments.length > 2 && !failureCallbackOrContext) throw new Error('Query.on failed: third argument must either be a cancel callback or a context object.');
let _failureCallback;
let _context;
if (context) {
_context = context;
_failureCallback = failureCallbackOrContext;
} else if (isFunction(failureCallbackOrContext)) {
_failureCallback = failureCallbackOrContext;
} else {
_context = failureCallbackOrContext;
// 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, cancelCallback: () => any): Function {
if (!eventType) {
throw new Error('Query.on failed: Function called with 0 arguments. Expects at least 2.');
}
if (_failureCallback) {
_failureCallback = (error) => {
if (error.message.startsWith('FirebaseError: permission_denied')) {
// eslint-disable-next-line
error.message = `permission_denied at /${this.path}: Client doesn't have permission to access the desired data.`
}
failureCallbackOrContext(error);
};
}
// brb, helping someone
let _successCallback;
if (_context) {
_successCallback = successCallback.bind(_context);
} else {
_successCallback = successCallback;
if (!isString(eventType) || !ReferenceEventTypes[eventType]) {
throw new Error(`Query.on failed: First argument must be a valid string event type: "${Object.keys(ReferenceEventTypes).join(', ')}"`);
}
const listener = {
listenerId: Object.keys(this._refListeners).length + 1,
eventName: eventType,
successCallback: _successCallback,
failureCallback: _failureCallback,
};
if (!callback) {
throw new Error('Query.on failed: Function called with 1 argument. Expects at least 2.');
}
this._refListeners[listener.listenerId] = listener;
this._database.on(this, listener);
return successCallback;
if (!isFunction(callback)) {
throw new Error('Query.on failed: Second argument must be a valid function.');
}
if (cancelCallback && !isFunction(cancelCallback)) {
throw new Error('Query.on failed: Function called with 3 arguments, but third optional argument `cancelCallback` was not a function.');
}
const eventQueryKey = `${this.makeQueryKey()}$${eventType}`;
INTERNALS.SharedEventEmitter.addListener(eventQueryKey, callback);
if (isFunction(cancelCallback)) {
INTERNALS.SharedEventEmitter.once(`${this.makeQueryKey()}:cancelled`, cancelCallback);
}
// initialise the native listener if not already listening
this._database._native.on({
eventType,
eventQueryKey,
id: this._refId, // todo remove
path: this.path,
modifiers: this._query.getModifiers(),
});
if (!this._database._references[this._refId]) {
this._database._references[this._refId] = this;
}
this._listeners = this._listeners + 1;
return callback;
}
/**
* Detaches a callback attached with on().
*
* Calling off() on a parent listener will not automatically remove listeners
* registered on child nodes.
*
* If on() was called multiple times with the same eventType off() must be
* called multiple times to completely remove it.
*
* If a callback is not specified, all callbacks for the specified eventType
* will be removed. If no eventType or callback is specified, all callbacks
* for the Reference will be removed.
*
* If a context is specified, it too is used as a filter parameter: a callback
* will only be detached if, when it was attached with on(), the same event type,
* callback function and context were provided.
*
* If no callbacks matching the parameters provided are found, no callbacks are
* detached.
*
* @param {('value'|'child_added'|'child_changed'|'child_removed'|'child_moved')=} eventType - Type of event to detach callback for.
* @param {Function=} originalCallback - Original callback passed to on()
* TODO @param {*=} context - The context passed to on() when the callback was bound
*
* {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#off}
* @param eventType
* @param originalCallback
*/
off(eventType?: string = '', originalCallback?: () => any) {
// $FlowFixMe
const listeners: Array<DatabaseListener> = Object.values(this._refListeners);
let listenersToRemove;
if (eventType && originalCallback) {
listenersToRemove = listeners.filter((listener) => {
return listener.eventName === eventType && listener.successCallback === originalCallback;
});
// Only remove a single listener as per the web spec
if (listenersToRemove.length > 1) listenersToRemove = [listenersToRemove[0]];
} else if (eventType) {
listenersToRemove = listeners.filter((listener) => {
return listener.eventName === eventType;
});
} else if (originalCallback) {
listenersToRemove = listeners.filter((listener) => {
return listener.successCallback === originalCallback;
});
} else {
listenersToRemove = listeners;
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(', ')}"`);
}
if (originalCallback && !isFunction(originalCallback)) {
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}`;
if (originalCallback) {
INTERNALS.SharedEventEmitter.removeListener(eventQueryKey, originalCallback);
} else {
INTERNALS.SharedEventEmitter.removeAllListeners(eventQueryKey);
INTERNALS.SharedEventEmitter.removeAllListeners(`${this.makeQueryKey()}: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 {
}
// Remove the listeners from the reference to prevent memory leaks
listenersToRemove.forEach((listener) => {
delete this._refListeners[listener.listenerId];
});
return this._database.off(this._refId, listenersToRemove, Object.keys(this._refListeners).length);
}

View File

@ -25,9 +25,9 @@ const NATIVE_MODULE_EVENTS = {
'onAuthStateChanged',
],
Database: [
'database_on_event',
'database_cancel_event',
'database_transaction_event',
'database_event',
'database_error',
// 'database_server_offset', // TODO
],
};

View File

@ -17,9 +17,11 @@ const DEFAULT_CHUNK_SIZE = 50;
* @param joiner
* @returns {*}
*/
export function deepGet(object: Object,
path: string,
joiner?: string = '/'): any {
export function deepGet(
object: Object,
path: string,
joiner?: string = '/'
): any {
const keys = path.split(joiner);
let i = 0;
@ -43,9 +45,11 @@ export function deepGet(object: Object,
* @param joiner
* @returns {*}
*/
export function deepExists(object: Object,
path: string,
joiner?: string = '/'): boolean {
export function deepExists(
object: Object,
path: string,
joiner?: string = '/'
): boolean {
const keys = path.split(joiner);
let i = 0;
@ -133,10 +137,12 @@ export function noop(): void {
* @param callback
* @private
*/
function _delayChunk(collection: Array<*>,
chunkSize: number,
operation: Function,
callback: Function): void {
function _delayChunk(
collection: Array<*>,
chunkSize: number,
operation: Function,
callback: Function
): void {
const length = collection.length;
const iterations = Math.ceil(length / chunkSize);
@ -164,10 +170,12 @@ function _delayChunk(collection: Array<*>,
* @param iterator
* @param cb
*/
export function each(array: Array<*>,
chunkSize: number | Function,
iterator: Function,
cb?: Function): void {
export function each(
array: Array<*>,
chunkSize: number | Function,
iterator: Function,
cb?: Function
): void {
if (typeof chunkSize === 'function') {
cb = iterator;
iterator = chunkSize;
@ -202,10 +210,12 @@ export function typeOf(value: any): string {
* @param cb
* @returns {*}
*/
export function map(array: Array<*>,
chunkSize: number | Function,
iterator: Function,
cb?: Function): void {
export function map(
array: Array<*>,
chunkSize: number | Function,
iterator: Function,
cb?: Function
): void {
if (typeof chunkSize === 'function') {
cb = iterator;
iterator = chunkSize;
@ -307,6 +317,24 @@ export function nativeWithApp(appName, NativeModule) {
return native;
}
export function objectToUniqueId(object: Object): String {
if (!isObject(object) || object === null) return JSON.stringify(object);
const keys = Object.keys(object).sort();
let key = '{';
for (let i = 0; i < keys.length; i++) {
if (i !== 0) key += ',';
key += JSON.stringify(keys[i]);
key += ':';
key += objectToUniqueId(object[keys[i]]);
}
key += '}';
return key;
}
/**
* Return the existing promise if no callback provided or
* exec the promise and callback if optionalCallback is valid.

View File

@ -22,10 +22,11 @@ import priorityTests from './priorityTests';
import DatabaseContents from '../../support/DatabaseContents';
const testGroups = [
issueSpecificTests, factoryTests, keyTests, parentTests, childTests, rootTests,
pushTests, onTests, onValueTests, onChildAddedTests, offTests, onceTests, updateTests,
removeTests, setTests, transactionTests, queryTests, refTests, isEqualTests,
priorityTests,
// issueSpecificTests, factoryTests, keyTests, parentTests, childTests, rootTests,
// pushTests, onTests, onValueTests, onChildAddedTests, offTests, onceTests, updateTests,
// removeTests, setTests, transactionTests, queryTests, refTests, isEqualTests,
// priorityTests,
onValueTests, onChildAddedTests, offTests,
];
function registerTestSuite(testSuite) {

View File

@ -97,6 +97,7 @@ function onTests({ fdescribe, context, it, firebase, tryCatch }) {
await new Promise((resolve) => {
ref.on('value', (snapshot) => {
console.log('>>> SNAP',snapshot.val())
callback(snapshot.val());
resolve();
});