Merge pull request #827 from invertase/firestore-transactions

Firestore Transactions
This commit is contained in:
Michael Diarmid 2018-03-05 00:05:54 +00:00 committed by GitHub
commit 6f0fd97a7e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 4434 additions and 1190 deletions

3
.gitignore vendored
View File

@ -1,3 +1,6 @@
coverage
coverage.ios.json
coverage.android.json
node_modules
npm-debug.log
*.DS_Store

View File

@ -1,4 +1,7 @@
node_modules
coverage.android.json
coverage.ios.json
coverage
npm-debug.log
*.DS_Store
.github

View File

@ -1,4 +1,4 @@
Copyright (c) 2017 Invertase Limited <oss@invertase.io>
Copyright (c) 2018 Invertase Limited <oss@invertase.io>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this library except in compliance with the License.

View File

@ -1,7 +1,9 @@
package io.invertase.firebase.firestore;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import android.util.Log;
import android.util.SparseArray;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
@ -12,8 +14,11 @@ 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.OnFailureListener;
import com.google.android.gms.tasks.OnSuccessListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.FirebaseApp;
import com.google.firebase.firestore.Transaction;
import com.google.firebase.firestore.DocumentReference;
import com.google.firebase.firestore.FieldValue;
import com.google.firebase.firestore.FirebaseFirestore;
@ -26,11 +31,12 @@ 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 SparseArray<RNFirebaseTransactionHandler> transactionHandlers = new SparseArray<>();
private SparseArray<RNFirebaseFirestoreTransactionHandler> transactionHandlers = new SparseArray<>();
RNFirebaseFirestore(ReactApplicationContext reactContext) {
super(reactContext);
@ -92,7 +98,7 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule {
break;
case "SET":
Map<String, Object> options = (Map) write.get("options");
if (options != null && options.containsKey("merge") && (boolean)options.get("merge")) {
if (options != null && options.containsKey("merge") && (boolean) options.get("merge")) {
batch = batch.set(ref, data, SetOptions.merge());
} else {
batch = batch.set(ref, data);
@ -113,7 +119,7 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule {
promise.resolve(null);
} else {
Log.e(TAG, "documentBatch:onComplete:failure", task.getException());
RNFirebaseFirestore.promiseRejectException(promise, (FirebaseFirestoreException)task.getException());
RNFirebaseFirestore.promiseRejectException(promise, (FirebaseFirestoreException) task.getException());
}
}
});
@ -160,6 +166,199 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule {
ref.update(data, promise);
}
/**
* Try clean up previous transactions on reload
*
*/
@Override
public void onCatalystInstanceDestroy() {
for (int i = 0, size = transactionHandlers.size(); i < size; i++) {
RNFirebaseFirestoreTransactionHandler transactionHandler = transactionHandlers.get(i);
if (transactionHandler != null) {
transactionHandler.abort();
}
}
transactionHandlers.clear();
}
/*
* Transaction Methods
*/
/**
* Calls the internal Firestore Transaction classes instance .get(ref) method and resolves with
* the DocumentSnapshot.
*
* @param appName
* @param transactionId
* @param path
* @param promise
*/
@ReactMethod
public void transactionGetDocument(String appName, int transactionId, String path, final Promise promise) {
RNFirebaseFirestoreTransactionHandler handler = transactionHandlers.get(transactionId);
if (handler == null) {
promise.reject(
"internal-error",
"An internal error occurred whilst attempting to find a native transaction by id."
);
} else {
DocumentReference ref = getDocumentForAppPath(appName, path).getRef();
handler.getDocument(ref, promise);
}
}
/**
* Aborts any pending signals and deletes the transaction handler.
*
* @param appName
* @param transactionId
*/
@ReactMethod
public void transactionDispose(String appName, int transactionId) {
RNFirebaseFirestoreTransactionHandler handler = transactionHandlers.get(transactionId);
if (handler != null) {
handler.abort();
transactionHandlers.delete(transactionId);
}
}
/**
* Signals to transactionHandler that the command buffer is ready.
*
* @param appName
* @param transactionId
* @param commandBuffer
*/
@ReactMethod
public void transactionApplyBuffer(String appName, int transactionId, ReadableArray commandBuffer) {
RNFirebaseFirestoreTransactionHandler handler = transactionHandlers.get(transactionId);
if (handler != null) {
handler.signalBufferReceived(commandBuffer);
}
}
/**
* Begin a new transaction via AsyncTask 's
*
* @param appName
* @param transactionId
*/
@ReactMethod
public void transactionBegin(final String appName, int transactionId) {
final RNFirebaseFirestoreTransactionHandler transactionHandler = new RNFirebaseFirestoreTransactionHandler(appName, transactionId);
transactionHandlers.put(transactionId, transactionHandler);
AsyncTask.execute(new Runnable() {
@Override
public void run() {
getFirestoreForApp(appName)
.runTransaction(new Transaction.Function<Void>() {
@Override
public Void apply(@NonNull Transaction transaction) throws FirebaseFirestoreException {
transactionHandler.resetState(transaction);
// emit the update cycle to JS land using an async task
// otherwise it gets blocked by the pending lock await
AsyncTask.execute(new Runnable() {
@Override
public void run() {
WritableMap eventMap = transactionHandler.createEventMap(null, "update");
Utils.sendEvent(getReactApplicationContext(), "firestore_transaction_event", eventMap);
}
});
// wait for a signal to be received from JS land code
transactionHandler.await();
// exit early if aborted - has to throw an exception otherwise will just keep trying ...
if (transactionHandler.aborted) {
throw new FirebaseFirestoreException("abort", FirebaseFirestoreException.Code.ABORTED);
}
// exit early if timeout from bridge - has to throw an exception otherwise will just keep trying ...
if (transactionHandler.timeout) {
throw new FirebaseFirestoreException("timeout", FirebaseFirestoreException.Code.DEADLINE_EXCEEDED);
}
// process any buffered commands from JS land
ReadableArray buffer = transactionHandler.getCommandBuffer();
// exit early if no commands
if (buffer == null) {
return null;
}
for (int i = 0, size = buffer.size(); i < size; i++) {
ReadableMap data;
ReadableMap command = buffer.getMap(i);
String path = command.getString("path");
String type = command.getString("type");
RNFirebaseFirestoreDocumentReference documentReference = getDocumentForAppPath(appName, path);
switch (type) {
case "set":
data = command.getMap("data");
ReadableMap options = command.getMap("options");
Map<String, Object> setData = FirestoreSerialize.parseReadableMap(RNFirebaseFirestore.getFirestoreForApp(appName), data);
if (options != null && options.hasKey("merge") && options.getBoolean("merge")) {
transaction.set(documentReference.getRef(), setData, SetOptions.merge());
} else {
transaction.set(documentReference.getRef(), setData);
}
break;
case "update":
data = command.getMap("data");
Map<String, Object> updateData = FirestoreSerialize.parseReadableMap(RNFirebaseFirestore.getFirestoreForApp(appName), data);
transaction.update(documentReference.getRef(), updateData);
break;
case "delete":
transaction.delete(documentReference.getRef());
break;
default:
throw new IllegalArgumentException("Unknown command type at index " + i + ".");
}
}
return null;
}
})
.addOnSuccessListener(new OnSuccessListener<Void>() {
@Override
public void onSuccess(Void aVoid) {
if (!transactionHandler.aborted) {
Log.d(TAG, "Transaction onSuccess!");
WritableMap eventMap = transactionHandler.createEventMap(null, "complete");
Utils.sendEvent(getReactApplicationContext(), "firestore_transaction_event", eventMap);
}
}
})
.addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
if (!transactionHandler.aborted) {
Log.w(TAG, "Transaction onFailure.", e);
WritableMap eventMap = transactionHandler.createEventMap((FirebaseFirestoreException) e, "error");
Utils.sendEvent(getReactApplicationContext(), "firestore_transaction_event", eventMap);
}
}
});
}
});
}
/*
* INTERNALS/UTILS
*/
@ -197,7 +396,7 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule {
* @param filters
* @param orders
* @param options
* @param path @return
* @param path @return
*/
private RNFirebaseFirestoreCollectionReference getCollectionForAppPath(String appName, String path,
ReadableArray filters,

View File

@ -145,6 +145,10 @@ public class RNFirebaseFirestoreDocumentReference {
* INTERNALS/UTILS
*/
public DocumentReference getRef() {
return ref;
}
boolean hasListeners() {
return !documentSnapshotListeners.isEmpty();
}

View File

@ -0,0 +1,167 @@
package io.invertase.firebase.firestore;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.WritableMap;
import com.google.firebase.firestore.DocumentReference;
import com.google.firebase.firestore.DocumentSnapshot;
import com.google.firebase.firestore.FirebaseFirestoreException;
import com.google.firebase.firestore.Transaction;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import javax.annotation.Nullable;
class RNFirebaseFirestoreTransactionHandler {
private String appName;
private long timeoutAt;
private int transactionId;
private final ReentrantLock lock;
private final Condition condition;
private ReadableArray commandBuffer;
private Transaction firestoreTransaction;
boolean aborted = false;
boolean timeout = false;
RNFirebaseFirestoreTransactionHandler(String app, int id) {
appName = app;
transactionId = id;
updateInternalTimeout();
lock = new ReentrantLock();
condition = lock.newCondition();
}
/*
* -------------
* PACKAGE API
* -------------
*/
/**
* Abort the currently in progress transaction if any.
*/
void abort() {
aborted = true;
safeUnlock();
}
/**
* Reset handler state - clears command buffer + updates to new Transaction instance
*
* @param firestoreTransaction
*/
void resetState(Transaction firestoreTransaction) {
this.commandBuffer = null;
this.firestoreTransaction = firestoreTransaction;
}
/**
* Signal that the transaction buffer has been received and needs to be processed.
*
* @param buffer
*/
void signalBufferReceived(ReadableArray buffer) {
lock.lock();
try {
commandBuffer = buffer;
condition.signalAll();
} finally {
safeUnlock();
}
}
/**
* Wait for signalBufferReceived to signal condition
*
* @throws InterruptedException
*/
void await() {
lock.lock();
updateInternalTimeout();
try {
while (!aborted && !timeout && !condition.await(10, TimeUnit.MILLISECONDS)) {
if (System.currentTimeMillis() > timeoutAt) timeout = true;
}
} catch (InterruptedException ie) {
// should never be interrupted
} finally {
safeUnlock();
}
}
/**
* Get the current pending command buffer.
*
* @return
*/
ReadableArray getCommandBuffer() {
return commandBuffer;
}
/**
* Get and resolve a DocumentSnapshot from transaction.get(ref);
*
* @param ref
* @param promise
*/
void getDocument(DocumentReference ref, Promise promise) {
updateInternalTimeout();
try {
DocumentSnapshot documentSnapshot = firestoreTransaction.get(ref);
WritableMap writableMap = FirestoreSerialize.snapshotToWritableMap(documentSnapshot);
promise.resolve(writableMap);
} catch (FirebaseFirestoreException firestoreException) {
WritableMap jsError = RNFirebaseFirestore.getJSError(firestoreException);
promise.reject(jsError.getString("code"), jsError.getString("message"));
}
}
/**
* Event map for `firestore_transaction_event` events.
*
* @param error
* @param type
* @return
*/
WritableMap createEventMap(@Nullable FirebaseFirestoreException error, String type) {
WritableMap eventMap = Arguments.createMap();
eventMap.putInt("id", transactionId);
eventMap.putString("appName", appName);
if (error != null) {
eventMap.putString("type", "error");
eventMap.putMap("error", RNFirebaseFirestore.getJSError(error));
} else {
eventMap.putString("type", type);
}
return eventMap;
}
/*
* -------------
* INTERNAL API
* -------------
*/
private void safeUnlock() {
if (lock.isLocked()) {
lock.unlock();
}
}
private void updateInternalTimeout() {
timeoutAt = System.currentTimeMillis() + 15000;
}
}

View File

@ -1,8 +0,0 @@
#!/usr/bin/env bash
# Copy firebase lib into tests directory
mkdir tests/firebase
cp -R lib/* tests/firebase
# Install /tests npm packages
cd tests && npm install

View File

@ -18,6 +18,7 @@ static NSString *const DATABASE_CHILD_REMOVED_EVENT = @"child_removed";
static NSString *const DATABASE_CHILD_MOVED_EVENT = @"child_moved";
// Firestore
static NSString *const FIRESTORE_TRANSACTION_EVENT = @"firestore_transaction_event";
static NSString *const FIRESTORE_COLLECTION_SYNC_EVENT = @"firestore_collection_sync_event";
static NSString *const FIRESTORE_DOCUMENT_SYNC_EVENT = @"firestore_document_sync_event";

View File

@ -22,44 +22,44 @@ RCT_EXPORT_MODULE();
return self;
}
RCT_EXPORT_METHOD(goOnline:(NSString *) appDisplayName) {
RCT_EXPORT_METHOD(goOnline:(NSString *)appDisplayName) {
[[RNFirebaseDatabase getDatabaseForApp:appDisplayName] goOnline];
}
RCT_EXPORT_METHOD(goOffline:(NSString *) appDisplayName) {
RCT_EXPORT_METHOD(goOffline:(NSString *)appDisplayName) {
[[RNFirebaseDatabase getDatabaseForApp:appDisplayName] goOffline];
}
RCT_EXPORT_METHOD(setPersistence:(NSString *) appDisplayName
state:(BOOL) state) {
RCT_EXPORT_METHOD(setPersistence:(NSString *)appDisplayName
state:(BOOL)state) {
[RNFirebaseDatabase getDatabaseForApp:appDisplayName].persistenceEnabled = state;
}
RCT_EXPORT_METHOD(setPersistenceCacheSizeBytes:(NSString *) appDisplayName
size:(NSInteger *) size) {
RCT_EXPORT_METHOD(setPersistenceCacheSizeBytes:(NSString *)appDisplayName
size:(NSInteger *)size) {
[RNFirebaseDatabase getDatabaseForApp:appDisplayName].persistenceCacheSizeBytes = (NSUInteger)size;
}
RCT_EXPORT_METHOD(enableLogging:(BOOL) enabled) {
RCT_EXPORT_METHOD(enableLogging:(BOOL)enabled) {
[FIRDatabase setLoggingEnabled:enabled];
}
RCT_EXPORT_METHOD(keepSynced:(NSString *) appDisplayName
key:(NSString *) key
path:(NSString *) path
modifiers:(NSArray *) modifiers
state:(BOOL) state) {
RCT_EXPORT_METHOD(keepSynced:(NSString *)appDisplayName
key:(NSString *)key
path:(NSString *)path
modifiers:(NSArray *)modifiers
state:(BOOL)state) {
FIRDatabaseQuery *query = [self getInternalReferenceForApp:appDisplayName key:key path:path modifiers:modifiers].query;
[query keepSynced:state];
}
RCT_EXPORT_METHOD(transactionTryCommit:(NSString *) appDisplayName
transactionId:(nonnull NSNumber *) transactionId
updates:(NSDictionary *) updates) {
RCT_EXPORT_METHOD(transactionTryCommit:(NSString *)appDisplayName
transactionId:(nonnull NSNumber *)transactionId
updates:(NSDictionary *)updates) {
__block NSMutableDictionary *transactionState;
dispatch_sync(_transactionQueue, ^{
transactionState = _transactions[transactionId];
transactionState = _transactions[[transactionId stringValue]];
});
if (!transactionState) {
@ -82,171 +82,166 @@ RCT_EXPORT_METHOD(transactionTryCommit:(NSString *) appDisplayName
}
RCT_EXPORT_METHOD(transactionStart:(NSString *) appDisplayName
path:(NSString *) path
transactionId:(nonnull NSNumber *) transactionId
applyLocally:(BOOL) applyLocally) {
RCT_EXPORT_METHOD(transactionStart:(NSString *)appDisplayName
path:(NSString *)path
transactionId:(nonnull NSNumber *)transactionId
applyLocally:(BOOL)applyLocally) {
dispatch_async(_transactionQueue, ^{
NSMutableDictionary *transactionState = [NSMutableDictionary new];
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
transactionState[@"semaphore"] = sema;
FIRDatabaseReference *ref = [self getReferenceForAppPath:appDisplayName path:path];
[ref runTransactionBlock:^FIRTransactionResult * _Nonnull(FIRMutableData *
_Nonnull currentData) {
dispatch_barrier_async(_transactionQueue, ^{
[_transactions setValue:transactionState forKey:transactionId];
NSDictionary *updateMap = [self createTransactionUpdateMap:appDisplayName transactionId:transactionId updatesData:currentData];
[RNFirebaseUtil sendJSEvent:self name:DATABASE_TRANSACTION_EVENT body:updateMap];
});
[ref runTransactionBlock:^FIRTransactionResult *_Nonnull (FIRMutableData *_Nonnull currentData) {
dispatch_barrier_async(_transactionQueue, ^{
[_transactions setValue:transactionState forKey:[transactionId stringValue]];
NSDictionary *updateMap = [self createTransactionUpdateMap:appDisplayName transactionId:transactionId updatesData:currentData];
[RNFirebaseUtil sendJSEvent:self name:DATABASE_TRANSACTION_EVENT body:updateMap];
});
// wait for the js event handler to call tryCommitTransaction
// this wait occurs on the Firebase Worker Queue
// so if the tryCommitTransaction fails to signal the semaphore
// no further blocks will be executed by Firebase until the timeout expires
dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC);
BOOL timedout = dispatch_semaphore_wait(sema, delayTime) != 0;
// wait for the js event handler to call tryCommitTransaction
// this wait occurs on the Firebase Worker Queue
// so if the tryCommitTransaction fails to signal the semaphore
// no further blocks will be executed by Firebase until the timeout expires
dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC);
BOOL timedout = dispatch_semaphore_wait(sema, delayTime) != 0;
BOOL abort = [transactionState valueForKey:@"abort"] || timedout;
id value = [transactionState valueForKey:@"value"];
BOOL abort = [transactionState valueForKey:@"abort"] || timedout;
id value = [transactionState valueForKey:@"value"];
dispatch_barrier_async(_transactionQueue, ^{
[_transactions removeObjectForKey:transactionId];
});
dispatch_barrier_async(_transactionQueue, ^{
[_transactions removeObjectForKey:[transactionId stringValue]];
});
if (abort) {
return [FIRTransactionResult abort];
} else {
currentData.value = value;
return [FIRTransactionResult successWithValue:currentData];
}
}
andCompletionBlock:
^(NSError *_Nullable databaseError, BOOL committed, FIRDataSnapshot *_Nullable snapshot) {
if (abort) {
return [FIRTransactionResult abort];
} else {
currentData.value = value;
return [FIRTransactionResult successWithValue:currentData];
}
} andCompletionBlock:^(NSError *_Nullable databaseError, BOOL committed, FIRDataSnapshot *_Nullable snapshot) {
NSDictionary *resultMap = [self createTransactionResultMap:appDisplayName transactionId:transactionId error:databaseError committed:committed snapshot:snapshot];
[RNFirebaseUtil sendJSEvent:self name:DATABASE_TRANSACTION_EVENT body:resultMap];
}
withLocalEvents:
applyLocally];
} withLocalEvents:applyLocally];
});
}
RCT_EXPORT_METHOD(onDisconnectSet:(NSString *) appDisplayName
path:(NSString *) path
props:(NSDictionary *) props
resolver:(RCTPromiseResolveBlock) resolve
rejecter:(RCTPromiseRejectBlock) reject) {
RCT_EXPORT_METHOD(onDisconnectSet:(NSString *)appDisplayName
path:(NSString *)path
props:(NSDictionary *)props
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
FIRDatabaseReference *ref = [self getReferenceForAppPath:appDisplayName path:path];
[ref onDisconnectSetValue:props[@"value"] withCompletionBlock:^(NSError *_Nullable error, FIRDatabaseReference *_Nonnull _ref) {
[RNFirebaseDatabase handlePromise:resolve rejecter:reject databaseError:error];
}];
}
RCT_EXPORT_METHOD(onDisconnectUpdate:(NSString *) appDisplayName
path:(NSString *) path
props:(NSDictionary *) props
resolver:(RCTPromiseResolveBlock) resolve
rejecter:(RCTPromiseRejectBlock) reject) {
RCT_EXPORT_METHOD(onDisconnectUpdate:(NSString *)appDisplayName
path:(NSString *)path
props:(NSDictionary *)props
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
FIRDatabaseReference *ref = [self getReferenceForAppPath:appDisplayName path:path];
[ref onDisconnectUpdateChildValues:props withCompletionBlock:^(NSError *_Nullable error, FIRDatabaseReference *_Nonnull _ref) {
[RNFirebaseDatabase handlePromise:resolve rejecter:reject databaseError:error];
}];
}
RCT_EXPORT_METHOD(onDisconnectRemove:(NSString *) appDisplayName
path:(NSString *) path
resolver:(RCTPromiseResolveBlock) resolve
rejecter:(RCTPromiseRejectBlock) reject) {
RCT_EXPORT_METHOD(onDisconnectRemove:(NSString *)appDisplayName
path:(NSString *)path
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
FIRDatabaseReference *ref = [self getReferenceForAppPath:appDisplayName path:path];
[ref onDisconnectRemoveValueWithCompletionBlock:^(NSError *_Nullable error, FIRDatabaseReference *_Nonnull _ref) {
[RNFirebaseDatabase handlePromise:resolve rejecter:reject databaseError:error];
}];
}
RCT_EXPORT_METHOD(onDisconnectCancel:(NSString *) appDisplayName
path:(NSString *) path
resolver:(RCTPromiseResolveBlock) resolve
rejecter:(RCTPromiseRejectBlock) reject) {
RCT_EXPORT_METHOD(onDisconnectCancel:(NSString *)appDisplayName
path:(NSString *)path
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
FIRDatabaseReference *ref = [self getReferenceForAppPath:appDisplayName path:path];
[ref cancelDisconnectOperationsWithCompletionBlock:^(NSError *_Nullable error, FIRDatabaseReference *_Nonnull _ref) {
[RNFirebaseDatabase handlePromise:resolve rejecter:reject databaseError:error];
}];
}
RCT_EXPORT_METHOD(set:(NSString *) appDisplayName
path:(NSString *) path
props:(NSDictionary *) props
resolver:(RCTPromiseResolveBlock) resolve
rejecter:(RCTPromiseRejectBlock) reject) {
RCT_EXPORT_METHOD(set:(NSString *)appDisplayName
path:(NSString *)path
props:(NSDictionary *)props
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
FIRDatabaseReference *ref = [self getReferenceForAppPath:appDisplayName path:path];
[ref setValue:[props valueForKey:@"value"] withCompletionBlock:^(NSError *_Nullable error, FIRDatabaseReference *_Nonnull _ref) {
[RNFirebaseDatabase handlePromise:resolve rejecter:reject databaseError:error];
}];
}
RCT_EXPORT_METHOD(setPriority:(NSString *) appDisplayName
path:(NSString *) path
priority:(NSDictionary *) priority
resolver:(RCTPromiseResolveBlock) resolve
rejecter:(RCTPromiseRejectBlock) reject) {
RCT_EXPORT_METHOD(setPriority:(NSString *)appDisplayName
path:(NSString *)path
priority:(NSDictionary *)priority
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
FIRDatabaseReference *ref = [self getReferenceForAppPath:appDisplayName path:path];
[ref setPriority:[priority valueForKey:@"value"] withCompletionBlock:^(NSError *_Nullable error, FIRDatabaseReference *_Nonnull ref) {
[RNFirebaseDatabase handlePromise:resolve rejecter:reject databaseError:error];
}];
}
RCT_EXPORT_METHOD(setWithPriority:(NSString *) appDisplayName
path:(NSString *) path
data:(NSDictionary *) data
priority:(NSDictionary *) priority
resolver:(RCTPromiseResolveBlock) resolve
rejecter:(RCTPromiseRejectBlock) reject) {
RCT_EXPORT_METHOD(setWithPriority:(NSString *)appDisplayName
path:(NSString *)path
data:(NSDictionary *)data
priority:(NSDictionary *)priority
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
FIRDatabaseReference *ref = [self getReferenceForAppPath:appDisplayName path:path];
[ref setValue:[data valueForKey:@"value"] andPriority:[priority valueForKey:@"value"] withCompletionBlock:^(NSError *_Nullable error, FIRDatabaseReference *_Nonnull ref) {
[RNFirebaseDatabase handlePromise:resolve rejecter:reject databaseError:error];
}];
}
RCT_EXPORT_METHOD(update:(NSString *) appDisplayName
path:(NSString *) path
props:(NSDictionary *) props
resolver:(RCTPromiseResolveBlock) resolve
rejecter:(RCTPromiseRejectBlock) reject) {
RCT_EXPORT_METHOD(update:(NSString *)appDisplayName
path:(NSString *)path
props:(NSDictionary *)props
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
FIRDatabaseReference *ref = [self getReferenceForAppPath:appDisplayName path:path];
[ref updateChildValues:props withCompletionBlock:^(NSError *_Nullable error, FIRDatabaseReference *_Nonnull _ref) {
[RNFirebaseDatabase handlePromise:resolve rejecter:reject databaseError:error];
}];
}
RCT_EXPORT_METHOD(remove:(NSString *) appDisplayName
path:(NSString *) path
resolver:(RCTPromiseResolveBlock) resolve
rejecter:(RCTPromiseRejectBlock) reject) {
RCT_EXPORT_METHOD(remove:(NSString *)appDisplayName
path:(NSString *)path
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
FIRDatabaseReference *ref = [self getReferenceForAppPath:appDisplayName path:path];
[ref removeValueWithCompletionBlock:^(NSError *_Nullable error, FIRDatabaseReference *_Nonnull _ref) {
[RNFirebaseDatabase handlePromise:resolve rejecter:reject databaseError:error];
}];
}
RCT_EXPORT_METHOD(once:(NSString *) appDisplayName
key:(NSString *) key
path:(NSString *) path
modifiers:(NSArray *) modifiers
eventName:(NSString *) eventName
resolver:(RCTPromiseResolveBlock) resolve
rejecter:(RCTPromiseRejectBlock) reject) {
RCT_EXPORT_METHOD(once:(NSString *)appDisplayName
key:(NSString *)key
path:(NSString *)path
modifiers:(NSArray *)modifiers
eventName:(NSString *)eventName
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
RNFirebaseDatabaseReference *ref = [self getInternalReferenceForApp:appDisplayName key:key path:path modifiers:modifiers];
[ref once:eventName resolver:resolve rejecter:reject];
}
RCT_EXPORT_METHOD(on:(NSString *) appDisplayName
props:(NSDictionary *) props) {
RCT_EXPORT_METHOD(on:(NSString *)appDisplayName
props:(NSDictionary *)props) {
RNFirebaseDatabaseReference *ref = [self getCachedInternalReferenceForApp:appDisplayName props:props];
[ref on:props[@"eventType"] registration:props[@"registration"]];
}
RCT_EXPORT_METHOD(off:(NSString *) key
eventRegistrationKey:(NSString *) eventRegistrationKey) {
RCT_EXPORT_METHOD(off:(NSString *)key
eventRegistrationKey:(NSString *)eventRegistrationKey) {
RNFirebaseDatabaseReference *ref = _dbReferences[key];
if (ref) {
[ref removeEventListener:eventRegistrationKey];
@ -331,7 +326,7 @@ RCT_EXPORT_METHOD(off:(NSString *) key
message = [RNFirebaseDatabase getMessageWithService:@"The write was canceled by the user." service:service fullCode:code];
break;
// TODO: Missing iOS equivalent codes
// TODO: Missing iOS equivalent codes
case -1:
code = [RNFirebaseDatabase getCodeWithService:service code:@"data-stale"];
message = [RNFirebaseDatabase getMessageWithService:@"The transaction needs to be run again with current data." service:service fullCode:code];
@ -413,8 +408,7 @@ RCT_EXPORT_METHOD(off:(NSString *) key
return @[DATABASE_SYNC_EVENT, DATABASE_TRANSACTION_EVENT];
}
+ (BOOL)requiresMainQueueSetup
{
+ (BOOL)requiresMainQueueSetup {
return YES;
}
@ -424,3 +418,7 @@ RCT_EXPORT_METHOD(off:(NSString *) key
@implementation RNFirebaseDatabase
@end
#endif

View File

@ -5,10 +5,10 @@
#if __has_include(<FirebaseDatabase/FIRDatabase.h>)
- (id)initWithPathAndModifiers:(RCTEventEmitter *)emitter
appDisplayName:(NSString *) appDisplayName
key:(NSString *) key
refPath:(NSString *) refPath
modifiers:(NSArray *) modifiers {
appDisplayName:(NSString *)appDisplayName
key:(NSString *)key
refPath:(NSString *)refPath
modifiers:(NSArray *)modifiers {
self = [super init];
if (self) {
_emitter = emitter;
@ -22,14 +22,14 @@
}
- (void)removeEventListener:(NSString *)eventRegistrationKey {
FIRDatabaseHandle handle = (FIRDatabaseHandle) [_listeners[eventRegistrationKey] integerValue];
FIRDatabaseHandle handle = (FIRDatabaseHandle)[_listeners[eventRegistrationKey] integerValue];
if (handle) {
[_query removeObserverWithHandle:handle];
[_listeners removeObjectForKey:eventRegistrationKey];
}
}
- (void)on:(NSString *) eventType registration:(NSDictionary *) registration {
- (void)on:(NSString *)eventType registration:(NSDictionary *)registration {
NSString *eventRegistrationKey = registration[@"eventRegistrationKey"];
if (![self hasEventListener:eventRegistrationKey]) {
id andPreviousSiblingKeyWithBlock = ^(FIRDataSnapshot *_Nonnull snapshot, NSString *_Nullable previousChildName) {
@ -46,9 +46,9 @@
}
}
- (void)once:(NSString *) eventType
resolver:(RCTPromiseResolveBlock) resolve
rejecter:(RCTPromiseRejectBlock) reject {
- (void)once:(NSString *)eventType
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject {
FIRDataEventType firDataEventType = (FIRDataEventType)[self eventTypeFromName:eventType];
[_query observeSingleEventOfType:firDataEventType andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *_Nonnull snapshot, NSString *_Nullable previousChildName) {
NSDictionary *data = [RNFirebaseDatabaseReference snapshotToDictionary:snapshot previousChildName:previousChildName];
@ -59,33 +59,33 @@
}];
}
- (void)handleDatabaseEvent:(NSString *) eventType
registration:(NSDictionary *) registration
dataSnapshot:(FIRDataSnapshot *) dataSnapshot
previousChildName:(NSString *) previousChildName {
- (void)handleDatabaseEvent:(NSString *)eventType
registration:(NSDictionary *)registration
dataSnapshot:(FIRDataSnapshot *)dataSnapshot
previousChildName:(NSString *)previousChildName {
NSMutableDictionary *event = [[NSMutableDictionary alloc] init];
NSDictionary *data = [RNFirebaseDatabaseReference snapshotToDictionary:dataSnapshot previousChildName:previousChildName];
[event setValue:data forKey:@"data"];
[event setValue:_key forKey:@"key"];
[event setValue:eventType forKey:@"eventType"];
[event setValue:registration forKey:@"registration"];
[RNFirebaseUtil sendJSEvent:self.emitter name:DATABASE_SYNC_EVENT body:event];
}
- (void)handleDatabaseError:(NSDictionary *) registration
- (void)handleDatabaseError:(NSDictionary *)registration
error:(NSError *)error {
NSMutableDictionary *event = [[NSMutableDictionary alloc] init];
[event setValue:_key forKey:@"key"];
[event setValue:[RNFirebaseDatabase getJSError:error] forKey:@"error"];
[event setValue:registration forKey:@"registration"];
[RNFirebaseUtil sendJSEvent:self.emitter name:DATABASE_SYNC_EVENT body:event];
}
+ (NSDictionary *)snapshotToDictionary:(FIRDataSnapshot *) dataSnapshot
previousChildName:(NSString *) previousChildName {
+ (NSDictionary *)snapshotToDictionary:(FIRDataSnapshot *)dataSnapshot
previousChildName:(NSString *)previousChildName {
NSMutableDictionary *result = [[NSMutableDictionary alloc] init];
NSDictionary *snapshot = [RNFirebaseDatabaseReference snapshotToDict:dataSnapshot];
@ -97,7 +97,7 @@
+ (NSDictionary *)snapshotToDict:(FIRDataSnapshot *)dataSnapshot {
NSMutableDictionary *snapshot = [[NSMutableDictionary alloc] init];
[snapshot setValue:dataSnapshot.key forKey:@"key"];
[snapshot setValue:@(dataSnapshot.exists) forKey:@"exists"];
[snapshot setValue:@(dataSnapshot.hasChildren) forKey:@"hasChildren"];
@ -105,11 +105,11 @@
[snapshot setValue:[RNFirebaseDatabaseReference getChildKeys:dataSnapshot] forKey:@"childKeys"];
[snapshot setValue:dataSnapshot.priority forKey:@"priority"];
[snapshot setValue:dataSnapshot.value forKey:@"value"];
return snapshot;
}
+ (NSMutableArray *) getChildKeys:(FIRDataSnapshot *) snapshot {
+ (NSMutableArray *)getChildKeys:(FIRDataSnapshot *)snapshot {
NSMutableArray *childKeys = [NSMutableArray array];
if (snapshot.childrenCount > 0) {
NSEnumerator *children = [snapshot children];
@ -121,7 +121,7 @@
return childKeys;
}
- (FIRDatabaseQuery *)buildQueryAtPathWithModifiers:(NSString *) path
- (FIRDatabaseQuery *)buildQueryAtPathWithModifiers:(NSString *)path
modifiers:(NSArray *)modifiers {
FIRDatabase *firebaseDatabase = [RNFirebaseDatabase getDatabaseForApp:_appDisplayName];
FIRDatabaseQuery *query = [[firebaseDatabase reference] child:path];
@ -186,7 +186,7 @@
}
}
- (BOOL)hasEventListener:(NSString *) eventRegistrationKey {
- (BOOL)hasEventListener:(NSString *)eventRegistrationKey {
return _listeners[eventRegistrationKey] != nil;
}
@ -214,3 +214,4 @@
#endif
@end

View File

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

View File

@ -13,49 +13,217 @@ RCT_EXPORT_MODULE();
- (id)init {
self = [super init];
if (self != nil) {
_transactions = [[NSMutableDictionary alloc] init];
_transactionQueue = dispatch_queue_create("io.invertase.react-native-firebase.firestore", DISPATCH_QUEUE_CONCURRENT);
}
return self;
}
RCT_EXPORT_METHOD(enableLogging:(BOOL) enabled) {
/**
* TRANSACTIONS
*/
RCT_EXPORT_METHOD(transactionGetDocument:(NSString *)appDisplayName
transactionId:(nonnull NSNumber *)transactionId
path:(NSString *)path
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
__block NSMutableDictionary *transactionState;
dispatch_sync(_transactionQueue, ^{
transactionState = _transactions[[transactionId stringValue]];
});
if (!transactionState) {
NSLog(@"transactionGetDocument called for non-existant transactionId %@", transactionId);
return;
}
NSError *error = nil;
FIRTransaction *transaction = [transactionState valueForKey:@"transaction"];
FIRDocumentReference *ref = [self getDocumentForAppPath:appDisplayName path:path].ref;
FIRDocumentSnapshot *snapshot = [transaction getDocument:ref error:&error];
if (error != nil) {
[RNFirebaseFirestore promiseRejectException:reject error:error];
} else {
NSDictionary *snapshotDict = [RNFirebaseFirestoreDocumentReference snapshotToDictionary:snapshot];
NSString *path = snapshotDict[@"path"];
if (path == nil) {
[snapshotDict setValue:ref.path forKey:@"path"];
}
resolve(snapshotDict);
}
}
RCT_EXPORT_METHOD(transactionDispose:(NSString *)appDisplayName
transactionId:(nonnull NSNumber *)transactionId) {
__block NSMutableDictionary *transactionState;
dispatch_sync(_transactionQueue, ^{
transactionState = _transactions[[transactionId stringValue]];
});
if (!transactionState) {
NSLog(@"transactionGetDocument called for non-existant transactionId %@", transactionId);
return;
}
dispatch_semaphore_t semaphore = [transactionState valueForKey:@"semaphore"];
[transactionState setValue:@true forKey:@"abort"];
dispatch_semaphore_signal(semaphore);
}
RCT_EXPORT_METHOD(transactionApplyBuffer:(NSString *)appDisplayName
transactionId:(nonnull NSNumber *)transactionId
commandBuffer:(NSArray *)commandBuffer) {
__block NSMutableDictionary *transactionState;
dispatch_sync(_transactionQueue, ^{
transactionState = _transactions[[transactionId stringValue]];
});
if (!transactionState) {
NSLog(@"transactionGetDocument called for non-existant transactionId %@", transactionId);
return;
}
dispatch_semaphore_t semaphore = [transactionState valueForKey:@"semaphore"];
[transactionState setValue:commandBuffer forKey:@"commandBuffer"];
dispatch_semaphore_signal(semaphore);
}
RCT_EXPORT_METHOD(transactionBegin:(NSString *)appDisplayName
transactionId:(nonnull NSNumber *)transactionId) {
FIRFirestore *firestore = [RNFirebaseFirestore getFirestoreForApp:appDisplayName];
__block BOOL aborted = false;
dispatch_async(_transactionQueue, ^{
NSMutableDictionary *transactionState = [NSMutableDictionary new];
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
transactionState[@"semaphore"] = semaphore;
[firestore runTransactionWithBlock:^id (FIRTransaction *transaction, NSError * *errorPointer) {
transactionState[@"transaction"] = transaction;
// Build and send transaction update event
dispatch_barrier_async(_transactionQueue, ^{
[_transactions setValue:transactionState forKey:[transactionId stringValue]];
NSMutableDictionary *eventMap = [NSMutableDictionary new];
eventMap[@"type"] = @"update";
eventMap[@"id"] = transactionId;
eventMap[@"appName"] = appDisplayName;
[RNFirebaseUtil sendJSEvent:self name:FIRESTORE_TRANSACTION_EVENT body:eventMap];
});
// wait for the js event handler to call transactionApplyBuffer
// this wait occurs on the RNFirestore Worker Queue so if transactionApplyBuffer fails to
// signal the semaphore then no further blocks will be executed by RNFirestore until the timeout expires
dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, 3000 * NSEC_PER_SEC);
BOOL timedOut = dispatch_semaphore_wait(semaphore, delayTime) != 0;
aborted = [transactionState valueForKey:@"abort"];
// dispose of transaction dictionary
dispatch_barrier_async(_transactionQueue, ^{
[_transactions removeObjectForKey:[transactionId stringValue]];
});
if (aborted) {
*errorPointer = [NSError errorWithDomain:FIRFirestoreErrorDomain code:FIRFirestoreErrorCodeAborted userInfo:@{}];
return nil;
}
if (timedOut) {
*errorPointer = [NSError errorWithDomain:FIRFirestoreErrorDomain code:FIRFirestoreErrorCodeDeadlineExceeded userInfo:@{}];
return nil;
}
NSArray *commandBuffer = [transactionState valueForKey:@"commandBuffer"];
for (NSDictionary *command in commandBuffer) {
NSString *type = command[@"type"];
NSString *path = command[@"path"];
NSDictionary *data = [RNFirebaseFirestoreDocumentReference parseJSMap:firestore jsMap:command[@"data"]];
FIRDocumentReference *ref = [firestore documentWithPath:path];
if ([type isEqualToString:@"delete"]) {
[transaction deleteDocument:ref];
} else if ([type isEqualToString:@"set"]) {
NSDictionary *options = command[@"options"];
if (options && options[@"merge"]) {
[transaction setData:data forDocument:ref options:[FIRSetOptions merge]];
} else {
[transaction setData:data forDocument:ref];
}
} else if ([type isEqualToString:@"update"]) {
[transaction updateData:data forDocument:ref];
}
}
return nil;
} completion:^(id result, NSError *error) {
if (aborted == NO) {
NSMutableDictionary *eventMap = [NSMutableDictionary new];
eventMap[@"id"] = transactionId;
eventMap[@"appName"] = appDisplayName;
if (error != nil) {
eventMap[@"type"] = @"error";
eventMap[@"error"] = [RNFirebaseFirestore getJSError:error];
} else {
eventMap[@"type"] = @"complete";
}
[RNFirebaseUtil sendJSEvent:self name:FIRESTORE_TRANSACTION_EVENT body:eventMap];
}
}];
});
}
/**
* TRANSACTIONS END
*/
RCT_EXPORT_METHOD(enableLogging:(BOOL)enabled) {
[FIRFirestore enableLogging:enabled];
}
RCT_EXPORT_METHOD(collectionGet:(NSString *) appDisplayName
path:(NSString *) path
filters:(NSArray *) filters
orders:(NSArray *) orders
options:(NSDictionary *) options
resolver:(RCTPromiseResolveBlock) resolve
rejecter:(RCTPromiseRejectBlock) reject) {
RCT_EXPORT_METHOD(collectionGet:(NSString *)appDisplayName
path:(NSString *)path
filters:(NSArray *)filters
orders:(NSArray *)orders
options:(NSDictionary *)options
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
[[self getCollectionForAppPath:appDisplayName path:path filters:filters orders:orders options:options] get:resolve rejecter:reject];
}
RCT_EXPORT_METHOD(collectionOffSnapshot:(NSString *) appDisplayName
path:(NSString *) path
filters:(NSArray *) filters
orders:(NSArray *) orders
options:(NSDictionary *) options
listenerId:(nonnull NSString *) listenerId) {
RCT_EXPORT_METHOD(collectionOffSnapshot:(NSString *)appDisplayName
path:(NSString *)path
filters:(NSArray *)filters
orders:(NSArray *)orders
options:(NSDictionary *)options
listenerId:(nonnull NSString *)listenerId) {
[RNFirebaseFirestoreCollectionReference offSnapshot:listenerId];
}
RCT_EXPORT_METHOD(collectionOnSnapshot:(NSString *) appDisplayName
path:(NSString *) path
filters:(NSArray *) filters
orders:(NSArray *) orders
options:(NSDictionary *) options
listenerId:(nonnull NSString *) listenerId
queryListenOptions:(NSDictionary *) queryListenOptions) {
RCT_EXPORT_METHOD(collectionOnSnapshot:(NSString *)appDisplayName
path:(NSString *)path
filters:(NSArray *)filters
orders:(NSArray *)orders
options:(NSDictionary *)options
listenerId:(nonnull NSString *)listenerId
queryListenOptions:(NSDictionary *)queryListenOptions) {
RNFirebaseFirestoreCollectionReference *ref = [self getCollectionForAppPath:appDisplayName path:path filters:filters orders:orders options:options];
[ref onSnapshot:listenerId queryListenOptions:queryListenOptions];
}
RCT_EXPORT_METHOD(documentBatch:(NSString *) appDisplayName
writes:(NSArray *) writes
resolver:(RCTPromiseResolveBlock) resolve
rejecter:(RCTPromiseRejectBlock) reject) {
RCT_EXPORT_METHOD(documentBatch:(NSString *)appDisplayName
writes:(NSArray *)writes
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
FIRFirestore *firestore = [RNFirebaseFirestore getFirestoreForApp:appDisplayName];
FIRWriteBatch *batch = [firestore batch];
@ -80,7 +248,7 @@ RCT_EXPORT_METHOD(documentBatch:(NSString *) appDisplayName
}
}
[batch commitWithCompletion:^(NSError * _Nullable error) {
[batch commitWithCompletion:^(NSError *_Nullable error) {
if (error) {
[RNFirebaseFirestore promiseRejectException:reject error:error];
} else {
@ -89,55 +257,55 @@ RCT_EXPORT_METHOD(documentBatch:(NSString *) appDisplayName
}];
}
RCT_EXPORT_METHOD(documentDelete:(NSString *) appDisplayName
path:(NSString *) path
resolver:(RCTPromiseResolveBlock) resolve
rejecter:(RCTPromiseRejectBlock) reject) {
RCT_EXPORT_METHOD(documentDelete:(NSString *)appDisplayName
path:(NSString *)path
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
[[self getDocumentForAppPath:appDisplayName path:path] delete:resolve rejecter:reject];
}
RCT_EXPORT_METHOD(documentGet:(NSString *) appDisplayName
path:(NSString *) path
resolver:(RCTPromiseResolveBlock) resolve
rejecter:(RCTPromiseRejectBlock) reject) {
RCT_EXPORT_METHOD(documentGet:(NSString *)appDisplayName
path:(NSString *)path
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
[[self getDocumentForAppPath:appDisplayName path:path] get:resolve rejecter:reject];
}
RCT_EXPORT_METHOD(documentGetAll:(NSString *) appDisplayName
documents:(NSString *) documents
resolver:(RCTPromiseResolveBlock) resolve
rejecter:(RCTPromiseRejectBlock) reject) {
RCT_EXPORT_METHOD(documentGetAll:(NSString *)appDisplayName
documents:(NSString *)documents
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
// Not supported on iOS out of the box
}
RCT_EXPORT_METHOD(documentOffSnapshot:(NSString *) appDisplayName
path:(NSString *) path
listenerId:(nonnull NSString *) listenerId) {
RCT_EXPORT_METHOD(documentOffSnapshot:(NSString *)appDisplayName
path:(NSString *)path
listenerId:(nonnull NSString *)listenerId) {
[RNFirebaseFirestoreDocumentReference offSnapshot:listenerId];
}
RCT_EXPORT_METHOD(documentOnSnapshot:(NSString *) appDisplayName
path:(NSString *) path
listenerId:(nonnull NSString *) listenerId
docListenOptions:(NSDictionary *) docListenOptions) {
RCT_EXPORT_METHOD(documentOnSnapshot:(NSString *)appDisplayName
path:(NSString *)path
listenerId:(nonnull NSString *)listenerId
docListenOptions:(NSDictionary *)docListenOptions) {
RNFirebaseFirestoreDocumentReference *ref = [self getDocumentForAppPath:appDisplayName path:path];
[ref onSnapshot:listenerId docListenOptions:docListenOptions];
}
RCT_EXPORT_METHOD(documentSet:(NSString *) appDisplayName
path:(NSString *) path
data:(NSDictionary *) data
options:(NSDictionary *) options
resolver:(RCTPromiseResolveBlock) resolve
rejecter:(RCTPromiseRejectBlock) reject) {
RCT_EXPORT_METHOD(documentSet:(NSString *)appDisplayName
path:(NSString *)path
data:(NSDictionary *)data
options:(NSDictionary *)options
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
[[self getDocumentForAppPath:appDisplayName path:path] set:data options:options resolver:resolve rejecter:reject];
}
RCT_EXPORT_METHOD(documentUpdate:(NSString *) appDisplayName
path:(NSString *) path
data:(NSDictionary *) data
resolver:(RCTPromiseResolveBlock) resolve
rejecter:(RCTPromiseRejectBlock) reject) {
RCT_EXPORT_METHOD(documentUpdate:(NSString *)appDisplayName
path:(NSString *)path
data:(NSDictionary *)data
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
[[self getDocumentForAppPath:appDisplayName path:path] update:data resolver:resolve rejecter:reject];
}
@ -262,11 +430,10 @@ RCT_EXPORT_METHOD(documentUpdate:(NSString *) appDisplayName
}
- (NSArray<NSString *> *)supportedEvents {
return @[FIRESTORE_COLLECTION_SYNC_EVENT, FIRESTORE_DOCUMENT_SYNC_EVENT];
return @[FIRESTORE_COLLECTION_SYNC_EVENT, FIRESTORE_DOCUMENT_SYNC_EVENT, FIRESTORE_TRANSACTION_EVENT];
}
+ (BOOL)requiresMainQueueSetup
{
+ (BOOL)requiresMainQueueSetup {
return YES;
}
@ -276,3 +443,4 @@ RCT_EXPORT_METHOD(documentUpdate:(NSString *) appDisplayName
@implementation RNFirebaseFirestore
@end
#endif

View File

@ -21,14 +21,13 @@ const generateTransactionId = (): number => transactionId++;
*/
export default class TransactionHandler {
_database: Database;
_transactionListener: Function;
_transactions: { [number]: Object };
constructor(database: Database) {
this._transactions = {};
this._database = database;
this._transactionListener = SharedEventEmitter.addListener(
SharedEventEmitter.addListener(
getAppEventName(this._database, 'database_transaction_event'),
this._handleTransactionEvent.bind(this)
);

View File

@ -0,0 +1,181 @@
/**
* @flow
* Firestore Transaction representation wrapper
*/
import { mergeFieldPathData } from './utils';
import { buildNativeMap } from './utils/serialize';
import type Firestore from './';
import type { TransactionMeta } from './TransactionHandler';
import type DocumentReference from './DocumentReference';
import DocumentSnapshot from './DocumentSnapshot';
import { isObject, isString } from '../../utils';
import FieldPath from './FieldPath';
import { getNativeModule } from '../../utils/native';
type Command = {
type: 'set' | 'update' | 'delete',
path: string,
data: ?{ [string]: any },
options: ?{ merge: boolean },
};
type SetOptions = {
merge: boolean,
};
// TODO docs state all get requests must be made FIRST before any modifications
// TODO so need to validate that
/**
* @class Transaction
*/
export default class Transaction {
_pendingResult: ?any;
_firestore: Firestore;
_meta: TransactionMeta;
_commandBuffer: Array<Command>;
constructor(firestore: Firestore, meta: TransactionMeta) {
this._meta = meta;
this._commandBuffer = [];
this._firestore = firestore;
this._pendingResult = undefined;
}
/**
* -------------
* INTERNAL API
* -------------
*/
/**
* Clears the command buffer and any pending result in prep for
* the next transaction iteration attempt.
*
* @private
*/
_prepare() {
this._commandBuffer = [];
this._pendingResult = undefined;
}
/**
* -------------
* PUBLIC API
* -------------
*/
/**
* Reads the document referenced by the provided DocumentReference.
*
* @param documentRef DocumentReference A reference to the document to be retrieved. Value must not be null.
*
* @returns Promise<DocumentSnapshot>
*/
get(documentRef: DocumentReference): Promise<DocumentSnapshot> {
// todo validate doc ref
return getNativeModule(this._firestore)
.transactionGetDocument(this._meta.id, documentRef.path)
.then(result => new DocumentSnapshot(this._firestore, result));
}
/**
* Writes to the document referred to by the provided DocumentReference.
* If the document does not exist yet, it will be created. If you pass options,
* the provided data can be merged into the existing document.
*
* @param documentRef DocumentReference A reference to the document to be created. Value must not be null.
* @param data Object An object of the fields and values for the document.
* @param options SetOptions An object to configure the set behavior.
* Pass {merge: true} to only replace the values specified in the data argument.
* Fields omitted will remain untouched.
*
* @returns {Transaction}
*/
set(
documentRef: DocumentReference,
data: Object,
options?: SetOptions
): Transaction {
// todo validate doc ref
// todo validate data is object
this._commandBuffer.push({
type: 'set',
path: documentRef.path,
data: buildNativeMap(data),
options: options || {},
});
return this;
}
/**
* Updates fields in the document referred to by this DocumentReference.
* The update will fail if applied to a document that does not exist. Nested
* fields can be updated by providing dot-separated field path strings or by providing FieldPath objects.
*
* @param documentRef DocumentReference A reference to the document to be updated. Value must not be null.
* @param args any Either an object containing all of the fields and values to update,
* or a series of arguments alternating between fields (as string or FieldPath
* objects) and values.
*
* @returns {Transaction}
*/
update(documentRef: DocumentReference, ...args: Array<any>): Transaction {
// todo validate doc ref
let data = {};
if (args.length === 1) {
if (!isObject(args[0])) {
throw new Error(
'Transaction.update failed: If using a single data argument, it must be an object.'
);
}
[data] = args;
} else if (args.length % 2 === 1) {
throw new Error(
'Transaction.update failed: Must have either a single object data argument, or equal numbers of data key/value pairs.'
);
} else {
for (let i = 0; i < args.length; i += 2) {
const key = args[i];
const value = args[i + 1];
if (isString(key)) {
data[key] = value;
} else if (key instanceof FieldPath) {
data = mergeFieldPathData(data, key._segments, value);
} else {
throw new Error(
`Transaction.update failed: Argument at index ${i} must be a string or FieldPath`
);
}
}
}
this._commandBuffer.push({
type: 'update',
path: documentRef.path,
data: buildNativeMap(data),
});
return this;
}
/**
* Deletes the document referred to by the provided DocumentReference.
*
* @param documentRef DocumentReference A reference to the document to be deleted. Value must not be null.
*
* @returns {Transaction}
*/
delete(documentRef: DocumentReference): Transaction {
// todo validate doc ref
this._commandBuffer.push({
type: 'delete',
path: documentRef.path,
});
return this;
}
}

View File

@ -0,0 +1,234 @@
/**
* @flow
* Firestore Transaction representation wrapper
*/
import { getAppEventName, SharedEventEmitter } from '../../utils/events';
import { getLogger } from '../../utils/log';
import { getNativeModule } from '../../utils/native';
import Transaction from './Transaction';
import type Firestore from './';
let transactionId = 0;
/**
* Uses the push id generator to create a transaction id
* @returns {number}
* @private
*/
const generateTransactionId = (): number => transactionId++;
export type TransactionMeta = {
id: number,
stack: Array<string>,
reject: null | Function,
resolve: null | Function,
transaction: Transaction,
updateFunction: (transaction: Transaction) => Promise<any>,
};
type TransactionEvent = {
id: number,
type: 'update' | 'error' | 'complete',
error: ?{ code: string, message: string },
};
/**
* @class TransactionHandler
*/
export default class TransactionHandler {
_firestore: Firestore;
_pending: { [number]: TransactionMeta };
constructor(firestore: Firestore) {
this._pending = {};
this._firestore = firestore;
SharedEventEmitter.addListener(
getAppEventName(this._firestore, 'firestore_transaction_event'),
this._handleTransactionEvent.bind(this)
);
}
/**
* -------------
* INTERNAL API
* -------------
*/
/**
* Add a new transaction and start it natively.
* @param updateFunction
*/
_add(
updateFunction: (transaction: Transaction) => Promise<any>
): Promise<any> {
const id = generateTransactionId();
const meta = {
id,
reject: null,
resolve: null,
updateFunction,
stack: new Error().stack
.split('\n')
.slice(4)
.join('\n'),
};
meta.transaction = new Transaction(this._firestore, meta);
this._pending[id] = meta;
// deferred promise
return new Promise((resolve, reject) => {
getNativeModule(this._firestore).transactionBegin(id);
meta.resolve = r => {
resolve(r);
this._remove(id);
};
meta.reject = e => {
reject(e);
this._remove(id);
};
});
}
/**
* Destroys a local instance of a transaction meta
*
* @param id
* @private
*/
_remove(id) {
// todo confirm pending arg no longer needed
getNativeModule(this._firestore).transactionDispose(id);
// TODO may need delaying to next event loop
delete this._pending[id];
}
/**
* -------------
* EVENTS
* -------------
*/
/**
* Handles incoming native transaction events and distributes to correct
* internal handler by event.type
*
* @param event
* @returns {*}
* @private
*/
_handleTransactionEvent(event: TransactionEvent) {
switch (event.type) {
case 'update':
return this._handleUpdate(event);
case 'error':
return this._handleError(event);
case 'complete':
return this._handleComplete(event);
default:
getLogger(this._firestore).warn(
`Unknown transaction event type: '${event.type}'`,
event
);
return undefined;
}
}
/**
* Handles incoming native transaction update events
*
* @param event
* @private
*/
async _handleUpdate(event: TransactionEvent) {
const { id } = event;
// abort if no longer exists js side
if (!this._pending[id]) return this._remove(id);
const { updateFunction, transaction, reject } = this._pending[id];
// clear any saved state from previous transaction runs
transaction._prepare();
let finalError;
let updateFailed;
let pendingResult;
// run the users custom update functionality
try {
const possiblePromise = updateFunction(transaction);
// validate user has returned a promise in their update function
// TODO must it actually return a promise? Can't find any usages of it without one...
if (!possiblePromise || !possiblePromise.then) {
finalError = new Error(
'Update function for `firestore.runTransaction(updateFunction)` must return a Promise.'
);
} else {
pendingResult = await possiblePromise;
}
} catch (exception) {
// exception can still be falsey if user `Promise.reject();` 's with no args
// so we track the exception with a updateFailed boolean to ensure no fall-through
updateFailed = true;
finalError = exception;
}
// reject the final promise and remove from native
// update is failed when either the users updateFunction
// throws an error or rejects a promise
if (updateFailed) {
return reject(finalError);
}
// capture the resolved result as we'll need this
// to resolve the runTransaction() promise when
// native emits that the transaction is final
transaction._pendingResult = pendingResult;
// send the buffered update/set/delete commands for native to process
return getNativeModule(this._firestore).transactionApplyBuffer(
id,
transaction._commandBuffer
);
}
/**
* Handles incoming native transaction error events
*
* @param event
* @private
*/
_handleError(event: TransactionEvent) {
const { id, error } = event;
const meta = this._pending[id];
if (meta) {
const { code, message } = error;
// build a JS error and replace its stack
// with the captured one at start of transaction
// so it's actually relevant to the user
const errorWithStack = new Error(message);
errorWithStack.code = code;
errorWithStack.stack = meta.stack;
meta.reject(errorWithStack);
}
}
/**
* Handles incoming native transaction complete events
*
* @param event
* @private
*/
_handleComplete(event: TransactionEvent) {
const { id } = event;
const meta = this._pending[id];
if (meta) {
const pendingResult = meta.transaction._pendingResult;
meta.resolve(pendingResult);
}
}
}

View File

@ -13,6 +13,8 @@ import FieldValue from './FieldValue';
import GeoPoint from './GeoPoint';
import Path from './Path';
import WriteBatch from './WriteBatch';
import TransactionHandler from './TransactionHandler';
import Transaction from './Transaction';
import INTERNALS from '../../utils/internals';
import type DocumentSnapshot from './DocumentSnapshot';
@ -36,8 +38,9 @@ type DocumentSyncEvent = {
};
const NATIVE_EVENTS = [
'firestore_collection_sync_event',
'firestore_transaction_event',
'firestore_document_sync_event',
'firestore_collection_sync_event',
];
export const MODULE_NAME = 'RNFirebaseFirestore';
@ -48,6 +51,7 @@ export const NAMESPACE = 'firestore';
*/
export default class Firestore extends ModuleBase {
_referencePath: Path;
_transactionHandler: TransactionHandler;
constructor(app: App) {
super(app, {
@ -56,7 +60,9 @@ export default class Firestore extends ModuleBase {
multiApp: true,
namespace: NAMESPACE,
});
this._referencePath = new Path([]);
this._transactionHandler = new TransactionHandler(this);
SharedEventEmitter.addListener(
// sub to internal native event - this fans out to
@ -73,11 +79,23 @@ export default class Firestore extends ModuleBase {
);
}
/**
* -------------
* PUBLIC API
* -------------
*/
/**
* Creates a write batch, used for performing multiple writes as a single atomic operation.
*
* @returns {WriteBatch}
*/
batch(): WriteBatch {
return new WriteBatch(this);
}
/**
* Gets a CollectionReference instance that refers to the collection at the specified path.
*
* @param collectionPath
* @returns {CollectionReference}
@ -92,6 +110,7 @@ export default class Firestore extends ModuleBase {
}
/**
* Gets a DocumentReference instance that refers to the document at the specified path.
*
* @param documentPath
* @returns {DocumentReference}
@ -105,13 +124,27 @@ export default class Firestore extends ModuleBase {
return new DocumentReference(this, path);
}
enablePersistence(): Promise<void> {
throw new Error('Persistence is enabled by default on the Firestore SDKs');
/**
* Executes the given updateFunction and then attempts to commit the
* changes applied within the transaction. If any document read within
* the transaction has changed, Cloud Firestore retries the updateFunction.
*
* If it fails to commit after 5 attempts, the transaction fails.
*
* @param updateFunction
* @returns {void|Promise<any>}
*/
runTransaction(
updateFunction: (transaction: Transaction) => Promise<any>
): Promise<any> {
return this._transactionHandler._add(updateFunction);
}
runTransaction(): Promise<any> {
throw new Error('firebase.firestore().runTransaction() coming soon');
}
/**
* -------------
* UNSUPPORTED
* -------------
*/
setLogLevel(): void {
throw new Error(
@ -140,12 +173,29 @@ export default class Firestore extends ModuleBase {
);
}
/**
* -------------
* MISC
* -------------
*/
enablePersistence(): Promise<void> {
throw new Error('Persistence is enabled by default on the Firestore SDKs');
}
settings(): void {
throw new Error('firebase.firestore().settings() coming soon');
}
/**
* -------------
* INTERNALS
* -------------
*/
/**
* Internal collection sync listener
*
* @param event
* @private
*/
@ -165,6 +215,7 @@ export default class Firestore extends ModuleBase {
/**
* Internal document sync listener
*
* @param event
* @private
*/

View File

@ -49,7 +49,7 @@ export const buildNativeArray = (array: Object[]): NativeTypeMap[] => {
export const buildTypeMap = (value: any): NativeTypeMap | null => {
const type = typeOf(value);
if (value === null || value === undefined) {
if (value === null || value === undefined || Number.isNaN(value)) {
return {
type: 'null',
value: null,

2775
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -32,7 +32,7 @@
"jest": {
"preset": "jest-react-native",
"setupFiles": [],
"unmockedModulePathPatterns": ["./node_modules/react", "./node_modules/react-native", "./node_modules/react-native-mock", "./node_modules/react-addons-test-utils"]
"unmockedModulePathPatterns": ["./node_modules/react", "./node_modules/react-native", "./node_modues/react-native-mock", "./node_modules/react-addons-test-utils"]
},
"license": "APACHE-2.0",
"keywords": ["react", "admob", "auth", "config", "digits", "fabric", "phone-auth", "sms", "firestore", "cloud-firestore", "datastore", "remote-config", "transactions", "react-native", "react-native-firebase", "firebase", "fcm", "apn", "gcm", "analytics", "messaging", "database", "android", "ios", "crash", "firestack", "performance", "firestore", "dynamic-links", "crashlytics"],
@ -47,35 +47,35 @@
"babel-jest": "^14.1.0",
"babel-preset-react-native": "^1.9.0",
"babel-preset-react-native-syntax": "^1.0.0",
"debug": "^2.2.0",
"debug": "^2.6.9",
"enzyme": "^2.4.1",
"eslint": "^4.11.0",
"eslint": "^4.18.1",
"eslint-config-airbnb": "^16.1.0",
"eslint-config-prettier": "^2.9.0",
"eslint-plugin-flowtype": "^2.39.1",
"eslint-plugin-import": "^2.8.0",
"eslint-plugin-jsx-a11y": "^6.0.2",
"eslint-plugin-prettier": "^2.5.0",
"eslint-plugin-react": "^7.4.0",
"eslint-plugin-flowtype": "^2.46.1",
"eslint-plugin-import": "^2.9.0",
"eslint-plugin-jsx-a11y": "^6.0.3",
"eslint-plugin-prettier": "^2.6.0",
"eslint-plugin-react": "^7.7.0",
"flow-bin": "^0.61.0",
"flow-copy-source": "^1.2.1",
"flow-copy-source": "^1.3.0",
"genversion": "^2.0.1",
"husky": "^0.14.3",
"lint-staged": "^6.0.1",
"lint-staged": "^6.1.1",
"prettier": "1.10.2",
"react": "^16.2.0",
"react-dom": "^16.2.0",
"react-native": "^0.52.0",
"react-native": "^0.52.3",
"rimraf": "^2.6.2",
"shelljs": "^0.7.8",
"typescript": "^2.6.2",
"typescript": "^2.7.2",
"wml": "0.0.82"
},
"dependencies": {
"bows": "^1.6.0",
"opencollective": "^1.0.3",
"postinstall-build": "^5.0.1",
"prop-types": "^15.6.0"
"prop-types": "^15.6.1"
},
"rnpm": {
"android": {

View File

@ -141,32 +141,32 @@ PODS:
- nanopb/decode (0.3.8)
- nanopb/encode (0.3.8)
- Protobuf (3.5.0)
- React (0.52.0):
- React/Core (= 0.52.0)
- React/BatchedBridge (0.52.0):
- React (0.52.3):
- React/Core (= 0.52.3)
- React/BatchedBridge (0.52.3):
- React/Core
- React/cxxreact_legacy
- React/Core (0.52.0):
- yoga (= 0.52.0.React)
- React/cxxreact_legacy (0.52.0):
- React/Core (0.52.3):
- yoga (= 0.52.3.React)
- React/cxxreact_legacy (0.52.3):
- React/jschelpers_legacy
- React/jsinspector_legacy
- React/fishhook (0.52.0)
- React/jschelpers_legacy (0.52.0)
- React/jsinspector_legacy (0.52.0)
- React/RCTBlob (0.52.0):
- React/fishhook (0.52.3)
- React/jschelpers_legacy (0.52.3)
- React/jsinspector_legacy (0.52.3)
- React/RCTBlob (0.52.3):
- React/Core
- React/RCTNetwork (0.52.0):
- React/RCTNetwork (0.52.3):
- React/Core
- React/RCTText (0.52.0):
- React/RCTText (0.52.3):
- React/Core
- React/RCTWebSocket (0.52.0):
- React/RCTWebSocket (0.52.3):
- React/Core
- React/fishhook
- React/RCTBlob
- RNFirebase (3.2.7):
- React
- yoga (0.52.0.React)
- yoga (0.52.3.React)
DEPENDENCIES:
- Crashlytics (~> 3.9.3)
@ -192,11 +192,11 @@ DEPENDENCIES:
EXTERNAL SOURCES:
React:
:path: "../node_modules/react-native"
:path: ../node_modules/react-native
RNFirebase:
:path: "../../ios/RNFirebase.podspec"
:path: ../../ios/RNFirebase.podspec
yoga:
:path: "../node_modules/react-native/ReactCommon/yoga"
:path: ../node_modules/react-native/ReactCommon/yoga
SPEC CHECKSUMS:
BoringSSL: f3d6b8ce199b9c450a8cfc14895d07a2627fc232
@ -224,13 +224,13 @@ SPEC CHECKSUMS:
gRPC-ProtoRPC: f29e8b7445e0d3c0311678ab121e6c164da4ca5e
gRPC-RxLibrary: 8e0067bfe8a054022c7a81470baace4f2f633b48
GTMSessionFetcher: 5bb1eae636127de695590f50e7d248483eb891e6
leveldb-library: '08cba283675b7ed2d99629a4bc5fd052cd2bb6a5'
leveldb-library: 08cba283675b7ed2d99629a4bc5fd052cd2bb6a5
nanopb: 5601e6bca2dbf1ed831b519092ec110f66982ca3
Protobuf: 8a9838fba8dae3389230e1b7f8c104aa32389c03
React: 61a6bdf17a9ff16875c230e6ff278d9de274e16c
React: c0dfd2dfc970019d1ae7d48bf24cef530992e079
RNFirebase: 3a141a97041ea0757e2036c2bb18acbe9f0e105d
yoga: 646606bf554d54a16711f35596178522fbc00480
yoga: f45a46b966e1eb0c7a532cfd4beec5b97332ba48
PODFILE CHECKSUM: 67c98bcb203cb992da590bcab6f690f727653ca5
COCOAPODS: 1.2.1
COCOAPODS: 1.3.1

View File

@ -1006,7 +1006,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n";
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
6AE1012F46FF8A4D1D818A12 /* [CP] Copy Pods Resources */ = {
@ -1016,7 +1016,7 @@
);
inputPaths = (
"${SRCROOT}/Pods/Target Support Files/Pods-ReactNativeFirebaseDemo/Pods-ReactNativeFirebaseDemo-resources.sh",
$PODS_CONFIGURATION_BUILD_DIR/gRPC/gRPCCertificates.bundle,
"$PODS_CONFIGURATION_BUILD_DIR/gRPC/gRPCCertificates.bundle",
);
name = "[CP] Copy Pods Resources";
outputPaths = (

1238
tests/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -28,26 +28,26 @@
"bows": "^1.6.0",
"cuid": "^1.3.8",
"deeps": "^1.4.4",
"firebase": "^4.8.2",
"js-beautify": "^1.6.11",
"firebase": "^4.10.1",
"js-beautify": "^1.7.5",
"lodash.groupby": "^4.6.0",
"lodash.some": "^4.6.0",
"prop-types": "^15.6.0",
"query-string": "^5.0.0",
"prop-types": "^15.6.1",
"query-string": "^5.1.0",
"react": "^16.2.0",
"react-native": "^0.52.0",
"react-native": "^0.52.3",
"react-native-vector-icons": "^4.5.0",
"react-navigation": "^1.0.0-beta.9",
"react-redux": "^5.0.3",
"react-navigation": "^1.2.1",
"react-redux": "^5.0.7",
"react-test-renderer": "16.2.0",
"redux": "^3.6.0",
"redux-logger": "^2.8.2",
"redux-persist": "^4.4.2",
"redux-persist": "^4.10.2",
"redux-thunk": "^2.2.0",
"should": "^11.2.0",
"should-sinon": "^0.0.5",
"sinon": "^3.2.1",
"url-parse": "^1.1.9"
"url-parse": "^1.2.0"
},
"devDependencies": {
"babel-cli": "^6.24.0",
@ -59,8 +59,8 @@
"colors": "^1.1.2",
"eslint": "^3.16.1",
"eslint-config-airbnb": "^14.1.0",
"eslint-plugin-flowtype": "^2.30.0",
"eslint-plugin-import": "^2.2.0",
"eslint-plugin-flowtype": "^2.46.1",
"eslint-plugin-import": "^2.9.0",
"eslint-plugin-jsx-a11y": "^4.0.0",
"eslint-plugin-react": "^6.10.0",
"jest": "19.0.2",

View File

@ -55,8 +55,10 @@ console.log('RNApps -->', RNfirebase.apps);
instances.native
.auth()
.signInAnonymouslyAndRetrieveData()
.then(userCredential => {
console.log('defaultApp user ->', userCredential.user.toJSON());
.then(credential => {
if (credential) {
console.log('anotherApp credential ->', credential.user.toJSON());
}
});
// dynamically initialized apps need a ready check
@ -64,8 +66,10 @@ instances.another.onReady().then(app => {
app
.auth()
.signInAnonymouslyAndRetrieveData()
.then(userCredential => {
console.log('anotherApp user ->', userCredential.user.toJSON());
.then(credential => {
if (credential) {
console.log('anotherApp credential ->', credential.user.toJSON());
}
});
});

View File

@ -1,6 +1,6 @@
import should from 'should';
function firestoreTests({ describe, it, context, firebase }) {
function firestoreTests({ describe, it, context, fcontext, firebase }) {
describe('firestore()', () => {
context('collection()', () => {
it('should create CollectionReference with the right id', () =>
@ -173,6 +173,80 @@ function firestoreTests({ describe, it, context, firebase }) {
}).should.throw('firebase.firestore().settings() coming soon');
});
});
context('runTransaction()', () => {
it('should set, update and delete transactionally and allow a return value', async () => {
let deleteMe = false;
const firestore = firebase.native.firestore();
const docRef = firestore
.collection('transactions')
.doc(Date.now().toString());
const updateFunction = async transaction => {
const doc = await transaction.get(docRef);
if (doc.exists && deleteMe) {
transaction.delete(docRef);
return 'bye';
}
if (!doc.exists) {
transaction.set(docRef, { value: 1 });
return 1;
}
const newValue = doc.data().value + 1;
if (newValue > 2) {
return Promise.reject(
new Error('Value should not be greater than 2!')
);
}
transaction.update(docRef, {
value: newValue,
somethingElse: 'update',
});
return newValue;
};
// set tests
const val1 = await firestore.runTransaction(updateFunction);
should.equal(val1, 1);
const doc1 = await docRef.get();
doc1.data().value.should.equal(1);
should.equal(doc1.data().somethingElse, undefined);
// update
const val2 = await firestore.runTransaction(updateFunction);
should.equal(val2, 2);
const doc2 = await docRef.get();
doc2.data().value.should.equal(2);
doc2.data().somethingElse.should.equal('update');
// rejecting / cancelling transaction
let didReject = false;
try {
await firestore.runTransaction(updateFunction);
} catch (e) {
didReject = true;
}
should.equal(didReject, true);
const doc3 = await docRef.get();
doc3.data().value.should.equal(2);
doc3.data().somethingElse.should.equal('update');
// delete
deleteMe = true;
const val4 = await firestore.runTransaction(updateFunction);
should.equal(val4, 'bye');
const doc4 = await docRef.get();
should.equal(doc4.exists, false);
return Promise.resolve('Test Completed');
});
});
});
}