diff --git a/docs/api/database.md b/docs/api/database.md index c019356..7145d71 100644 --- a/docs/api/database.md +++ b/docs/api/database.md @@ -36,6 +36,14 @@ firestack.database() ``` Useful for `orderByPriority` queries. + +Transaction Support: +```javascript +firestack.database() + .ref('posts/1234/title') + .transaction((title) => 'My Awesome Post'); +``` + ## Unmounted components Listening to database updates on unmounted components will trigger a warning: diff --git a/ios/Firestack/FirestackDatabase.h b/ios/Firestack/FirestackDatabase.h index 77aa8f3..850a36a 100644 --- a/ios/Firestack/FirestackDatabase.h +++ b/ios/Firestack/FirestackDatabase.h @@ -18,6 +18,8 @@ } @property NSMutableDictionary *dbReferences; +@property NSMutableDictionary *transactions; +@property dispatch_queue_t transactionQueue; @end diff --git a/ios/Firestack/FirestackDatabase.m b/ios/Firestack/FirestackDatabase.m index 314500e..5cd833d 100644 --- a/ios/Firestack/FirestackDatabase.m +++ b/ios/Firestack/FirestackDatabase.m @@ -21,6 +21,8 @@ @interface FirestackDBReference : NSObject @property FIRDatabaseHandle childRemovedHandler; @property FIRDatabaseHandle childMovedHandler; @property FIRDatabaseHandle childValueHandler; ++ (NSDictionary *) snapshotToDict:(FIRDataSnapshot *) snapshot; + @end @implementation FirestackDBReference @@ -52,7 +54,7 @@ - (void) addEventHandler:(NSString *) eventName { if (![self isListeningTo:eventName]) { id withBlock = ^(FIRDataSnapshot * _Nonnull snapshot) { - NSDictionary *props = [self snapshotToDict:snapshot]; + NSDictionary *props = [FirestackDBReference snapshotToDict:snapshot]; [self sendJSEvent:DATABASE_DATA_EVENT title:eventName props: @{ @@ -142,7 +144,7 @@ - (void) removeEventHandler:(NSString *) name [self unsetListeningOn:name]; } -- (NSDictionary *) snapshotToDict:(FIRDataSnapshot *) snapshot ++ (NSDictionary *) snapshotToDict:(FIRDataSnapshot *) snapshot { NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; [dict setValue:snapshot.key forKey:@"key"]; @@ -377,6 +379,8 @@ - (id) init self = [super init]; if (self != nil) { _dbReferences = [[NSMutableDictionary alloc] init]; + _transactions = [[NSMutableDictionary alloc] init]; + _transactionQueue = dispatch_queue_create("com.fullstackreact.react-native-firestack", DISPATCH_QUEUE_CONCURRENT); } return self; } @@ -479,7 +483,85 @@ - (id) init } } +RCT_EXPORT_METHOD(beginTransaction:(NSString *) path + withIdentifier:(NSString *) identifier + applyLocally:(BOOL) applyLocally + onComplete:(RCTResponseSenderBlock) onComplete) +{ + dispatch_async(_transactionQueue, ^{ + NSMutableDictionary *transactionState = [NSMutableDictionary new]; + + dispatch_semaphore_t sema = dispatch_semaphore_create(0); + [transactionState setObject:sema forKey:@"semaphore"]; + + FIRDatabaseReference *ref = [self getPathRef:path]; + [ref runTransactionBlock:^FIRTransactionResult * _Nonnull(FIRMutableData * _Nonnull currentData) { + dispatch_barrier_async(_transactionQueue, ^{ + [_transactions setValue:transactionState forKey:identifier]; + [self sendEventWithName:DATABASE_TRANSACTION_EVENT + body:@{ + @"id": identifier, + @"originalValue": currentData.value + }]; + }); + // Wait for the event handler to call tryCommitTransaction + // WARNING: This wait occurs on the Firebase Worker Queue + // so if 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:identifier]; + }); + if (abort) { + return [FIRTransactionResult abort]; + } else { + currentData.value = value; + return [FIRTransactionResult successWithValue:currentData]; + } + } andCompletionBlock:^(NSError * _Nullable databaseError, BOOL committed, FIRDataSnapshot * _Nullable snapshot) { + if (databaseError != nil) { + NSDictionary *evt = @{ + @"errorCode": [NSNumber numberWithInt:[databaseError code]], + @"errorDetails": [databaseError debugDescription], + @"description": [databaseError description] + }; + onComplete(@[evt]); + } else { + onComplete(@[[NSNull null], @{ + @"committed": [NSNumber numberWithBool:committed], + @"snapshot": [FirestackDBReference snapshotToDict:snapshot], + @"status": @"success", + @"method": @"transaction" + }]); + } + } withLocalEvents:applyLocally]; + }); +} +RCT_EXPORT_METHOD(tryCommitTransaction:(NSString *) identifier + withData:(NSDictionary *) data + orAbort:(BOOL) abort) +{ + __block NSMutableDictionary *transactionState; + dispatch_sync(_transactionQueue, ^{ + transactionState = [_transactions objectForKey: identifier]; + }); + if (!transactionState) { + NSLog(@"tryCommitTransaction for unknown ID %@", identifier); + return; + } + dispatch_semaphore_t sema = [transactionState valueForKey:@"semaphore"]; + if (abort) { + [transactionState setValue:@true forKey:@"abort"]; + } else { + id newValue = [data valueForKey:@"value"]; + [transactionState setValue:newValue forKey:@"value"]; + } + dispatch_semaphore_signal(sema); +} RCT_EXPORT_METHOD(on:(NSString *) path modifiersString:(NSString *) modifiersString @@ -634,7 +716,7 @@ - (NSString *) getDBListenerKey:(NSString *) path // Not sure how to get away from this... yet - (NSArray *)supportedEvents { - return @[DATABASE_DATA_EVENT, DATABASE_ERROR_EVENT]; + return @[DATABASE_DATA_EVENT, DATABASE_ERROR_EVENT, DATABASE_TRANSACTION_EVENT]; } diff --git a/ios/Firestack/FirestackEvents.h b/ios/Firestack/FirestackEvents.h index fd6b01d..68ead8b 100644 --- a/ios/Firestack/FirestackEvents.h +++ b/ios/Firestack/FirestackEvents.h @@ -29,6 +29,7 @@ static NSString *const DEBUG_EVENT = @"debug"; // Database static NSString *const DATABASE_DATA_EVENT = @"database_event"; static NSString *const DATABASE_ERROR_EVENT = @"database_error"; +static NSString *const DATABASE_TRANSACTION_EVENT = @"database_transaction_update"; static NSString *const DATABASE_VALUE_EVENT = @"value"; static NSString *const DATABASE_CHILD_ADDED_EVENT = @"child_added"; diff --git a/lib/modules/database/index.js b/lib/modules/database/index.js index e117648..f3eecdd 100644 --- a/lib/modules/database/index.js +++ b/lib/modules/database/index.js @@ -19,7 +19,10 @@ export default class Database extends Base { constructor(firestack: Object, options: Object = {}) { super(firestack, options); this.subscriptions = {}; + + this.transactions = {}; this.errorSubscriptions = {}; + this.serverTimeOffset = 0; this.persistenceEnabled = false; this.namespace = 'firestack:database'; @@ -34,6 +37,11 @@ export default class Database extends Base { err => this._handleDatabaseError(err) ); + this.transactionListener = FirestackDatabaseEvt.addListener( + 'database_transaction_update', + event => this._handleDatabaseTransaction(event) + ); + this.offsetRef = this.ref('.info/serverTimeOffset'); this.offsetRef.on('value', (snapshot) => { @@ -164,6 +172,34 @@ export default class Database extends Base { FirestackDatabase.goOffline(); } + addTransaction(path, updateCallback, applyLocally) { + let id = this._generateTransactionID(); + this.transactions[id] = updateCallback; + return promisify('beginTransaction', FirestackDatabase)(path, id, applyLocally || false) + .then((v) => {delete this.transactions[id]; return v;}, + (e) => {delete this.transactions[id]; throw e;}); + } + + _generateTransactionID() { + // 10 char random alphanumeric + return Math.random().toString(36).substr(2, 10); + } + + _handleDatabaseTransaction(event) { + const {id, originalValue} = event; + let newValue; + try { + const updateCallback = this.transactions[id]; + newValue = updateCallback(originalValue); + } finally { + let abort = false; + if (newValue === undefined) { + abort = true; + } + FirestackDatabase.tryCommitTransaction(id, {value: newValue}, abort); + } + } + /** * INTERNALS */ diff --git a/lib/modules/database/reference.js b/lib/modules/database/reference.js index e74b5aa..adc6952 100644 --- a/lib/modules/database/reference.js +++ b/lib/modules/database/reference.js @@ -137,6 +137,19 @@ export default class Reference extends ReferenceBase { return this.db.off(path, modifiersString, eventName, origCB); } + transaction(transactionUpdate, onComplete, applyLocally) { + const path = this._dbPath(); + return this.db.addTransaction(path, transactionUpdate, applyLocally) + .then((({ snapshot, committed }) => {return {snapshot: new Snapshot(this, snapshot), committed}}).bind(this)) + .then(({ snapshot, committed }) => { + if (isFunction(onComplete)) onComplete(null, snapshot); + return {snapshot, committed}; + }).catch((e) => { + if (isFunction(onComplete)) return onComplete(e, null); + throw e; + }); + } + /** * MODIFIERS */ pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy