[firestore] Add collection `onSnapshot` support

This commit is contained in:
Chris Bianca 2017-10-03 10:12:25 +01:00
parent d40f464f1c
commit 22f7d77f54
15 changed files with 559 additions and 107 deletions

View File

@ -32,8 +32,6 @@ import io.invertase.firebase.Utils;
public class RNFirebaseFirestore extends ReactContextBaseJavaModule {
private static final String TAG = "RNFirebaseFirestore";
private HashMap<String, RNFirebaseFirestoreCollectionReference> collectionReferences = new HashMap<>();
private HashMap<String, RNFirebaseFirestoreDocumentReference> documentReferences = new HashMap<>();
// private SparseArray<RNFirebaseTransactionHandler> transactionHandlers = new SparseArray<>();
RNFirebaseFirestore(ReactApplicationContext reactContext) {
@ -51,6 +49,20 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule {
ref.get(promise);
}
@ReactMethod
public void collectionOffSnapshot(String appName, String path, ReadableArray filters,
ReadableArray orders, ReadableMap options, String listenerId) {
RNFirebaseFirestoreCollectionReference.offSnapshot(listenerId);
}
@ReactMethod
public void collectionOnSnapshot(String appName, String path, ReadableArray filters,
ReadableArray orders, ReadableMap options, String listenerId) {
RNFirebaseFirestoreCollectionReference ref = getCollectionForAppPath(appName, path, filters, orders, options);
ref.onSnapshot(listenerId);
}
@ReactMethod
public void documentBatch(final String appName, final ReadableArray writes,
final ReadableMap commitOptions, final Promise promise) {
@ -134,18 +146,13 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule {
}
@ReactMethod
public void documentOffSnapshot(String appName, String path, int listenerId) {
RNFirebaseFirestoreDocumentReference ref = getCachedDocumentForAppPath(appName, path);
ref.offSnapshot(listenerId);
if (!ref.hasListeners()) {
clearCachedDocumentForAppPath(appName, path);
}
public void documentOffSnapshot(String appName, String path, String listenerId) {
RNFirebaseFirestoreDocumentReference.offSnapshot(listenerId);
}
@ReactMethod
public void documentOnSnapshot(String appName, String path, int listenerId) {
RNFirebaseFirestoreDocumentReference ref = getCachedDocumentForAppPath(appName, path);
public void documentOnSnapshot(String appName, String path, String listenerId) {
RNFirebaseFirestoreDocumentReference ref = getDocumentForAppPath(appName, path);
ref.onSnapshot(listenerId);
}
@ -204,36 +211,7 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule {
ReadableArray filters,
ReadableArray orders,
ReadableMap options) {
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);
return new RNFirebaseFirestoreCollectionReference(this.getReactApplicationContext(), appName, path, filters, orders, options);
}
/**

View File

@ -4,16 +4,21 @@ package io.invertase.firebase.firestore;
import android.support.annotation.NonNull;
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.ReadableArray;
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.EventListener;
import com.google.firebase.firestore.FirebaseFirestoreException;
import com.google.firebase.firestore.ListenerRegistration;
import com.google.firebase.firestore.Query;
import com.google.firebase.firestore.QuerySnapshot;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -21,21 +26,26 @@ import io.invertase.firebase.Utils;
public class RNFirebaseFirestoreCollectionReference {
private static final String TAG = "RNFSCollectionReference";
private static Map<String, ListenerRegistration> collectionSnapshotListeners = new HashMap<>();
private final String appName;
private final String path;
private final ReadableArray filters;
private final ReadableArray orders;
private final ReadableMap options;
private final Query query;
private ReactContext reactContext;
RNFirebaseFirestoreCollectionReference(String appName, String path, ReadableArray filters,
ReadableArray orders, ReadableMap options) {
RNFirebaseFirestoreCollectionReference(ReactContext reactContext, String appName, String path,
ReadableArray filters, ReadableArray orders,
ReadableMap options) {
this.appName = appName;
this.path = path;
this.filters = filters;
this.orders = orders;
this.options = options;
this.query = buildQuery();
this.reactContext = reactContext;
}
void get(final Promise promise) {
@ -54,6 +64,42 @@ public class RNFirebaseFirestoreCollectionReference {
});
}
public static void offSnapshot(final String listenerId) {
ListenerRegistration listenerRegistration = collectionSnapshotListeners.remove(listenerId);
if (listenerRegistration != null) {
listenerRegistration.remove();
}
}
public void onSnapshot(final String listenerId) {
if (!collectionSnapshotListeners.containsKey(listenerId)) {
final EventListener<QuerySnapshot> listener = new EventListener<QuerySnapshot>() {
@Override
public void onEvent(QuerySnapshot querySnapshot, FirebaseFirestoreException exception) {
if (exception == null) {
handleQuerySnapshotEvent(listenerId, querySnapshot);
} else {
ListenerRegistration listenerRegistration = collectionSnapshotListeners.remove(listenerId);
if (listenerRegistration != null) {
listenerRegistration.remove();
}
handleQuerySnapshotError(listenerId, exception);
}
}
};
ListenerRegistration listenerRegistration = this.query.addSnapshotListener(listener);
collectionSnapshotListeners.put(listenerId, listenerRegistration);
}
}
/*
* INTERNALS/UTILS
*/
boolean hasListeners() {
return !collectionSnapshotListeners.isEmpty();
}
private Query buildQuery() {
Query query = RNFirebaseFirestore.getFirestoreForApp(appName).collection(path);
query = applyFilters(query);
@ -134,4 +180,39 @@ public class RNFirebaseFirestoreCollectionReference {
}
return query;
}
/**
* Handles documentSnapshot events.
*
* @param listenerId
* @param querySnapshot
*/
private void handleQuerySnapshotEvent(String listenerId, QuerySnapshot querySnapshot) {
WritableMap event = Arguments.createMap();
WritableMap data = FirestoreSerialize.snapshotToWritableMap(querySnapshot);
event.putString("appName", appName);
event.putString("path", path);
event.putString("listenerId", listenerId);
event.putMap("querySnapshot", data);
Utils.sendEvent(reactContext, "firestore_collection_sync_event", event);
}
/**
* Handles a documentSnapshot error event
*
* @param listenerId
* @param exception
*/
private void handleQuerySnapshotError(String listenerId, FirebaseFirestoreException exception) {
WritableMap event = Arguments.createMap();
event.putString("appName", appName);
event.putString("path", path);
event.putString("listenerId", listenerId);
event.putMap("error", RNFirebaseFirestore.getJSError(exception));
Utils.sendEvent(reactContext, "firestore_collection_sync_event", event);
}
}

View File

@ -25,11 +25,12 @@ import io.invertase.firebase.Utils;
public class RNFirebaseFirestoreDocumentReference {
private static final String TAG = "RNFBFSDocumentReference";
private static Map<String, ListenerRegistration> documentSnapshotListeners = new HashMap<>();
private final String appName;
private final String path;
private ReactContext reactContext;
private final DocumentReference ref;
private Map<Integer, ListenerRegistration> documentSnapshotListeners = new HashMap<>();
RNFirebaseFirestoreDocumentReference(ReactContext reactContext, String appName, String path) {
this.appName = appName;
@ -79,14 +80,14 @@ public class RNFirebaseFirestoreDocumentReference {
});
}
public void offSnapshot(final int listenerId) {
public static void offSnapshot(final String listenerId) {
ListenerRegistration listenerRegistration = documentSnapshotListeners.remove(listenerId);
if (listenerRegistration != null) {
listenerRegistration.remove();
}
}
public void onSnapshot(final int listenerId) {
public void onSnapshot(final String listenerId) {
if (!documentSnapshotListeners.containsKey(listenerId)) {
final EventListener<DocumentSnapshot> listener = new EventListener<DocumentSnapshot>() {
@Override
@ -154,7 +155,7 @@ public class RNFirebaseFirestoreDocumentReference {
* INTERNALS/UTILS
*/
public boolean hasListeners() {
boolean hasListeners() {
return !documentSnapshotListeners.isEmpty();
}
@ -164,14 +165,14 @@ public class RNFirebaseFirestoreDocumentReference {
* @param listenerId
* @param documentSnapshot
*/
private void handleDocumentSnapshotEvent(int listenerId, DocumentSnapshot documentSnapshot) {
private void handleDocumentSnapshotEvent(String 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);
event.putString("listenerId", listenerId);
event.putMap("documentSnapshot", data);
Utils.sendEvent(reactContext, "firestore_document_sync_event", event);
}
@ -182,12 +183,12 @@ public class RNFirebaseFirestoreDocumentReference {
* @param listenerId
* @param exception
*/
private void handleDocumentSnapshotError(int listenerId, FirebaseFirestoreException exception) {
private void handleDocumentSnapshotError(String listenerId, FirebaseFirestoreException exception) {
WritableMap event = Arguments.createMap();
event.putString("appName", appName);
event.putString("path", path);
event.putInt("listenerId", listenerId);
event.putString("listenerId", listenerId);
event.putMap("error", RNFirebaseFirestore.getJSError(exception));
Utils.sendEvent(reactContext, "firestore_document_sync_event", event);

View File

@ -10,8 +10,6 @@
#import <React/RCTEventEmitter.h>
@interface RNFirebaseFirestore : RCTEventEmitter <RCTBridgeModule> {}
@property NSMutableDictionary *collectionReferences;
@property NSMutableDictionary *documentReferences;
+ (void)promiseRejectException:(RCTPromiseRejectBlock)reject error:(NSError *)error;

View File

@ -13,7 +13,7 @@ RCT_EXPORT_MODULE();
- (id)init {
self = [super init];
if (self != nil) {
_documentReferences = [[NSMutableDictionary alloc] init];
}
return self;
}
@ -28,6 +28,25 @@ RCT_EXPORT_METHOD(collectionGet:(NSString *) appName
[[self getCollectionForAppPath:appName path:path filters:filters orders:orders options:options] get:resolve rejecter:reject];
}
RCT_EXPORT_METHOD(collectionOffSnapshot:(NSString *) appName
path:(NSString *) path
filters:(NSArray *) filters
orders:(NSArray *) orders
options:(NSDictionary *) options
listenerId:(nonnull NSString *) listenerId) {
[RNFirebaseFirestoreCollectionReference offSnapshot:listenerId];
}
RCT_EXPORT_METHOD(collectionOnSnapshot:(NSString *) appName
path:(NSString *) path
filters:(NSArray *) filters
orders:(NSArray *) orders
options:(NSDictionary *) options
listenerId:(nonnull NSString *) listenerId) {
RNFirebaseFirestoreCollectionReference *ref = [self getCollectionForAppPath:appName path:path filters:filters orders:orders options:options];
[ref onSnapshot:listenerId];
}
RCT_EXPORT_METHOD(documentBatch:(NSString *) appName
writes:(NSArray *) writes
commitOptions:(NSDictionary *) commitOptions
@ -111,19 +130,14 @@ RCT_EXPORT_METHOD(documentGetAll:(NSString *) appName
RCT_EXPORT_METHOD(documentOffSnapshot:(NSString *) appName
path:(NSString *) path
listenerId:(nonnull NSNumber *) listenerId) {
RNFirebaseFirestoreDocumentReference *ref = [self getCachedDocumentForAppPath:appName path:path];
[ref offSnapshot:listenerId];
if (![ref hasListeners]) {
[self clearCachedDocumentForAppPath:appName path:path];
}
listenerId:(nonnull NSString *) listenerId) {
[RNFirebaseFirestoreDocumentReference offSnapshot:listenerId];
}
RCT_EXPORT_METHOD(documentOnSnapshot:(NSString *) appName
path:(NSString *) path
listenerId:(nonnull NSNumber *) listenerId) {
RNFirebaseFirestoreDocumentReference *ref = [self getCachedDocumentForAppPath:appName path:path];
listenerId:(nonnull NSString *) listenerId) {
RNFirebaseFirestoreDocumentReference *ref = [self getDocumentForAppPath:appName path:path];
[ref onSnapshot:listenerId];
}
@ -158,23 +172,7 @@ RCT_EXPORT_METHOD(documentUpdate:(NSString *) appName
}
- (RNFirebaseFirestoreCollectionReference *)getCollectionForAppPath:(NSString *)appName path:(NSString *)path filters:(NSArray *)filters orders:(NSArray *)orders options:(NSDictionary *)options {
return [[RNFirebaseFirestoreCollectionReference alloc] initWithPathAndModifiers:appName path:path filters:filters orders:orders options:options];
}
- (RNFirebaseFirestoreDocumentReference *)getCachedDocumentForAppPath:(NSString *)appName path:(NSString *)path {
NSString *key = [NSString stringWithFormat:@"%@/%@", appName, path];
RNFirebaseFirestoreDocumentReference *ref = _documentReferences[key];
if (ref == nil) {
ref = [self getDocumentForAppPath:appName path:path];
_documentReferences[key] = ref;
}
return ref;
}
- (void)clearCachedDocumentForAppPath:(NSString *)appName path:(NSString *)path {
NSString *key = [NSString stringWithFormat:@"%@/%@", appName, path];
[_documentReferences removeObjectForKey:key];
return [[RNFirebaseFirestoreCollectionReference alloc] initWithPathAndModifiers:self app:appName path:path filters:filters orders:orders options:options];
}
- (RNFirebaseFirestoreDocumentReference *)getDocumentForAppPath:(NSString *)appName path:(NSString *)path {

View File

@ -5,10 +5,13 @@
#if __has_include(<Firestore/FIRFirestore.h>)
#import <Firestore/Firestore.h>
#import <React/RCTEventEmitter.h>
#import "RNFirebaseEvents.h"
#import "RNFirebaseFirestore.h"
#import "RNFirebaseFirestoreDocumentReference.h"
@interface RNFirebaseFirestoreCollectionReference : NSObject
@property RCTEventEmitter *emitter;
@property NSString *app;
@property NSString *path;
@property NSArray *filters;
@ -16,8 +19,10 @@
@property NSDictionary *options;
@property FIRQuery *query;
- (id)initWithPathAndModifiers:(NSString *)app path:(NSString *)path filters:(NSArray *)filters orders:(NSArray *)orders options:(NSDictionary *)options;
- (id)initWithPathAndModifiers:(RCTEventEmitter *)emitter app:(NSString *)app path:(NSString *)path filters:(NSArray *)filters orders:(NSArray *)orders options:(NSDictionary *)options;
- (void)get:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject;
+ (void)offSnapshot:(NSString *)listenerId;
- (void)onSnapshot:(NSString *)listenerId;
+ (NSDictionary *)snapshotToDictionary:(FIRQuerySnapshot *)querySnapshot;
@end

View File

@ -4,13 +4,17 @@
#if __has_include(<Firestore/FIRFirestore.h>)
- (id)initWithPathAndModifiers:(NSString *) app
static NSMutableDictionary *_listeners;
- (id)initWithPathAndModifiers:(RCTEventEmitter *) emitter
app:(NSString *) app
path:(NSString *) path
filters:(NSArray *) filters
orders:(NSArray *) orders
options:(NSDictionary *) options {
self = [super init];
if (self) {
_emitter = emitter;
_app = app;
_path = path;
_filters = filters;
@ -18,6 +22,10 @@
_options = options;
_query = [self buildQuery];
}
// Initialise the static listeners object if required
if (!_listeners) {
_listeners = [[NSMutableDictionary alloc] init];
}
return self;
}
@ -33,6 +41,33 @@
}];
}
+ (void)offSnapshot:(NSString *) listenerId {
id<FIRListenerRegistration> listener = _listeners[listenerId];
if (listener) {
[_listeners removeObjectForKey:listenerId];
[listener remove];
}
}
- (void)onSnapshot:(NSString *) listenerId {
if (_listeners[listenerId] == nil) {
id listenerBlock = ^(FIRQuerySnapshot * _Nullable snapshot, NSError * _Nullable error) {
if (error) {
id<FIRListenerRegistration> listener = _listeners[listenerId];
if (listener) {
[_listeners removeObjectForKey:listenerId];
[listener remove];
}
[self handleQuerySnapshotError:listenerId error:error];
} else {
[self handleQuerySnapshotEvent:listenerId querySnapshot:snapshot];
}
};
id<FIRListenerRegistration> listener = [_query addSnapshotListener:listenerBlock];
_listeners[listenerId] = listener;
}
}
- (FIRQuery *)buildQuery {
FIRQuery *query = (FIRQuery*)[[RNFirebaseFirestore getFirestoreForApp:_app] collectionWithPath:_path];
query = [self applyFilters:query];
@ -96,6 +131,28 @@
return query;
}
- (void)handleQuerySnapshotError:(NSString *)listenerId
error:(NSError *)error {
NSMutableDictionary *event = [[NSMutableDictionary alloc] init];
[event setValue:_app forKey:@"appName"];
[event setValue:_path forKey:@"path"];
[event setValue:listenerId forKey:@"listenerId"];
[event setValue:[RNFirebaseFirestore getJSError:error] forKey:@"error"];
[_emitter sendEventWithName:FIRESTORE_COLLECTION_SYNC_EVENT body:event];
}
- (void)handleQuerySnapshotEvent:(NSString *)listenerId
querySnapshot:(FIRQuerySnapshot *)querySnapshot {
NSMutableDictionary *event = [[NSMutableDictionary alloc] init];
[event setValue:_app forKey:@"appName"];
[event setValue:_path forKey:@"path"];
[event setValue:listenerId forKey:@"listenerId"];
[event setValue:[RNFirebaseFirestoreCollectionReference snapshotToDictionary:querySnapshot] forKey:@"querySnapshot"];
[_emitter sendEventWithName:FIRESTORE_COLLECTION_SYNC_EVENT body:event];
}
+ (NSDictionary *)snapshotToDictionary:(FIRQuerySnapshot *)querySnapshot {
NSMutableDictionary *snapshot = [[NSMutableDictionary alloc] init];
[snapshot setValue:[self documentChangesToArray:querySnapshot.documentChanges] forKey:@"changes"];

View File

@ -15,15 +15,14 @@
@property NSString *app;
@property NSString *path;
@property FIRDocumentReference *ref;
@property NSMutableDictionary *listeners;
- (id)initWithPath:(RCTEventEmitter *)emitter app:(NSString *)app path:(NSString *)path;
- (void)collections:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject;
- (void)create:(NSDictionary *)data resolver:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject;
- (void)delete:(NSDictionary *)options resolver:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject;
- (void)get:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject;
- (void)offSnapshot:(NSNumber *)listenerId;
- (void)onSnapshot:(NSNumber *)listenerId;
+ (void)offSnapshot:(NSString *)listenerId;
- (void)onSnapshot:(NSString *)listenerId;
- (void)set:(NSDictionary *)data options:(NSDictionary *)options resolver:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject;
- (void)update:(NSDictionary *)data resolver:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject;
- (BOOL)hasListeners;

View File

@ -4,6 +4,8 @@
#if __has_include(<Firestore/FIRFirestore.h>)
static NSMutableDictionary *_listeners;
- (id)initWithPath:(RCTEventEmitter *)emitter
app:(NSString *) app
path:(NSString *) path {
@ -13,6 +15,9 @@
_app = app;
_path = path;
_ref = [[RNFirebaseFirestore getFirestoreForApp:_app] documentWithPath:_path];
}
// Initialise the static listeners object if required
if (!_listeners) {
_listeners = [[NSMutableDictionary alloc] init];
}
return self;
@ -49,7 +54,7 @@
}];
}
- (void)offSnapshot:(NSNumber *) listenerId {
+ (void)offSnapshot:(NSString *) listenerId {
id<FIRListenerRegistration> listener = _listeners[listenerId];
if (listener) {
[_listeners removeObjectForKey:listenerId];
@ -57,7 +62,7 @@
}
}
- (void)onSnapshot:(NSNumber *) listenerId {
- (void)onSnapshot:(NSString *) listenerId {
if (_listeners[listenerId] == nil) {
id listenerBlock = ^(FIRDocumentSnapshot * _Nullable snapshot, NSError * _Nullable error) {
if (error) {
@ -130,7 +135,7 @@
return snapshot;
}
- (void)handleDocumentSnapshotError:(NSNumber *)listenerId
- (void)handleDocumentSnapshotError:(NSString *)listenerId
error:(NSError *)error {
NSMutableDictionary *event = [[NSMutableDictionary alloc] init];
[event setValue:_app forKey:@"appName"];
@ -141,13 +146,13 @@
[_emitter sendEventWithName:FIRESTORE_DOCUMENT_SYNC_EVENT body:event];
}
- (void)handleDocumentSnapshotEvent:(NSNumber *)listenerId
- (void)handleDocumentSnapshotEvent:(NSString *)listenerId
documentSnapshot:(FIRDocumentSnapshot *)documentSnapshot {
NSMutableDictionary *event = [[NSMutableDictionary alloc] init];
[event setValue:_app forKey:@"appName"];
[event setValue:_path forKey:@"path"];
[event setValue:listenerId forKey:@"listenerId"];
[event setValue:[RNFirebaseFirestoreDocumentReference snapshotToDictionary:documentSnapshot] forKey:@"document"];
[event setValue:[RNFirebaseFirestoreDocumentReference snapshotToDictionary:documentSnapshot] forKey:@"documentSnapshot"];
[_emitter sendEventWithName:FIRESTORE_DOCUMENT_SYNC_EVENT body:event];
}

View File

@ -6,6 +6,7 @@ import CollectionReference from './CollectionReference';
import DocumentSnapshot from './DocumentSnapshot';
import Path from './Path';
import INTERNALS from './../../internals';
import { firestoreAutoId } from '../../utils';
export type DeleteOptions = {
lastUpdateTime?: string,
@ -19,9 +20,6 @@ export type WriteResult = {
writeTime: string,
}
// track all event registrations
let listeners = 0;
/**
* @class DocumentReference
*/
@ -94,12 +92,17 @@ export default class DocumentReference {
onSnapshot(onNext: Function, onError?: Function): () => void {
// TODO: Validation
const listenerId = listeners++;
const listenerId = firestoreAutoId();
const listener = (nativeDocumentSnapshot) => {
const documentSnapshot = new DocumentSnapshot(this, nativeDocumentSnapshot);
onNext(documentSnapshot);
};
// Listen to snapshot events
this._firestore.on(
this._firestore._getAppEventName(`onDocumentSnapshot:${listenerId}`),
onNext,
listener,
);
// Listen for snapshot error events
@ -115,7 +118,7 @@ export default class DocumentReference {
.documentOnSnapshot(this.path, listenerId);
// Return an unsubscribe method
return this._offDocumentSnapshot.bind(this, listenerId, onNext);
return this._offDocumentSnapshot.bind(this, listenerId, listener);
}
set(data: { [string]: any }, writeOptions?: WriteOptions): Promise<WriteResult> {
@ -130,11 +133,14 @@ export default class DocumentReference {
}
/**
* Remove auth change listener
* Remove document snapshot listener
* @param listener
*/
_offDocumentSnapshot(listenerId: number, listener: Function) {
this._firestore.log.info('Removing onDocumentSnapshot listener');
this._firestore.removeListener(this._firestore._getAppEventName(`onDocumentSnapshot:${listenerId}`), listener);
this._firestore.removeListener(this._firestore._getAppEventName(`onDocumentSnapshotError:${listenerId}`), listener);
this._firestore._native
.documentOffSnapshot(this.path, listenerId);
}
}

View File

@ -5,7 +5,8 @@
import DocumentSnapshot from './DocumentSnapshot';
import Path from './Path';
import QuerySnapshot from './QuerySnapshot';
import INTERNALS from './../../internals';
import INTERNALS from '../../internals';
import { firestoreAutoId } from '../../utils';
const DIRECTIONS = {
ASC: 'ASCENDING',
@ -51,6 +52,7 @@ export default class Query {
_fieldFilters: FieldFilter[];
_fieldOrders: FieldOrder[];
_firestore: Object;
_iid: number;
_queryOptions: QueryOptions;
_referencePath: Path;
@ -128,7 +130,40 @@ export default class Query {
}
onSnapshot(onNext: () => any, onError?: () => any): () => void {
// TODO: Validation
const listenerId = firestoreAutoId();
const listener = (nativeQuerySnapshot) => {
const querySnapshot = new QuerySnapshot(this._firestore, this, nativeQuerySnapshot);
onNext(querySnapshot);
};
// Listen to snapshot events
this._firestore.on(
this._firestore._getAppEventName(`onQuerySnapshot:${listenerId}`),
listener,
);
// Listen for snapshot error events
if (onError) {
this._firestore.on(
this._firestore._getAppEventName(`onQuerySnapshotError:${listenerId}`),
onError,
);
}
// Add the native listener
this._firestore._native
.collectionOnSnapshot(
this._referencePath.relativeName,
this._fieldFilters,
this._fieldOrders,
this._queryOptions,
listenerId
);
// Return an unsubscribe method
return this._offCollectionSnapshot.bind(this, listenerId, listener);
}
orderBy(fieldPath: string, directionStr?: Direction = 'asc'): Query {
@ -216,4 +251,22 @@ export default class Query {
return new Query(this.firestore, this._referencePath, combinedFilters,
this._fieldOrders, this._queryOptions);
}
/**
* Remove query snapshot listener
* @param listener
*/
_offCollectionSnapshot(listenerId: number, listener: Function) {
this._firestore.log.info('Removing onQuerySnapshot listener');
this._firestore.removeListener(this._firestore._getAppEventName(`onQuerySnapshot:${listenerId}`), listener);
this._firestore.removeListener(this._firestore._getAppEventName(`onQuerySnapshotError:${listenerId}`), listener);
this._firestore._native
.collectionOffSnapshot(
this._referencePath.relativeName,
this._fieldFilters,
this._fieldOrders,
this._queryOptions,
listenerId
);
}
}

View File

@ -59,7 +59,7 @@ export default class QuerySnapshot {
// TODO: Validation
// validate.isFunction('callback', callback);
for (const doc of this.docs) {
for (const doc of this._docs) {
callback(doc);
}
}

View File

@ -16,11 +16,19 @@ import INTERNALS from './../../internals';
const unquotedIdentifier_ = '(?:[A-Za-z_][A-Za-z_0-9]*)';
const UNQUOTED_IDENTIFIER_REGEX = new RegExp(`^${unquotedIdentifier_}$`);
type CollectionSyncEvent = {
appName: string,
querySnapshot?: QuerySnapshot,
error?: Object,
listenerId: string,
path: string,
}
type DocumentSyncEvent = {
appName: string,
document?: DocumentSnapshot,
documentSnapshot?: DocumentSnapshot,
error?: Object,
listenerId: number,
listenerId: string,
path: string,
}
@ -139,11 +147,11 @@ export default class Firestore extends ModuleBase {
* @param event
* @private
*/
_onCollectionSyncEvent(event: DocumentSyncEvent) {
_onCollectionSyncEvent(event: CollectionSyncEvent) {
if (event.error) {
this.emit(this._getAppEventName(`onCollectionSnapshotError:${event.listenerId}`, event.error));
this.emit(this._getAppEventName(`onQuerySnapshotError:${event.listenerId}`), event.error);
} else {
this.emit(this._getAppEventName(`onCollectionSnapshot:${event.listenerId}`, event.document));
this.emit(this._getAppEventName(`onQuerySnapshot:${event.listenerId}`), event.querySnapshot);
}
}
@ -156,8 +164,7 @@ export default class Firestore extends ModuleBase {
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);
this.emit(this._getAppEventName(`onDocumentSnapshot:${event.listenerId}`), event.documentSnapshot);
}
}
}

View File

@ -1,3 +1,5 @@
import sinon from 'sinon';
import 'should-sinon';
import should from 'should';
function collectionReferenceTests({ describe, it, context, firebase }) {
@ -50,6 +52,266 @@ function collectionReferenceTests({ describe, it, context, firebase }) {
});
});
context('onSnapshot()', () => {
it('calls callback with the initial data and then when document changes', () => {
return new Promise(async (resolve) => {
const collectionRef = firebase.native.firestore().collection('document-tests');
const currentDocValue = { name: 'doc1' };
const newDocValue = { name: 'updated' };
const callback = sinon.spy();
// Test
let unsubscribe;
await new Promise((resolve2) => {
unsubscribe = collectionRef.onSnapshot((snapshot) => {
snapshot.forEach(doc => callback(doc.data()));
resolve2();
});
});
callback.should.be.calledWith(currentDocValue);
const docRef = firebase.native.firestore().doc('document-tests/doc1');
await docRef.set(newDocValue);
await new Promise((resolve2) => {
setTimeout(() => resolve2(), 5);
});
// Assertions
callback.should.be.calledWith(newDocValue);
callback.should.be.calledTwice();
// Tear down
unsubscribe();
resolve();
});
});
});
context('onSnapshot()', () => {
it('calls callback with the initial data and then when document is added', () => {
return new Promise(async (resolve) => {
const collectionRef = firebase.native.firestore().collection('document-tests');
const currentDocValue = { name: 'doc1' };
const newDocValue = { name: 'updated' };
const callback = sinon.spy();
// Test
let unsubscribe;
await new Promise((resolve2) => {
unsubscribe = collectionRef.onSnapshot((snapshot) => {
snapshot.forEach(doc => callback(doc.data()));
resolve2();
});
});
callback.should.be.calledWith(currentDocValue);
const docRef = firebase.native.firestore().doc('document-tests/doc2');
await docRef.set(newDocValue);
await new Promise((resolve2) => {
setTimeout(() => resolve2(), 5);
});
// Assertions
callback.should.be.calledWith(currentDocValue);
callback.should.be.calledWith(newDocValue);
callback.should.be.calledThrice();
// 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 collectionRef = firebase.native.firestore().collection('document-tests');
const currentDocValue = { name: 'doc1' };
const callback = sinon.spy();
// Test
let unsubscribe;
await new Promise((resolve2) => {
unsubscribe = collectionRef.onSnapshot((snapshot) => {
snapshot.forEach(doc => callback(doc.data()));
resolve2();
});
});
callback.should.be.calledWith(currentDocValue);
const docRef = firebase.native.firestore().doc('document-tests/doc1');
await docRef.set(currentDocValue);
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 collectionRef = firebase.native.firestore().collection('document-tests');
const currentDocValue = { name: 'doc1' };
const newDocValue = { name: 'updated' };
const callbackA = sinon.spy();
const callbackB = sinon.spy();
// Test
let unsubscribeA;
let unsubscribeB;
await new Promise((resolve2) => {
unsubscribeA = collectionRef.onSnapshot((snapshot) => {
snapshot.forEach(doc => callbackA(doc.data()));
resolve2();
});
});
await new Promise((resolve2) => {
unsubscribeB = collectionRef.onSnapshot((snapshot) => {
snapshot.forEach(doc => callbackB(doc.data()));
resolve2();
});
});
callbackA.should.be.calledWith(currentDocValue);
callbackA.should.be.calledOnce();
callbackB.should.be.calledWith(currentDocValue);
callbackB.should.be.calledOnce();
const docRef = firebase.native.firestore().doc('document-tests/doc1');
await docRef.set(newDocValue);
await new Promise((resolve2) => {
setTimeout(() => resolve2(), 5);
});
callbackA.should.be.calledWith(newDocValue);
callbackB.should.be.calledWith(newDocValue);
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 collectionRef = firebase.native.firestore().collection('document-tests');
const currentDocValue = { name: 'doc1' };
const newDocValue = { name: 'updated' };
const callbackA = sinon.spy();
const callbackB = sinon.spy();
// Test
let unsubscribeA;
let unsubscribeB;
await new Promise((resolve2) => {
unsubscribeA = collectionRef.onSnapshot((snapshot) => {
snapshot.forEach(doc => callbackA(doc.data()));
resolve2();
});
});
await new Promise((resolve2) => {
unsubscribeB = collectionRef.onSnapshot((snapshot) => {
snapshot.forEach(doc => callbackB(doc.data()));
resolve2();
});
});
callbackA.should.be.calledWith(currentDocValue);
callbackA.should.be.calledOnce();
callbackB.should.be.calledWith(currentDocValue);
callbackB.should.be.calledOnce();
const docRef = firebase.native.firestore().doc('document-tests/doc1');
await docRef.set(newDocValue);
await new Promise((resolve2) => {
setTimeout(() => resolve2(), 5);
});
callbackA.should.be.calledWith(newDocValue);
callbackB.should.be.calledWith(newDocValue);
callbackA.should.be.calledTwice();
callbackB.should.be.calledTwice();
// Unsubscribe A
unsubscribeA();
await docRef.set(currentDocValue);
await new Promise((resolve2) => {
setTimeout(() => resolve2(), 5);
});
callbackB.should.be.calledWith(currentDocValue);
callbackA.should.be.calledTwice();
callbackB.should.be.calledThrice();
// Unsubscribe B
unsubscribeB();
await docRef.set(newDocValue);
await new Promise((resolve2) => {
setTimeout(() => resolve2(), 5);
});
callbackA.should.be.calledTwice();
callbackB.should.be.calledThrice();
resolve();
});
});
});
// Where
context('where()', () => {
it('correctly handles == boolean values', () => {

View File

@ -49,6 +49,8 @@ function collectionReferenceTests({ describe, it, context, firebase }) {
callback.should.be.calledWith(currentDataValue);
// Update the document
await docRef.set(newDataValue);
await new Promise((resolve2) => {