From 15e49a38a5f949674497332e64752263367a9537 Mon Sep 17 00:00:00 2001 From: Francisco Saldana Date: Fri, 6 Jan 2017 00:09:37 -0600 Subject: [PATCH 1/8] Add transaction support to Database Reference. --- ios/Firestack/FirestackDatabase.h | 1 + ios/Firestack/FirestackDatabase.m | 54 ++++++++++++++++++++++++++++++- ios/Firestack/FirestackEvents.h | 1 + lib/modules/database/index.js | 37 +++++++++++++++++++++ lib/modules/database/reference.js | 5 +++ 5 files changed, 97 insertions(+), 1 deletion(-) diff --git a/ios/Firestack/FirestackDatabase.h b/ios/Firestack/FirestackDatabase.h index 7659f4d..24eb8e4 100644 --- a/ios/Firestack/FirestackDatabase.h +++ b/ios/Firestack/FirestackDatabase.h @@ -18,6 +18,7 @@ } @property NSMutableDictionary *dbReferences; +@property NSMutableDictionary *transactions; @end diff --git a/ios/Firestack/FirestackDatabase.m b/ios/Firestack/FirestackDatabase.m index bdb10fa..7596be3 100644 --- a/ios/Firestack/FirestackDatabase.m +++ b/ios/Firestack/FirestackDatabase.m @@ -364,6 +364,7 @@ - (id) init self = [super init]; if (self != nil) { _dbReferences = [[NSMutableDictionary alloc] init]; + _transactions = [[NSMutableDictionary alloc] init]; } return self; } @@ -455,7 +456,58 @@ - (id) init } } +RCT_EXPORT_METHOD(beginTransaction:(NSString *) path + withIdentifier:(NSString *) identifier + applyLocally:(BOOL) applyLocally + onComplete:(RCTResponseSenderBlock) onComplete) +{ + NSMutableDictionary *transactionState = [NSMutableDictionary new]; + [_transactions setValue:transactionState forKey:identifier]; + dispatch_semaphore_t sema = dispatch_semaphore_create(0); + [transactionState setObject:sema forKey:@"semaphore"]; + + FIRDatabaseReference *ref = [self getPathRef:path]; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [ref runTransactionBlock:^FIRTransactionResult * _Nonnull(FIRMutableData * _Nonnull currentData) { + [self sendEventWithName:DATABASE_TRANSACTION_EVENT + body:@{ + @"id": identifier, + @"originalValue": currentData.value + }]; + // Wait for the event handler to call tryCommitTransaction + dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); + BOOL abort = [transactionState valueForKey:@"abort"]; + id value = [transactionState valueForKey:@"value"]; + [_transactions removeObjectForKey:identifier]; + if (abort) { + return [FIRTransactionResult abort]; + } else { + currentData.value = value; + return [FIRTransactionResult successWithValue:currentData]; + } + } andCompletionBlock:^(NSError * _Nullable error, BOOL committed, FIRDataSnapshot * _Nullable snapshot) { + [self handleCallback:@"transaction" callback:onComplete databaseError:error]; + } withLocalEvents:applyLocally]; + }); +} +RCT_EXPORT_METHOD(tryCommitTransaction:(NSString *) identifier + withData:(NSDictionary *) data + orAbort:(BOOL) abort) +{ + NSMutableDictionary *transactionState = [_transactions valueForKey:identifier]; + if (!transactionState) { + NSLog(@"tryCommitTransaction for unknown ID %@", identifier); + } + 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 @@ -610,7 +662,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 ba976ef..1ea449f 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 f4477e7..8463a08 100644 --- a/lib/modules/database/index.js +++ b/lib/modules/database/index.js @@ -19,6 +19,7 @@ export default class Database extends Base { constructor(firestack: Object, options: Object = {}) { super(firestack, options); this.subscriptions = {}; + this.transactions = {}; this.serverTimeOffset = 0; this.persistenceEnabled = false; this.namespace = 'firestack:database'; @@ -33,6 +34,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) => { this.serverTimeOffset = snapshot.val() || this.serverTimeOffset; @@ -153,6 +159,37 @@ export default class Database extends Base { FirestackDatabase.goOffline(); } + addTransaction(path, updateCallback, applyLocally, onComplete) { + let id = this._generateTransactionID(); + this.transactions[id] = updateCallback; + return new Promise((resolve, reject) => { + FirestackDatabase.beginTransaction(path, id, applyLocally || false, (error, result) => { + onComplete && onComplete(error); + if (error) + reject(error); + else + resolve(); + delete this.transactions[id]; + }); + }); + } + + _generateTransactionID() { + // 10 char random alphanumeric + return Math.random().toString(36).substr(2, 10); + } + + _handleDatabaseTransaction(event) { + const {id, originalValue} = event; + const updateCallback = this.transactions[id]; + const newValue = updateCallback(originalValue); + 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 3471383..caf4edd 100644 --- a/lib/modules/database/reference.js +++ b/lib/modules/database/reference.js @@ -123,6 +123,11 @@ 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, onComplete); + } + /** * MODIFIERS */ From dba8c11d15e9ac92efde92bb77e7ccaf8f85037f Mon Sep 17 00:00:00 2001 From: Francisco Saldana Date: Fri, 6 Jan 2017 00:24:33 -0600 Subject: [PATCH 2/8] Resolve promise and send callback appropriate result. --- ios/Firestack/FirestackDatabase.m | 25 ++++++++++++++++++++----- lib/modules/database/index.js | 14 ++++++++++---- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/ios/Firestack/FirestackDatabase.m b/ios/Firestack/FirestackDatabase.m index 7596be3..9c67095 100644 --- a/ios/Firestack/FirestackDatabase.m +++ b/ios/Firestack/FirestackDatabase.m @@ -21,6 +21,7 @@ @interface FirestackDBReference : NSObject @property FIRDatabaseHandle childRemovedHandler; @property FIRDatabaseHandle childMovedHandler; @property FIRDatabaseHandle childValueHandler; ++ (NSDictionary *) snapshotToDict:(FIRDataSnapshot *) snapshot; @end @implementation FirestackDBReference @@ -46,7 +47,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: @{ @@ -74,7 +75,7 @@ - (void) addSingleEventHandler:(RCTResponseSenderBlock) callback { [_query observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) { - NSDictionary *props = [self snapshotToDict:snapshot]; + NSDictionary *props = [FirestackDBReference snapshotToDict:snapshot]; callback(@[[NSNull null], @{ @"eventName": @"value", @"path": _path, @@ -131,7 +132,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"]; @@ -485,8 +486,22 @@ - (id) init currentData.value = value; return [FIRTransactionResult successWithValue:currentData]; } - } andCompletionBlock:^(NSError * _Nullable error, BOOL committed, FIRDataSnapshot * _Nullable snapshot) { - [self handleCallback:@"transaction" callback:onComplete databaseError:error]; + } 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]; }); } diff --git a/lib/modules/database/index.js b/lib/modules/database/index.js index 8463a08..b3e840b 100644 --- a/lib/modules/database/index.js +++ b/lib/modules/database/index.js @@ -164,11 +164,17 @@ export default class Database extends Base { this.transactions[id] = updateCallback; return new Promise((resolve, reject) => { FirestackDatabase.beginTransaction(path, id, applyLocally || false, (error, result) => { - onComplete && onComplete(error); - if (error) + let snapshot; + if (result.snapshot) + snapshot = new Snapshot(new Reference(this, path.split('/'), null), result.snapshot); + onComplete && onComplete(error, snapshot); + if (error) { reject(error); - else - resolve(); + } + else { + let {committed} = result; + resolve({committed, snapshot}); + } delete this.transactions[id]; }); }); From 69fb950eff0f3be066777bc5f8928c8f5a74c7b1 Mon Sep 17 00:00:00 2001 From: Francisco Saldana Date: Fri, 6 Jan 2017 01:33:47 -0600 Subject: [PATCH 3/8] Cleaner handling chain. --- lib/modules/database/index.js | 21 ++++----------------- lib/modules/database/reference.js | 10 +++++++++- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/lib/modules/database/index.js b/lib/modules/database/index.js index b3e840b..06579be 100644 --- a/lib/modules/database/index.js +++ b/lib/modules/database/index.js @@ -159,25 +159,12 @@ export default class Database extends Base { FirestackDatabase.goOffline(); } - addTransaction(path, updateCallback, applyLocally, onComplete) { + addTransaction(path, updateCallback, applyLocally) { let id = this._generateTransactionID(); this.transactions[id] = updateCallback; - return new Promise((resolve, reject) => { - FirestackDatabase.beginTransaction(path, id, applyLocally || false, (error, result) => { - let snapshot; - if (result.snapshot) - snapshot = new Snapshot(new Reference(this, path.split('/'), null), result.snapshot); - onComplete && onComplete(error, snapshot); - if (error) { - reject(error); - } - else { - let {committed} = result; - resolve({committed, snapshot}); - } - delete this.transactions[id]; - }); - }); + return promisify('beginTransaction', FirestackDatabase)(path, id, applyLocally || false) + .then((v) => {delete this.transactions[id]; return v;}, + (e) => {delete this.transactions[id]; throw e;}); } _generateTransactionID() { diff --git a/lib/modules/database/reference.js b/lib/modules/database/reference.js index caf4edd..530690b 100644 --- a/lib/modules/database/reference.js +++ b/lib/modules/database/reference.js @@ -125,7 +125,15 @@ export default class Reference extends ReferenceBase { transaction(transactionUpdate, onComplete, applyLocally) { const path = this._dbPath(); - return this.db.addTransaction(path, transactionUpdate, applyLocally, onComplete); + return this.db.addTransaction(path, transactionUpdate, applyLocally) + .then(({ snapshot, committed }) => {snapshot: new Snapshot(this, snapshot), committed}) + .then(({ snapshot, committed }) => { + if (isFunction(onComplete)) onComplete(null, snapshot); + return {snapshot, committed}; + }).catch((e) => { + if (isFunction(onComplete)) return onComplete(e, null); + throw e; + }); } /** From db3f0159f0787b25780490902720e902e6cd506b Mon Sep 17 00:00:00 2001 From: Francisco Saldana Date: Fri, 6 Jan 2017 03:40:39 -0600 Subject: [PATCH 4/8] Concurrent transaction queue, serialize access to _transactions MutableDictionary. --- ios/Firestack/FirestackDatabase.h | 1 + ios/Firestack/FirestackDatabase.m | 39 ++++++++++++++++++++----------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/ios/Firestack/FirestackDatabase.h b/ios/Firestack/FirestackDatabase.h index 24eb8e4..9f27ff5 100644 --- a/ios/Firestack/FirestackDatabase.h +++ b/ios/Firestack/FirestackDatabase.h @@ -19,6 +19,7 @@ @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 9c67095..c0f24d5 100644 --- a/ios/Firestack/FirestackDatabase.m +++ b/ios/Firestack/FirestackDatabase.m @@ -22,6 +22,7 @@ @interface FirestackDBReference : NSObject @property FIRDatabaseHandle childMovedHandler; @property FIRDatabaseHandle childValueHandler; + (NSDictionary *) snapshotToDict:(FIRDataSnapshot *) snapshot; + @end @implementation FirestackDBReference @@ -366,6 +367,7 @@ - (id) 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; } @@ -462,24 +464,29 @@ - (id) init applyLocally:(BOOL) applyLocally onComplete:(RCTResponseSenderBlock) onComplete) { - NSMutableDictionary *transactionState = [NSMutableDictionary new]; - [_transactions setValue:transactionState forKey:identifier]; - dispatch_semaphore_t sema = dispatch_semaphore_create(0); - [transactionState setObject:sema forKey:@"semaphore"]; - - FIRDatabaseReference *ref = [self getPathRef:path]; - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + 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) { - [self sendEventWithName:DATABASE_TRANSACTION_EVENT - body:@{ - @"id": identifier, - @"originalValue": currentData.value - }]; + 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 dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); BOOL abort = [transactionState valueForKey:@"abort"]; id value = [transactionState valueForKey:@"value"]; - [_transactions removeObjectForKey:identifier]; + dispatch_barrier_async(_transactionQueue, ^{ + [_transactions removeObjectForKey:identifier]; + }); if (abort) { return [FIRTransactionResult abort]; } else { @@ -510,9 +517,13 @@ - (id) init withData:(NSDictionary *) data orAbort:(BOOL) abort) { - NSMutableDictionary *transactionState = [_transactions valueForKey:identifier]; + __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) { From 1dc491101ab22d72bd02587e53c61f53280c199b Mon Sep 17 00:00:00 2001 From: Francisco Saldana Date: Sat, 7 Jan 2017 20:03:43 -0600 Subject: [PATCH 5/8] Add to readme --- docs/api/database.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/api/database.md b/docs/api/database.md index 54594f1..2d31183 100644 --- a/docs/api/database.md +++ b/docs/api/database.md @@ -25,6 +25,13 @@ firestack.database() }); ``` +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: From 12a3f1cd8cd8975a96d2f674f7980cf6071fc1cf Mon Sep 17 00:00:00 2001 From: Francisco Saldana Date: Sat, 7 Jan 2017 20:25:12 -0600 Subject: [PATCH 6/8] Wrap update function in try block. --- lib/modules/database/index.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/modules/database/index.js b/lib/modules/database/index.js index 06579be..42b3041 100644 --- a/lib/modules/database/index.js +++ b/lib/modules/database/index.js @@ -174,13 +174,17 @@ export default class Database extends Base { _handleDatabaseTransaction(event) { const {id, originalValue} = event; - const updateCallback = this.transactions[id]; - const newValue = updateCallback(originalValue); - let abort = false; - if (newValue === undefined) { - abort = true; + 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); } - FirestackDatabase.tryCommitTransaction(id, {value: newValue}, abort); } /** From 5d31027be68b2fa74c5c04bb20ac0ab8c50c16c6 Mon Sep 17 00:00:00 2001 From: Francisco Saldana Date: Sat, 7 Jan 2017 23:54:49 -0600 Subject: [PATCH 7/8] Fix error 'TypeError: Cannot read property 'snapshot' of undefined' --- lib/modules/database/reference.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/database/reference.js b/lib/modules/database/reference.js index 530690b..b306365 100644 --- a/lib/modules/database/reference.js +++ b/lib/modules/database/reference.js @@ -126,7 +126,7 @@ export default class Reference extends ReferenceBase { transaction(transactionUpdate, onComplete, applyLocally) { const path = this._dbPath(); return this.db.addTransaction(path, transactionUpdate, applyLocally) - .then(({ snapshot, committed }) => {snapshot: new Snapshot(this, snapshot), committed}) + .then((({ snapshot, committed }) => {return {snapshot: new Snapshot(this, snapshot), committed}}).bind(this)) .then(({ snapshot, committed }) => { if (isFunction(onComplete)) onComplete(null, snapshot); return {snapshot, committed}; From 8fe95f28c4d7524bde7887241dc7c0188a86b07d Mon Sep 17 00:00:00 2001 From: Francisco Saldana Date: Sun, 8 Jan 2017 10:19:26 -0600 Subject: [PATCH 8/8] Add timeout --- ios/Firestack/FirestackDatabase.m | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ios/Firestack/FirestackDatabase.m b/ios/Firestack/FirestackDatabase.m index c0f24d5..f605a01 100644 --- a/ios/Firestack/FirestackDatabase.m +++ b/ios/Firestack/FirestackDatabase.m @@ -481,8 +481,12 @@ - (id) init }]; }); // Wait for the event handler to call tryCommitTransaction - dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); - BOOL abort = [transactionState valueForKey:@"abort"]; + // 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]; 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