#import "RNFirebaseDatabase.h" #if __has_include() #import #import "RNFirebaseDatabaseReference.h" #import "RNFirebaseEvents.h" @implementation RNFirebaseDatabase RCT_EXPORT_MODULE(); // TODO document methods - (id)init { self = [super init]; if (self != nil) { _dbReferences = [[NSMutableDictionary alloc] init]; _transactions = [[NSMutableDictionary alloc] init]; _transactionQueue = dispatch_queue_create("io.invertase.react-native-firebase", DISPATCH_QUEUE_CONCURRENT); } return self; } RCT_EXPORT_METHOD(goOnline:(NSString *) appName) { [[RNFirebaseDatabase getDatabaseForApp:appName] goOnline]; } RCT_EXPORT_METHOD(goOffline:(NSString *) appName) { [[RNFirebaseDatabase getDatabaseForApp:appName] goOffline]; } RCT_EXPORT_METHOD(setPersistence:(NSString *) appName state:(BOOL) state) { [RNFirebaseDatabase getDatabaseForApp:appName].persistenceEnabled = state; } RCT_EXPORT_METHOD(keepSynced:(NSString *) appName key:(NSString *) key path:(NSString *) path modifiers:(NSArray *) modifiers state:(BOOL) state) { FIRDatabaseQuery *query = [self getInternalReferenceForApp:appName key:key path:path modifiers:modifiers keep:false].query; [query keepSynced:state]; } RCT_EXPORT_METHOD(transactionTryCommit:(NSString *) appName transactionId:(nonnull NSNumber *) transactionId updates:(NSDictionary *) updates) { __block NSMutableDictionary *transactionState; dispatch_sync(_transactionQueue, ^{ transactionState = _transactions[transactionId]; }); if (!transactionState) { NSLog(@"tryCommitTransaction for unknown ID %@", transactionId); return; } dispatch_semaphore_t sema = [transactionState valueForKey:@"semaphore"]; BOOL abort = [[updates valueForKey:@"abort"] boolValue]; if (abort) { [transactionState setValue:@true forKey:@"abort"]; } else { id newValue = [updates valueForKey:@"value"]; [transactionState setValue:newValue forKey:@"value"]; } dispatch_semaphore_signal(sema); } RCT_EXPORT_METHOD(transactionStart:(NSString *) appName path:(NSString *) path transactionId:(nonnull NSNumber *) transactionId applyLocally:(BOOL) applyLocally) { dispatch_async(_transactionQueue, ^{ NSMutableDictionary *transactionState = [NSMutableDictionary new]; dispatch_semaphore_t sema = dispatch_semaphore_create(0); transactionState[@"semaphore"] = sema; FIRDatabaseReference *ref = [self getReferenceForAppPath:appName path:path]; [ref runTransactionBlock:^FIRTransactionResult * _Nonnull(FIRMutableData * _Nonnull currentData) { dispatch_barrier_async(_transactionQueue, ^{ [_transactions setValue:transactionState forKey:transactionId]; NSDictionary *updateMap = [self createTransactionUpdateMap:appName transactionId:transactionId updatesData:currentData]; // TODO: Temporary fix for https://github.com/invertase/react-native-firebase/issues/233 // until a better solution comes around if (self.bridge) { [self sendEventWithName:DATABASE_TRANSACTION_EVENT body:updateMap]; } }); // wait for the js event handler to call tryCommitTransaction // this wait occurs on the Firebase Worker Queue // so if the tryCommitTransaction fails to signal the semaphore // no further blocks will be executed by Firebase until the timeout expires dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC); BOOL timedout = dispatch_semaphore_wait(sema, delayTime) != 0; BOOL abort = [transactionState valueForKey:@"abort"] || timedout; id value = [transactionState valueForKey:@"value"]; dispatch_barrier_async(_transactionQueue, ^{ [_transactions removeObjectForKey:transactionId]; }); if (abort) { return [FIRTransactionResult abort]; } else { currentData.value = value; return [FIRTransactionResult successWithValue:currentData]; } } andCompletionBlock: ^(NSError *_Nullable databaseError, BOOL committed, FIRDataSnapshot *_Nullable snapshot) { NSDictionary *resultMap = [self createTransactionResultMap:appName transactionId:transactionId error:databaseError committed:committed snapshot:snapshot]; // TODO: Temporary fix for https://github.com/invertase/react-native-firebase/issues/233 // until a better solution comes around if (self.bridge) { [self sendEventWithName:DATABASE_TRANSACTION_EVENT body:resultMap]; } } withLocalEvents: applyLocally]; }); } RCT_EXPORT_METHOD(onDisconnectSet:(NSString *) appName path:(NSString *) path props:(NSDictionary *) props resolver:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject) { FIRDatabaseReference *ref = [self getReferenceForAppPath:appName path:path]; [ref onDisconnectSetValue:props[@"value"] withCompletionBlock:^(NSError *_Nullable error, FIRDatabaseReference *_Nonnull _ref) { [RNFirebaseDatabase handlePromise:resolve rejecter:reject databaseError:error]; }]; } RCT_EXPORT_METHOD(onDisconnectUpdate:(NSString *) appName path:(NSString *) path props:(NSDictionary *) props resolver:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject) { FIRDatabaseReference *ref = [self getReferenceForAppPath:appName path:path]; [ref onDisconnectUpdateChildValues:props withCompletionBlock:^(NSError *_Nullable error, FIRDatabaseReference *_Nonnull _ref) { [RNFirebaseDatabase handlePromise:resolve rejecter:reject databaseError:error]; }]; } RCT_EXPORT_METHOD(onDisconnectRemove:(NSString *) appName path:(NSString *) path resolver:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject) { FIRDatabaseReference *ref = [self getReferenceForAppPath:appName path:path]; [ref onDisconnectRemoveValueWithCompletionBlock:^(NSError *_Nullable error, FIRDatabaseReference *_Nonnull _ref) { [RNFirebaseDatabase handlePromise:resolve rejecter:reject databaseError:error]; }]; } RCT_EXPORT_METHOD(onDisconnectCancel:(NSString *) appName path:(NSString *) path resolver:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject) { FIRDatabaseReference *ref = [self getReferenceForAppPath:appName path:path]; [ref cancelDisconnectOperationsWithCompletionBlock:^(NSError *_Nullable error, FIRDatabaseReference *_Nonnull _ref) { [RNFirebaseDatabase handlePromise:resolve rejecter:reject databaseError:error]; }]; } RCT_EXPORT_METHOD(set:(NSString *) appName path:(NSString *) path props:(NSDictionary *) props resolver:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject) { FIRDatabaseReference *ref = [self getReferenceForAppPath:appName path:path]; [ref setValue:[props valueForKey:@"value"] withCompletionBlock:^(NSError *_Nullable error, FIRDatabaseReference *_Nonnull _ref) { [RNFirebaseDatabase handlePromise:resolve rejecter:reject databaseError:error]; }]; } RCT_EXPORT_METHOD(setPriority:(NSString *) appName path:(NSString *) path priority:(NSDictionary *) priority resolver:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject) { FIRDatabaseReference *ref = [self getReferenceForAppPath:appName 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 *) appName path:(NSString *) path data:(NSDictionary *) data priority:(NSDictionary *) priority resolver:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject) { FIRDatabaseReference *ref = [self getReferenceForAppPath:appName path:path]; [ref setValue:[data valueForKey:@"value"] andPriority:[priority valueForKey:@"value"] withCompletionBlock:^(NSError *_Nullable error, FIRDatabaseReference *_Nonnull ref) { [RNFirebaseDatabase handlePromise:resolve rejecter:reject databaseError:error]; }]; } RCT_EXPORT_METHOD(update:(NSString *) appName path:(NSString *) path props:(NSDictionary *) props resolver:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject) { FIRDatabaseReference *ref = [self getReferenceForAppPath:appName path:path]; [ref updateChildValues:props withCompletionBlock:^(NSError *_Nullable error, FIRDatabaseReference *_Nonnull _ref) { [RNFirebaseDatabase handlePromise:resolve rejecter:reject databaseError:error]; }]; } RCT_EXPORT_METHOD(remove:(NSString *) appName path:(NSString *) path resolver:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject) { FIRDatabaseReference *ref = [self getReferenceForAppPath:appName path:path]; [ref removeValueWithCompletionBlock:^(NSError *_Nullable error, FIRDatabaseReference *_Nonnull _ref) { [RNFirebaseDatabase handlePromise:resolve rejecter:reject databaseError:error]; }]; } RCT_EXPORT_METHOD(once:(NSString *) appName key:(NSString *) key path:(NSString *) path modifiers:(NSArray *) modifiers eventName:(NSString *) eventName resolver:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject) { RNFirebaseDatabaseReference *ref = [self getInternalReferenceForApp:appName key:key path:path modifiers:modifiers keep:false]; [ref once:eventName resolver:resolve rejecter:reject]; } RCT_EXPORT_METHOD(on:(NSString *) appName props:(NSDictionary *) props) { RNFirebaseDatabaseReference *ref = [self getInternalReferenceForApp:appName key:props[@"key"] path:props[@"path"] modifiers:props[@"modifiers"] keep:false]; [ref on:props[@"eventType"] registration:props[@"registration"]]; } RCT_EXPORT_METHOD(off:(NSString *) key eventRegistrationKey:(NSString *) eventRegistrationKey) { RNFirebaseDatabaseReference *ref = _dbReferences[key]; [ref removeEventListener:eventRegistrationKey]; if (![ref hasListeners]) { [_dbReferences removeObjectForKey:key]; } } /* * INTERNALS/UTILS */ + (void)handlePromise:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject databaseError:(NSError *)databaseError { if (databaseError != nil) { NSDictionary *jsError = [RNFirebaseDatabase getJSError:databaseError]; reject([jsError valueForKey:@"code"], [jsError valueForKey:@"message"], databaseError); } else { resolve([NSNull null]); } } + (FIRDatabase *)getDatabaseForApp:(NSString *)appName { FIRApp *app = [FIRApp appNamed:appName]; return [FIRDatabase databaseForApp:app]; } - (FIRDatabaseReference *)getReferenceForAppPath:(NSString *)appName path:(NSString *)path { return [[RNFirebaseDatabase getDatabaseForApp:appName] referenceWithPath:path]; } - (RNFirebaseDatabaseReference *)getInternalReferenceForApp:(NSString *)appName key:(NSString *)key path:(NSString *)path modifiers:(NSArray *)modifiers keep:(BOOL)keep { RNFirebaseDatabaseReference *ref = _dbReferences[key]; if (ref == nil) { ref = [[RNFirebaseDatabaseReference alloc] initWithPathAndModifiers:self app:appName key:key refPath:path modifiers:modifiers]; if (keep) { _dbReferences[key] = ref; } } return ref; } // TODO: Move to error util for use in other modules + (NSString *)getMessageWithService:(NSString *)message service:(NSString *)service fullCode:(NSString *)fullCode { return [NSString stringWithFormat:@"%@: %@ (%@).", service, message, [fullCode lowercaseString]]; } + (NSString *)getCodeWithService:(NSString *)service code:(NSString *)code { return [NSString stringWithFormat:@"%@/%@", [service lowercaseString], [code lowercaseString]]; } + (NSDictionary *)getJSError:(NSError *)nativeError { NSMutableDictionary *errorMap = [[NSMutableDictionary alloc] init]; [errorMap setValue:@(nativeError.code) forKey:@"nativeErrorCode"]; [errorMap setValue:[nativeError localizedDescription] forKey:@"nativeErrorMessage"]; NSString *code; NSString *message; NSString *service = @"Database"; switch (nativeError.code) { // iOS confirmed codes case 1: // -3 on Android code = [RNFirebaseDatabase getCodeWithService:service code:@"permission-denied"]; message = [RNFirebaseDatabase getMessageWithService:@"Client doesn't have permission to access the desired data." service:service fullCode:code]; break; case 2: // -10 on Android code = [RNFirebaseDatabase getCodeWithService:service code:@"unavailable"]; message = [RNFirebaseDatabase getMessageWithService:@"The service is unavailable." service:service fullCode:code]; break; case 3: // -25 on Android code = [RNFirebaseDatabase getCodeWithService:service code:@"write-cancelled"]; message = [RNFirebaseDatabase getMessageWithService:@"The write was canceled by the user." service:service fullCode:code]; break; // 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]; break; case -2: code = [RNFirebaseDatabase getCodeWithService:service code:@"failure"]; message = [RNFirebaseDatabase getMessageWithService:@"The server indicated that this operation failed." service:service fullCode:code]; break; case -4: code = [RNFirebaseDatabase getCodeWithService:service code:@"disconnected"]; message = [RNFirebaseDatabase getMessageWithService:@"The operation had to be aborted due to a network disconnect." service:service fullCode:code]; break; case -6: code = [RNFirebaseDatabase getCodeWithService:service code:@"expired-token"]; message = [RNFirebaseDatabase getMessageWithService:@"The supplied auth token has expired." service:service fullCode:code]; break; case -7: code = [RNFirebaseDatabase getCodeWithService:service code:@"invalid-token"]; message = [RNFirebaseDatabase getMessageWithService:@"The supplied auth token was invalid." service:service fullCode:code]; break; case -8: code = [RNFirebaseDatabase getCodeWithService:service code:@"max-retries"]; message = [RNFirebaseDatabase getMessageWithService:@"The transaction had too many retries." service:service fullCode:code]; break; case -9: code = [RNFirebaseDatabase getCodeWithService:service code:@"overridden-by-set"]; message = [RNFirebaseDatabase getMessageWithService:@"The transaction was overridden by a subsequent set." service:service fullCode:code]; break; case -11: code = [RNFirebaseDatabase getCodeWithService:service code:@"user-code-exception"]; message = [RNFirebaseDatabase getMessageWithService:@"User code called from the Firebase Database runloop threw an exception." service:service fullCode:code]; break; case -24: code = [RNFirebaseDatabase getCodeWithService:service code:@"network-error"]; message = [RNFirebaseDatabase getMessageWithService:@"The operation could not be performed due to a network error." service:service fullCode:code]; break; default: code = [RNFirebaseDatabase getCodeWithService:service code:@"unknown"]; message = [RNFirebaseDatabase getMessageWithService:@"An unknown error occurred." service:service fullCode:code]; break; } [errorMap setValue:code forKey:@"code"]; [errorMap setValue:message forKey:@"message"]; return errorMap; } - (NSDictionary *)createTransactionUpdateMap:(NSString *)appName transactionId:(NSNumber *)transactionId updatesData:(FIRMutableData *)updatesData { NSMutableDictionary *updatesMap = [[NSMutableDictionary alloc] init]; [updatesMap setValue:transactionId forKey:@"id"]; [updatesMap setValue:@"update" forKey:@"type"]; [updatesMap setValue:appName forKey:@"appName"]; [updatesMap setValue:updatesData.value forKey:@"value"]; return updatesMap; } - (NSDictionary *)createTransactionResultMap:(NSString *)appName transactionId:(NSNumber *)transactionId error:(NSError *)error committed:(BOOL)committed snapshot:(FIRDataSnapshot *)snapshot { NSMutableDictionary *resultMap = [[NSMutableDictionary alloc] init]; [resultMap setValue:transactionId forKey:@"id"]; [resultMap setValue:appName forKey:@"appName"]; // TODO: no timeout on iOS [resultMap setValue:@(committed) forKey:@"committed"]; // TODO: no interrupted on iOS if (error != nil) { [resultMap setValue:@"error" forKey:@"type"]; [resultMap setValue:[RNFirebaseDatabase getJSError:error] forKey:@"error"]; // TODO: timeout error on iOS } else { [resultMap setValue:@"complete" forKey:@"type"]; [resultMap setValue:[RNFirebaseDatabaseReference snapshotToDict:snapshot] forKey:@"snapshot"]; } return resultMap; } - (NSArray *)supportedEvents { return @[DATABASE_SYNC_EVENT, DATABASE_TRANSACTION_EVENT]; } + (BOOL)requiresMainQueueSetup { return YES; } @end #else @implementation RNFirebaseDatabase @end #endif