[firestore][android][js] Add document `onSnapshot` support plus tests

This commit is contained in:
Chris Bianca 2017-10-02 13:11:38 +01:00
parent b4743ffa8b
commit cda1c27b5c
11 changed files with 597 additions and 81 deletions

View File

@ -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();
}
}

View File

@ -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);

View File

@ -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);
}

View File

@ -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;
}
/**

View File

@ -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());
}
}
});

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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 {

View File

@ -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 = {

View File

@ -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 = {

View File

@ -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()