[firestore][android][js] Add document `onSnapshot` support plus tests
This commit is contained in:
parent
b4743ffa8b
commit
cda1c27b5c
|
@ -0,0 +1,27 @@
|
|||
package io.invertase.firebase;
|
||||
|
||||
public class ErrorUtils {
|
||||
/**
|
||||
* Wrap a message string with the specified service name e.g. 'Database'
|
||||
*
|
||||
* @param message
|
||||
* @param service
|
||||
* @param fullCode
|
||||
* @return
|
||||
*/
|
||||
public static String getMessageWithService(String message, String service, String fullCode) {
|
||||
// Service: Error message (service/code).
|
||||
return service + ": " + message + " (" + fullCode.toLowerCase() + ").";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a service error code string e.g. 'DATABASE/PERMISSION-DENIED'
|
||||
*
|
||||
* @param service
|
||||
* @param code
|
||||
* @return
|
||||
*/
|
||||
public static String getCodeWithService(String service, String code) {
|
||||
return service.toLowerCase() + "/" + code.toLowerCase();
|
||||
}
|
||||
}
|
|
@ -26,6 +26,7 @@ import java.util.HashMap;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import io.invertase.firebase.ErrorUtils;
|
||||
import io.invertase.firebase.Utils;
|
||||
|
||||
|
||||
|
@ -522,30 +523,6 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule {
|
|||
return existingRef;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a message string with the specified service name e.g. 'Database'
|
||||
*
|
||||
* @param message
|
||||
* @param service
|
||||
* @param fullCode
|
||||
* @return
|
||||
*/
|
||||
private static String getMessageWithService(String message, String service, String fullCode) {
|
||||
// Service: Error message (service/code).
|
||||
return service + ": " + message + " (" + fullCode.toLowerCase() + ").";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a service error code string e.g. 'DATABASE/PERMISSION-DENIED'
|
||||
*
|
||||
* @param service
|
||||
* @param code
|
||||
* @return
|
||||
*/
|
||||
private static String getCodeWithService(String service, String code) {
|
||||
return service.toLowerCase() + "/" + code.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert as firebase DatabaseError instance into a writable map
|
||||
* with the correct web-like error codes.
|
||||
|
@ -564,56 +541,56 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule {
|
|||
|
||||
switch (nativeError.getCode()) {
|
||||
case DatabaseError.DATA_STALE:
|
||||
code = getCodeWithService(service, "data-stale");
|
||||
message = getMessageWithService("The transaction needs to be run again with current data.", service, code);
|
||||
code = ErrorUtils.getCodeWithService(service, "data-stale");
|
||||
message = ErrorUtils.getMessageWithService("The transaction needs to be run again with current data.", service, code);
|
||||
break;
|
||||
case DatabaseError.OPERATION_FAILED:
|
||||
code = getCodeWithService(service, "failure");
|
||||
message = getMessageWithService("The server indicated that this operation failed.", service, code);
|
||||
code = ErrorUtils.getCodeWithService(service, "failure");
|
||||
message = ErrorUtils.getMessageWithService("The server indicated that this operation failed.", service, code);
|
||||
break;
|
||||
case DatabaseError.PERMISSION_DENIED:
|
||||
code = getCodeWithService(service, "permission-denied");
|
||||
message = getMessageWithService("Client doesn't have permission to access the desired data.", service, code);
|
||||
code = ErrorUtils.getCodeWithService(service, "permission-denied");
|
||||
message = ErrorUtils.getMessageWithService("Client doesn't have permission to access the desired data.", service, code);
|
||||
break;
|
||||
case DatabaseError.DISCONNECTED:
|
||||
code = getCodeWithService(service, "disconnected");
|
||||
message = getMessageWithService("The operation had to be aborted due to a network disconnect.", service, code);
|
||||
code = ErrorUtils.getCodeWithService(service, "disconnected");
|
||||
message = ErrorUtils.getMessageWithService("The operation had to be aborted due to a network disconnect.", service, code);
|
||||
break;
|
||||
case DatabaseError.EXPIRED_TOKEN:
|
||||
code = getCodeWithService(service, "expired-token");
|
||||
message = getMessageWithService("The supplied auth token has expired.", service, code);
|
||||
code = ErrorUtils.getCodeWithService(service, "expired-token");
|
||||
message = ErrorUtils.getMessageWithService("The supplied auth token has expired.", service, code);
|
||||
break;
|
||||
case DatabaseError.INVALID_TOKEN:
|
||||
code = getCodeWithService(service, "invalid-token");
|
||||
message = getMessageWithService("The supplied auth token was invalid.", service, code);
|
||||
code = ErrorUtils.getCodeWithService(service, "invalid-token");
|
||||
message = ErrorUtils.getMessageWithService("The supplied auth token was invalid.", service, code);
|
||||
break;
|
||||
case DatabaseError.MAX_RETRIES:
|
||||
code = getCodeWithService(service, "max-retries");
|
||||
message = getMessageWithService("The transaction had too many retries.", service, code);
|
||||
code = ErrorUtils.getCodeWithService(service, "max-retries");
|
||||
message = ErrorUtils.getMessageWithService("The transaction had too many retries.", service, code);
|
||||
break;
|
||||
case DatabaseError.OVERRIDDEN_BY_SET:
|
||||
code = getCodeWithService(service, "overridden-by-set");
|
||||
message = getMessageWithService("The transaction was overridden by a subsequent set.", service, code);
|
||||
code = ErrorUtils.getCodeWithService(service, "overridden-by-set");
|
||||
message = ErrorUtils.getMessageWithService("The transaction was overridden by a subsequent set.", service, code);
|
||||
break;
|
||||
case DatabaseError.UNAVAILABLE:
|
||||
code = getCodeWithService(service, "unavailable");
|
||||
message = getMessageWithService("The service is unavailable.", service, code);
|
||||
code = ErrorUtils.getCodeWithService(service, "unavailable");
|
||||
message = ErrorUtils.getMessageWithService("The service is unavailable.", service, code);
|
||||
break;
|
||||
case DatabaseError.USER_CODE_EXCEPTION:
|
||||
code = getCodeWithService(service, "user-code-exception");
|
||||
message = getMessageWithService("User code called from the Firebase Database runloop threw an exception.", service, code);
|
||||
code = ErrorUtils.getCodeWithService(service, "user-code-exception");
|
||||
message = ErrorUtils.getMessageWithService("User code called from the Firebase Database runloop threw an exception.", service, code);
|
||||
break;
|
||||
case DatabaseError.NETWORK_ERROR:
|
||||
code = getCodeWithService(service, "network-error");
|
||||
message = getMessageWithService("The operation could not be performed due to a network error.", service, code);
|
||||
code = ErrorUtils.getCodeWithService(service, "network-error");
|
||||
message = ErrorUtils.getMessageWithService("The operation could not be performed due to a network error.", service, code);
|
||||
break;
|
||||
case DatabaseError.WRITE_CANCELED:
|
||||
code = getCodeWithService(service, "write-cancelled");
|
||||
message = getMessageWithService("The write was canceled by the user.", service, code);
|
||||
code = ErrorUtils.getCodeWithService(service, "write-cancelled");
|
||||
message = ErrorUtils.getMessageWithService("The write was canceled by the user.", service, code);
|
||||
break;
|
||||
default:
|
||||
code = getCodeWithService(service, "unknown");
|
||||
message = getMessageWithService("An unknown error occurred.", service, code);
|
||||
code = ErrorUtils.getCodeWithService(service, "unknown");
|
||||
message = ErrorUtils.getMessageWithService("An unknown error occurred.", service, code);
|
||||
}
|
||||
|
||||
errorMap.putString("code", code);
|
||||
|
|
|
@ -29,8 +29,8 @@ class RNFirebaseDatabaseReference {
|
|||
private String appName;
|
||||
private ReactContext reactContext;
|
||||
private static final String TAG = "RNFirebaseDBReference";
|
||||
private HashMap<String, ChildEventListener> childEventListeners;
|
||||
private HashMap<String, ValueEventListener> valueEventListeners;
|
||||
private HashMap<String, ChildEventListener> childEventListeners = new HashMap<>();
|
||||
private HashMap<String, ValueEventListener> valueEventListeners = new HashMap<>();
|
||||
|
||||
/**
|
||||
* RNFirebase wrapper around FirebaseDatabaseReference,
|
||||
|
@ -47,8 +47,6 @@ class RNFirebaseDatabaseReference {
|
|||
query = null;
|
||||
appName = app;
|
||||
reactContext = context;
|
||||
childEventListeners = new HashMap<>();
|
||||
valueEventListeners = new HashMap<>();
|
||||
buildDatabaseQueryAtPathAndModifiers(refPath, modifiersArray);
|
||||
}
|
||||
|
||||
|
|
|
@ -11,12 +11,14 @@ import com.facebook.react.bridge.ReactMethod;
|
|||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.google.android.gms.tasks.OnCompleteListener;
|
||||
import com.google.android.gms.tasks.Task;
|
||||
import com.google.firebase.FirebaseApp;
|
||||
import com.google.firebase.firestore.DocumentReference;
|
||||
import com.google.firebase.firestore.FieldValue;
|
||||
import com.google.firebase.firestore.FirebaseFirestore;
|
||||
import com.google.firebase.firestore.FirebaseFirestoreException;
|
||||
import com.google.firebase.firestore.SetOptions;
|
||||
import com.google.firebase.firestore.WriteBatch;
|
||||
|
||||
|
@ -24,12 +26,14 @@ import java.util.HashMap;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import io.invertase.firebase.ErrorUtils;
|
||||
import io.invertase.firebase.Utils;
|
||||
|
||||
|
||||
public class RNFirebaseFirestore extends ReactContextBaseJavaModule {
|
||||
private static final String TAG = "RNFirebaseFirestore";
|
||||
// private HashMap<String, RNFirebaseDatabaseReference> references = new HashMap<>();
|
||||
private HashMap<String, RNFirebaseFirestoreCollectionReference> collectionReferences = new HashMap<>();
|
||||
private HashMap<String, RNFirebaseFirestoreDocumentReference> documentReferences = new HashMap<>();
|
||||
// private SparseArray<RNFirebaseTransactionHandler> transactionHandlers = new SparseArray<>();
|
||||
|
||||
RNFirebaseFirestore(ReactApplicationContext reactContext) {
|
||||
|
@ -94,7 +98,7 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule {
|
|||
promise.resolve(result);
|
||||
} else {
|
||||
Log.e(TAG, "set:onComplete:failure", task.getException());
|
||||
RNFirebaseFirestore.promiseRejectException(promise, task.getException());
|
||||
RNFirebaseFirestore.promiseRejectException(promise, (FirebaseFirestoreException)task.getException());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -129,6 +133,22 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule {
|
|||
// Not supported on Android out of the box
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void documentOffSnapshot(String appName, String path, int listenerId) {
|
||||
RNFirebaseFirestoreDocumentReference ref = getCachedDocumentForAppPath(appName, path);
|
||||
ref.offSnapshot(listenerId);
|
||||
|
||||
if (!ref.hasListeners()) {
|
||||
clearCachedDocumentForAppPath(appName, path);
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void documentOnSnapshot(String appName, String path, int listenerId) {
|
||||
RNFirebaseFirestoreDocumentReference ref = getCachedDocumentForAppPath(appName, path);
|
||||
ref.onSnapshot(listenerId);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void documentSet(String appName, String path, ReadableMap data, ReadableMap options, final Promise promise) {
|
||||
RNFirebaseFirestoreDocumentReference ref = getDocumentForAppPath(appName, path);
|
||||
|
@ -151,12 +171,11 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule {
|
|||
* @param exception Exception Exception normally from a task result.
|
||||
* @param promise Promise react native promise
|
||||
*/
|
||||
static void promiseRejectException(Promise promise, Exception exception) {
|
||||
// TODO
|
||||
// WritableMap jsError = getJSError(exception);
|
||||
static void promiseRejectException(Promise promise, FirebaseFirestoreException exception) {
|
||||
WritableMap jsError = getJSError(exception);
|
||||
promise.reject(
|
||||
"TODO", // jsError.getString("code"),
|
||||
exception.getMessage(), // jsError.getString("message"),
|
||||
jsError.getString("code"),
|
||||
jsError.getString("message"),
|
||||
exception
|
||||
);
|
||||
}
|
||||
|
@ -188,6 +207,35 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule {
|
|||
return new RNFirebaseFirestoreCollectionReference(appName, path, filters, orders, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a cached document reference for a specific app and path
|
||||
*
|
||||
* @param appName
|
||||
* @param path
|
||||
* @return
|
||||
*/
|
||||
private RNFirebaseFirestoreDocumentReference getCachedDocumentForAppPath(String appName, String path) {
|
||||
String key = appName + "/" + path;
|
||||
RNFirebaseFirestoreDocumentReference ref = documentReferences.get(key);
|
||||
if (ref == null) {
|
||||
ref = getDocumentForAppPath(appName, path);
|
||||
documentReferences.put(key, ref);
|
||||
}
|
||||
return ref;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear a cached document reference for a specific app and path
|
||||
*
|
||||
* @param appName
|
||||
* @param path
|
||||
* @return
|
||||
*/
|
||||
private void clearCachedDocumentForAppPath(String appName, String path) {
|
||||
String key = appName + "/" + path;
|
||||
documentReferences.remove(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a document reference for a specific app and path
|
||||
*
|
||||
|
@ -196,7 +244,99 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule {
|
|||
* @return
|
||||
*/
|
||||
private RNFirebaseFirestoreDocumentReference getDocumentForAppPath(String appName, String path) {
|
||||
return new RNFirebaseFirestoreDocumentReference(appName, path);
|
||||
return new RNFirebaseFirestoreDocumentReference(this.getReactApplicationContext(), appName, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert as firebase DatabaseError instance into a writable map
|
||||
* with the correct web-like error codes.
|
||||
*
|
||||
* @param nativeException
|
||||
* @return
|
||||
*/
|
||||
static WritableMap getJSError(FirebaseFirestoreException nativeException) {
|
||||
WritableMap errorMap = Arguments.createMap();
|
||||
errorMap.putInt("nativeErrorCode", nativeException.getCode().value());
|
||||
errorMap.putString("nativeErrorMessage", nativeException.getMessage());
|
||||
|
||||
String code;
|
||||
String message;
|
||||
String service = "Firestore";
|
||||
|
||||
// TODO: Proper error mappings
|
||||
switch (nativeException.getCode()) {
|
||||
case OK:
|
||||
code = ErrorUtils.getCodeWithService(service, "ok");
|
||||
message = ErrorUtils.getMessageWithService("Ok.", service, code);
|
||||
break;
|
||||
case CANCELLED:
|
||||
code = ErrorUtils.getCodeWithService(service, "cancelled");
|
||||
message = ErrorUtils.getMessageWithService("Cancelled.", service, code);
|
||||
break;
|
||||
case UNKNOWN:
|
||||
code = ErrorUtils.getCodeWithService(service, "unknown");
|
||||
message = ErrorUtils.getMessageWithService("An unknown error occurred.", service, code);
|
||||
break;
|
||||
case INVALID_ARGUMENT:
|
||||
code = ErrorUtils.getCodeWithService(service, "invalid-argument");
|
||||
message = ErrorUtils.getMessageWithService("Invalid argument.", service, code);
|
||||
break;
|
||||
case NOT_FOUND:
|
||||
code = ErrorUtils.getCodeWithService(service, "not-found");
|
||||
message = ErrorUtils.getMessageWithService("Not found.", service, code);
|
||||
break;
|
||||
case ALREADY_EXISTS:
|
||||
code = ErrorUtils.getCodeWithService(service, "already-exists");
|
||||
message = ErrorUtils.getMessageWithService("Already exists.", service, code);
|
||||
break;
|
||||
case PERMISSION_DENIED:
|
||||
code = ErrorUtils.getCodeWithService(service, "permission-denied");
|
||||
message = ErrorUtils.getMessageWithService("Permission denied.", service, code);
|
||||
break;
|
||||
case RESOURCE_EXHAUSTED:
|
||||
code = ErrorUtils.getCodeWithService(service, "resource-exhausted");
|
||||
message = ErrorUtils.getMessageWithService("Resource exhausted.", service, code);
|
||||
break;
|
||||
case FAILED_PRECONDITION:
|
||||
code = ErrorUtils.getCodeWithService(service, "failed-precondition");
|
||||
message = ErrorUtils.getMessageWithService("Failed precondition.", service, code);
|
||||
break;
|
||||
case ABORTED:
|
||||
code = ErrorUtils.getCodeWithService(service, "aborted");
|
||||
message = ErrorUtils.getMessageWithService("Aborted.", service, code);
|
||||
break;
|
||||
case OUT_OF_RANGE:
|
||||
code = ErrorUtils.getCodeWithService(service, "out-of-range");
|
||||
message = ErrorUtils.getMessageWithService("Out of range.", service, code);
|
||||
break;
|
||||
case UNIMPLEMENTED:
|
||||
code = ErrorUtils.getCodeWithService(service, "unimplemented");
|
||||
message = ErrorUtils.getMessageWithService("Unimplemented.", service, code);
|
||||
break;
|
||||
case INTERNAL:
|
||||
code = ErrorUtils.getCodeWithService(service, "internal");
|
||||
message = ErrorUtils.getMessageWithService("Internal.", service, code);
|
||||
break;
|
||||
case UNAVAILABLE:
|
||||
code = ErrorUtils.getCodeWithService(service, "unavailable");
|
||||
message = ErrorUtils.getMessageWithService("Unavailable.", service, code);
|
||||
break;
|
||||
case DATA_LOSS:
|
||||
code = ErrorUtils.getCodeWithService(service, "data-loss");
|
||||
message = ErrorUtils.getMessageWithService("Data loss.", service, code);
|
||||
break;
|
||||
case UNAUTHENTICATED:
|
||||
code = ErrorUtils.getCodeWithService(service, "unauthenticated");
|
||||
message = ErrorUtils.getMessageWithService("Unauthenticated.", service, code);
|
||||
break;
|
||||
default:
|
||||
code = ErrorUtils.getCodeWithService(service, "unknown");
|
||||
message = ErrorUtils.getMessageWithService("An unknown error occurred.", service, code);
|
||||
}
|
||||
|
||||
errorMap.putString("code", code);
|
||||
errorMap.putString("message", message);
|
||||
return errorMap;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -10,6 +10,7 @@ import com.facebook.react.bridge.ReadableMap;
|
|||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.google.android.gms.tasks.OnCompleteListener;
|
||||
import com.google.android.gms.tasks.Task;
|
||||
import com.google.firebase.firestore.FirebaseFirestoreException;
|
||||
import com.google.firebase.firestore.Query;
|
||||
import com.google.firebase.firestore.QuerySnapshot;
|
||||
|
||||
|
@ -47,7 +48,7 @@ public class RNFirebaseFirestoreCollectionReference {
|
|||
promise.resolve(data);
|
||||
} else {
|
||||
Log.e(TAG, "get:onComplete:failure", task.getException());
|
||||
RNFirebaseFirestore.promiseRejectException(promise, task.getException());
|
||||
RNFirebaseFirestore.promiseRejectException(promise, (FirebaseFirestoreException)task.getException());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -5,14 +5,19 @@ import android.util.Log;
|
|||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.google.android.gms.tasks.OnCompleteListener;
|
||||
import com.google.android.gms.tasks.Task;
|
||||
import com.google.firebase.firestore.DocumentReference;
|
||||
import com.google.firebase.firestore.DocumentSnapshot;
|
||||
import com.google.firebase.firestore.EventListener;
|
||||
import com.google.firebase.firestore.FirebaseFirestoreException;
|
||||
import com.google.firebase.firestore.ListenerRegistration;
|
||||
import com.google.firebase.firestore.SetOptions;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import io.invertase.firebase.Utils;
|
||||
|
@ -22,11 +27,14 @@ public class RNFirebaseFirestoreDocumentReference {
|
|||
private static final String TAG = "RNFBFSDocumentReference";
|
||||
private final String appName;
|
||||
private final String path;
|
||||
private ReactContext reactContext;
|
||||
private final DocumentReference ref;
|
||||
private Map<Integer, ListenerRegistration> documentSnapshotListeners = new HashMap<>();
|
||||
|
||||
RNFirebaseFirestoreDocumentReference(String appName, String path) {
|
||||
RNFirebaseFirestoreDocumentReference(ReactContext reactContext, String appName, String path) {
|
||||
this.appName = appName;
|
||||
this.path = path;
|
||||
this.reactContext = reactContext;
|
||||
this.ref = RNFirebaseFirestore.getFirestoreForApp(appName).document(path);
|
||||
}
|
||||
|
||||
|
@ -49,7 +57,7 @@ public class RNFirebaseFirestoreDocumentReference {
|
|||
promise.resolve(Arguments.createMap());
|
||||
} else {
|
||||
Log.e(TAG, "delete:onComplete:failure", task.getException());
|
||||
RNFirebaseFirestore.promiseRejectException(promise, task.getException());
|
||||
RNFirebaseFirestore.promiseRejectException(promise, (FirebaseFirestoreException)task.getException());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -65,12 +73,40 @@ public class RNFirebaseFirestoreDocumentReference {
|
|||
promise.resolve(data);
|
||||
} else {
|
||||
Log.e(TAG, "get:onComplete:failure", task.getException());
|
||||
RNFirebaseFirestore.promiseRejectException(promise, task.getException());
|
||||
RNFirebaseFirestore.promiseRejectException(promise, (FirebaseFirestoreException)task.getException());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void offSnapshot(final int listenerId) {
|
||||
ListenerRegistration listenerRegistration = documentSnapshotListeners.remove(listenerId);
|
||||
if (listenerRegistration != null) {
|
||||
listenerRegistration.remove();
|
||||
}
|
||||
}
|
||||
|
||||
public void onSnapshot(final int listenerId) {
|
||||
if (!documentSnapshotListeners.containsKey(listenerId)) {
|
||||
final EventListener<DocumentSnapshot> listener = new EventListener<DocumentSnapshot>() {
|
||||
@Override
|
||||
public void onEvent(DocumentSnapshot documentSnapshot, FirebaseFirestoreException exception) {
|
||||
if (exception == null) {
|
||||
handleDocumentSnapshotEvent(listenerId, documentSnapshot);
|
||||
} else {
|
||||
ListenerRegistration listenerRegistration = documentSnapshotListeners.remove(listenerId);
|
||||
if (listenerRegistration != null) {
|
||||
listenerRegistration.remove();
|
||||
}
|
||||
handleDocumentSnapshotError(listenerId, exception);
|
||||
}
|
||||
}
|
||||
};
|
||||
ListenerRegistration listenerRegistration = this.ref.addSnapshotListener(listener);
|
||||
documentSnapshotListeners.put(listenerId, listenerRegistration);
|
||||
}
|
||||
}
|
||||
|
||||
public void set(final ReadableMap data, final ReadableMap options, final Promise promise) {
|
||||
Map<String, Object> map = Utils.recursivelyDeconstructReadableMap(data);
|
||||
Task<Void> task;
|
||||
|
@ -90,7 +126,7 @@ public class RNFirebaseFirestoreDocumentReference {
|
|||
promise.resolve(Arguments.createMap());
|
||||
} else {
|
||||
Log.e(TAG, "set:onComplete:failure", task.getException());
|
||||
RNFirebaseFirestore.promiseRejectException(promise, task.getException());
|
||||
RNFirebaseFirestore.promiseRejectException(promise, (FirebaseFirestoreException)task.getException());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -108,9 +144,52 @@ public class RNFirebaseFirestoreDocumentReference {
|
|||
promise.resolve(Arguments.createMap());
|
||||
} else {
|
||||
Log.e(TAG, "update:onComplete:failure", task.getException());
|
||||
RNFirebaseFirestore.promiseRejectException(promise, task.getException());
|
||||
RNFirebaseFirestore.promiseRejectException(promise, (FirebaseFirestoreException)task.getException());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* INTERNALS/UTILS
|
||||
*/
|
||||
|
||||
public boolean hasListeners() {
|
||||
return !documentSnapshotListeners.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles documentSnapshot events.
|
||||
*
|
||||
* @param listenerId
|
||||
* @param documentSnapshot
|
||||
*/
|
||||
private void handleDocumentSnapshotEvent(int listenerId, DocumentSnapshot documentSnapshot) {
|
||||
WritableMap event = Arguments.createMap();
|
||||
WritableMap data = FirestoreSerialize.snapshotToWritableMap(documentSnapshot);
|
||||
|
||||
event.putString("appName", appName);
|
||||
event.putString("path", path);
|
||||
event.putInt("listenerId", listenerId);
|
||||
event.putMap("document", data);
|
||||
|
||||
Utils.sendEvent(reactContext, "firestore_document_sync_event", event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a documentSnapshot error event
|
||||
*
|
||||
* @param listenerId
|
||||
* @param exception
|
||||
*/
|
||||
private void handleDocumentSnapshotError(int listenerId, FirebaseFirestoreException exception) {
|
||||
WritableMap event = Arguments.createMap();
|
||||
|
||||
event.putString("appName", appName);
|
||||
event.putString("path", path);
|
||||
event.putInt("listenerId", listenerId);
|
||||
event.putMap("error", RNFirebaseFirestore.getJSError(exception));
|
||||
|
||||
Utils.sendEvent(reactContext, "firestore_document_sync_event", event);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,9 @@ export type WriteResult = {
|
|||
writeTime: string,
|
||||
}
|
||||
|
||||
// track all event registrations
|
||||
let listeners = 0;
|
||||
|
||||
/**
|
||||
* @class DocumentReference
|
||||
*/
|
||||
|
@ -89,8 +92,30 @@ export default class DocumentReference {
|
|||
throw new Error(INTERNALS.STRINGS.ERROR_UNSUPPORTED_CLASS_METHOD('DocumentReference', 'getCollections'));
|
||||
}
|
||||
|
||||
onSnapshot(onNext: () => any, onError?: () => any): () => void {
|
||||
// TODO
|
||||
onSnapshot(onNext: Function, onError?: Function): () => void {
|
||||
// TODO: Validation
|
||||
const listenerId = listeners++;
|
||||
|
||||
// Listen to snapshot events
|
||||
this._firestore.on(
|
||||
this._firestore._getAppEventName(`onDocumentSnapshot:${listenerId}`),
|
||||
onNext,
|
||||
);
|
||||
|
||||
// Listen for snapshot error events
|
||||
if (onError) {
|
||||
this._firestore.on(
|
||||
this._firestore._getAppEventName(`onDocumentSnapshotError:${listenerId}`),
|
||||
onError,
|
||||
);
|
||||
}
|
||||
|
||||
// Add the native listener
|
||||
this._firestore._native
|
||||
.documentOnSnapshot(this.path, listenerId);
|
||||
|
||||
// Return an unsubscribe method
|
||||
return this._offDocumentSnapshot.bind(this, listenerId, onNext);
|
||||
}
|
||||
|
||||
set(data: { [string]: any }, writeOptions?: WriteOptions): Promise<WriteResult> {
|
||||
|
@ -105,16 +130,11 @@ export default class DocumentReference {
|
|||
}
|
||||
|
||||
/**
|
||||
* INTERNALS
|
||||
* Remove auth change listener
|
||||
* @param listener
|
||||
*/
|
||||
|
||||
/**
|
||||
* Generate a string that uniquely identifies this DocumentReference
|
||||
*
|
||||
* @return {string}
|
||||
* @private
|
||||
*/
|
||||
_getDocumentKey() {
|
||||
return `$${this._firestore._appName}$/${this.path}`;
|
||||
_offDocumentSnapshot(listenerId: number, listener: Function) {
|
||||
this._firestore.log.info('Removing onDocumentSnapshot listener');
|
||||
this._firestore.removeListener(this._firestore._getAppEventName(`onDocumentSnapshot:${listenerId}`), listener);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -200,7 +200,7 @@ export default class Query {
|
|||
}
|
||||
|
||||
stream(): Stream<DocumentSnapshot> {
|
||||
|
||||
throw new Error(INTERNALS.STRINGS.ERROR_UNSUPPORTED_CLASS_METHOD('Query', 'stream'));
|
||||
}
|
||||
|
||||
where(fieldPath: string, opStr: Operator, value: any): Query {
|
||||
|
|
|
@ -16,6 +16,14 @@ import INTERNALS from './../../internals';
|
|||
const unquotedIdentifier_ = '(?:[A-Za-z_][A-Za-z_0-9]*)';
|
||||
const UNQUOTED_IDENTIFIER_REGEX = new RegExp(`^${unquotedIdentifier_}$`);
|
||||
|
||||
type DocumentSyncEvent = {
|
||||
appName: string,
|
||||
document?: DocumentSnapshot,
|
||||
error?: Object,
|
||||
listenerId: number,
|
||||
path: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* @class Firestore
|
||||
*/
|
||||
|
@ -28,6 +36,20 @@ export default class Firestore extends ModuleBase {
|
|||
constructor(firebaseApp: Object, options: Object = {}) {
|
||||
super(firebaseApp, options, true);
|
||||
this._referencePath = new Path([]);
|
||||
|
||||
this.addListener(
|
||||
// sub to internal native event - this fans out to
|
||||
// public event name: onCollectionSnapshot
|
||||
this._getAppEventName('firestore_collection_sync_event'),
|
||||
this._onCollectionSyncEvent.bind(this),
|
||||
);
|
||||
|
||||
this.addListener(
|
||||
// sub to internal native event - this fans out to
|
||||
// public event name: onDocumentSnapshot
|
||||
this._getAppEventName('firestore_document_sync_event'),
|
||||
this._onDocumentSyncEvent.bind(this),
|
||||
);
|
||||
}
|
||||
|
||||
batch(): WriteBatch {
|
||||
|
@ -107,6 +129,37 @@ export default class Firestore extends ModuleBase {
|
|||
|
||||
return fieldPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNALS
|
||||
*/
|
||||
|
||||
/**
|
||||
* Internal collection sync listener
|
||||
* @param event
|
||||
* @private
|
||||
*/
|
||||
_onCollectionSyncEvent(event: DocumentSyncEvent) {
|
||||
if (event.error) {
|
||||
this.emit(this._getAppEventName(`onCollectionSnapshotError:${event.listenerId}`, event.error));
|
||||
} else {
|
||||
this.emit(this._getAppEventName(`onCollectionSnapshot:${event.listenerId}`, event.document));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal document sync listener
|
||||
* @param event
|
||||
* @private
|
||||
*/
|
||||
_onDocumentSyncEvent(event: DocumentSyncEvent) {
|
||||
if (event.error) {
|
||||
this.emit(this._getAppEventName(`onDocumentSnapshotError:${event.listenerId}`), event.error);
|
||||
} else {
|
||||
const snapshot = new DocumentSnapshot(this, event.document);
|
||||
this.emit(this._getAppEventName(`onDocumentSnapshot:${event.listenerId}`), snapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const statics = {
|
||||
|
|
|
@ -29,6 +29,10 @@ const NATIVE_MODULE_EVENTS = {
|
|||
'database_transaction_event',
|
||||
// 'database_server_offset', // TODO
|
||||
],
|
||||
Firestore: [
|
||||
'firestore_collection_sync_event',
|
||||
'firestore_document_sync_event',
|
||||
],
|
||||
};
|
||||
|
||||
const DEFAULTS = {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import sinon from 'sinon';
|
||||
import 'should-sinon';
|
||||
import should from 'should';
|
||||
|
||||
function collectionReferenceTests({ describe, it, context, firebase }) {
|
||||
|
@ -26,6 +28,221 @@ function collectionReferenceTests({ describe, it, context, firebase }) {
|
|||
});
|
||||
});
|
||||
|
||||
context('onSnapshot()', () => {
|
||||
it('calls callback with the initial data and then when value changes', () => {
|
||||
return new Promise(async (resolve) => {
|
||||
const docRef = firebase.native.firestore().doc('document-tests/doc1');
|
||||
const currentDataValue = { name: 'doc1' };
|
||||
const newDataValue = { name: 'updated' };
|
||||
|
||||
const callback = sinon.spy();
|
||||
|
||||
// Test
|
||||
|
||||
let unsubscribe;
|
||||
await new Promise((resolve2) => {
|
||||
unsubscribe = docRef.onSnapshot((snapshot) => {
|
||||
callback(snapshot.data());
|
||||
resolve2();
|
||||
});
|
||||
});
|
||||
|
||||
callback.should.be.calledWith(currentDataValue);
|
||||
|
||||
await docRef.set(newDataValue);
|
||||
|
||||
await new Promise((resolve2) => {
|
||||
setTimeout(() => resolve2(), 5);
|
||||
});
|
||||
|
||||
// Assertions
|
||||
|
||||
callback.should.be.calledWith(newDataValue);
|
||||
callback.should.be.calledTwice();
|
||||
|
||||
// Tear down
|
||||
|
||||
unsubscribe();
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('onSnapshot()', () => {
|
||||
it('doesn\'t call callback when the ref is updated with the same value', async () => {
|
||||
return new Promise(async (resolve) => {
|
||||
const docRef = firebase.native.firestore().doc('document-tests/doc1');
|
||||
const currentDataValue = { name: 'doc1' };
|
||||
|
||||
const callback = sinon.spy();
|
||||
|
||||
// Test
|
||||
|
||||
let unsubscribe;
|
||||
await new Promise((resolve2) => {
|
||||
unsubscribe = docRef.onSnapshot((snapshot) => {
|
||||
callback(snapshot.data());
|
||||
resolve2();
|
||||
});
|
||||
});
|
||||
|
||||
callback.should.be.calledWith(currentDataValue);
|
||||
|
||||
await docRef.set(currentDataValue);
|
||||
|
||||
await new Promise((resolve2) => {
|
||||
setTimeout(() => resolve2(), 5);
|
||||
});
|
||||
|
||||
// Assertions
|
||||
|
||||
callback.should.be.calledOnce(); // Callback is not called again
|
||||
|
||||
// Tear down
|
||||
|
||||
unsubscribe();
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('onSnapshot()', () => {
|
||||
it('allows binding multiple callbacks to the same ref', () => {
|
||||
return new Promise(async (resolve) => {
|
||||
// Setup
|
||||
const docRef = firebase.native.firestore().doc('document-tests/doc1');
|
||||
const currentDataValue = { name: 'doc1' };
|
||||
const newDataValue = { name: 'updated' };
|
||||
|
||||
const callbackA = sinon.spy();
|
||||
const callbackB = sinon.spy();
|
||||
|
||||
// Test
|
||||
let unsubscribeA;
|
||||
let unsubscribeB;
|
||||
await new Promise((resolve2) => {
|
||||
unsubscribeA = docRef.onSnapshot((snapshot) => {
|
||||
callbackA(snapshot.data());
|
||||
resolve2();
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise((resolve2) => {
|
||||
unsubscribeB = docRef.onSnapshot((snapshot) => {
|
||||
callbackB(snapshot.data());
|
||||
resolve2();
|
||||
});
|
||||
});
|
||||
|
||||
callbackA.should.be.calledWith(currentDataValue);
|
||||
callbackA.should.be.calledOnce();
|
||||
|
||||
callbackB.should.be.calledWith(currentDataValue);
|
||||
callbackB.should.be.calledOnce();
|
||||
|
||||
await docRef.set(newDataValue);
|
||||
|
||||
await new Promise((resolve2) => {
|
||||
setTimeout(() => resolve2(), 5);
|
||||
});
|
||||
|
||||
callbackA.should.be.calledWith(newDataValue);
|
||||
callbackB.should.be.calledWith(newDataValue);
|
||||
|
||||
callbackA.should.be.calledTwice();
|
||||
callbackB.should.be.calledTwice();
|
||||
|
||||
// Tear down
|
||||
|
||||
unsubscribeA();
|
||||
unsubscribeB();
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('onSnapshot()', () => {
|
||||
it('listener stops listening when unsubscribed', () => {
|
||||
return new Promise(async (resolve) => {
|
||||
// Setup
|
||||
const docRef = firebase.native.firestore().doc('document-tests/doc1');
|
||||
const currentDataValue = { name: 'doc1' };
|
||||
const newDataValue = { name: 'updated' };
|
||||
|
||||
const callbackA = sinon.spy();
|
||||
const callbackB = sinon.spy();
|
||||
|
||||
// Test
|
||||
let unsubscribeA;
|
||||
let unsubscribeB;
|
||||
await new Promise((resolve2) => {
|
||||
unsubscribeA = docRef.onSnapshot((snapshot) => {
|
||||
callbackA(snapshot.data());
|
||||
resolve2();
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise((resolve2) => {
|
||||
unsubscribeB = docRef.onSnapshot((snapshot) => {
|
||||
callbackB(snapshot.data());
|
||||
resolve2();
|
||||
});
|
||||
});
|
||||
|
||||
callbackA.should.be.calledWith(currentDataValue);
|
||||
callbackA.should.be.calledOnce();
|
||||
|
||||
callbackB.should.be.calledWith(currentDataValue);
|
||||
callbackB.should.be.calledOnce();
|
||||
|
||||
await docRef.set(newDataValue);
|
||||
|
||||
await new Promise((resolve2) => {
|
||||
setTimeout(() => resolve2(), 5);
|
||||
});
|
||||
|
||||
callbackA.should.be.calledWith(newDataValue);
|
||||
callbackB.should.be.calledWith(newDataValue);
|
||||
|
||||
callbackA.should.be.calledTwice();
|
||||
callbackB.should.be.calledTwice();
|
||||
|
||||
// Unsubscribe A
|
||||
|
||||
unsubscribeA();
|
||||
|
||||
await docRef.set(currentDataValue);
|
||||
|
||||
await new Promise((resolve2) => {
|
||||
setTimeout(() => resolve2(), 5);
|
||||
});
|
||||
|
||||
callbackB.should.be.calledWith(currentDataValue);
|
||||
|
||||
callbackA.should.be.calledTwice();
|
||||
callbackB.should.be.calledThrice();
|
||||
|
||||
// Unsubscribe B
|
||||
|
||||
unsubscribeB();
|
||||
|
||||
await docRef.set(newDataValue);
|
||||
|
||||
await new Promise((resolve2) => {
|
||||
setTimeout(() => resolve2(), 5);
|
||||
});
|
||||
|
||||
callbackA.should.be.calledTwice();
|
||||
callbackB.should.be.calledThrice();
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('set()', () => {
|
||||
it('should create Document', () => {
|
||||
return firebase.native.firestore()
|
||||
|
|
Loading…
Reference in New Issue