Skip to content

Commit b395e26

Browse files
authored
Merge pull request #215 from FrankSalad/v3-transaction
[v3] [ios] transaction
2 parents 63fcf4b + 5a735ab commit b395e26

File tree

6 files changed

+145
-3
lines changed

6 files changed

+145
-3
lines changed

Diff for: docs/api/database.md

+8
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ firestack.database()
3636
```
3737
Useful for `orderByPriority` queries.
3838

39+
40+
Transaction Support:
41+
```javascript
42+
firestack.database()
43+
.ref('posts/1234/title')
44+
.transaction((title) => 'My Awesome Post');
45+
```
46+
3947
## Unmounted components
4048

4149
Listening to database updates on unmounted components will trigger a warning:

Diff for: ios/Firestack/FirestackDatabase.h

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
}
1919

2020
@property NSMutableDictionary *dbReferences;
21+
@property NSMutableDictionary *transactions;
22+
@property dispatch_queue_t transactionQueue;
2123

2224
@end
2325

Diff for: ios/Firestack/FirestackDatabase.m

+85-3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ @interface FirestackDBReference : NSObject
2121
@property FIRDatabaseHandle childRemovedHandler;
2222
@property FIRDatabaseHandle childMovedHandler;
2323
@property FIRDatabaseHandle childValueHandler;
24+
+ (NSDictionary *) snapshotToDict:(FIRDataSnapshot *) snapshot;
25+
2426
@end
2527

2628
@implementation FirestackDBReference
@@ -52,7 +54,7 @@ - (void) addEventHandler:(NSString *) eventName
5254
{
5355
if (![self isListeningTo:eventName]) {
5456
id withBlock = ^(FIRDataSnapshot * _Nonnull snapshot) {
55-
NSDictionary *props = [self snapshotToDict:snapshot];
57+
NSDictionary *props = [FirestackDBReference snapshotToDict:snapshot];
5658
[self sendJSEvent:DATABASE_DATA_EVENT
5759
title:eventName
5860
props: @{
@@ -142,7 +144,7 @@ - (void) removeEventHandler:(NSString *) name
142144
[self unsetListeningOn:name];
143145
}
144146

145-
- (NSDictionary *) snapshotToDict:(FIRDataSnapshot *) snapshot
147+
+ (NSDictionary *) snapshotToDict:(FIRDataSnapshot *) snapshot
146148
{
147149
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
148150
[dict setValue:snapshot.key forKey:@"key"];
@@ -377,6 +379,8 @@ - (id) init
377379
self = [super init];
378380
if (self != nil) {
379381
_dbReferences = [[NSMutableDictionary alloc] init];
382+
_transactions = [[NSMutableDictionary alloc] init];
383+
_transactionQueue = dispatch_queue_create("com.fullstackreact.react-native-firestack", DISPATCH_QUEUE_CONCURRENT);
380384
}
381385
return self;
382386
}
@@ -479,7 +483,85 @@ - (id) init
479483
}
480484
}
481485

486+
RCT_EXPORT_METHOD(beginTransaction:(NSString *) path
487+
withIdentifier:(NSString *) identifier
488+
applyLocally:(BOOL) applyLocally
489+
onComplete:(RCTResponseSenderBlock) onComplete)
490+
{
491+
dispatch_async(_transactionQueue, ^{
492+
NSMutableDictionary *transactionState = [NSMutableDictionary new];
493+
494+
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
495+
[transactionState setObject:sema forKey:@"semaphore"];
496+
497+
FIRDatabaseReference *ref = [self getPathRef:path];
498+
[ref runTransactionBlock:^FIRTransactionResult * _Nonnull(FIRMutableData * _Nonnull currentData) {
499+
dispatch_barrier_async(_transactionQueue, ^{
500+
[_transactions setValue:transactionState forKey:identifier];
501+
[self sendEventWithName:DATABASE_TRANSACTION_EVENT
502+
body:@{
503+
@"id": identifier,
504+
@"originalValue": currentData.value
505+
}];
506+
});
507+
// Wait for the event handler to call tryCommitTransaction
508+
// WARNING: This wait occurs on the Firebase Worker Queue
509+
// so if tryCommitTransaction fails to signal the semaphore
510+
// no further blocks will be executed by Firebase until the timeout expires
511+
dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC);
512+
BOOL timedout = dispatch_semaphore_wait(sema, delayTime) != 0;
513+
BOOL abort = [transactionState valueForKey:@"abort"] || timedout;
514+
id value = [transactionState valueForKey:@"value"];
515+
dispatch_barrier_async(_transactionQueue, ^{
516+
[_transactions removeObjectForKey:identifier];
517+
});
518+
if (abort) {
519+
return [FIRTransactionResult abort];
520+
} else {
521+
currentData.value = value;
522+
return [FIRTransactionResult successWithValue:currentData];
523+
}
524+
} andCompletionBlock:^(NSError * _Nullable databaseError, BOOL committed, FIRDataSnapshot * _Nullable snapshot) {
525+
if (databaseError != nil) {
526+
NSDictionary *evt = @{
527+
@"errorCode": [NSNumber numberWithInt:[databaseError code]],
528+
@"errorDetails": [databaseError debugDescription],
529+
@"description": [databaseError description]
530+
};
531+
onComplete(@[evt]);
532+
} else {
533+
onComplete(@[[NSNull null], @{
534+
@"committed": [NSNumber numberWithBool:committed],
535+
@"snapshot": [FirestackDBReference snapshotToDict:snapshot],
536+
@"status": @"success",
537+
@"method": @"transaction"
538+
}]);
539+
}
540+
} withLocalEvents:applyLocally];
541+
});
542+
}
482543

544+
RCT_EXPORT_METHOD(tryCommitTransaction:(NSString *) identifier
545+
withData:(NSDictionary *) data
546+
orAbort:(BOOL) abort)
547+
{
548+
__block NSMutableDictionary *transactionState;
549+
dispatch_sync(_transactionQueue, ^{
550+
transactionState = [_transactions objectForKey: identifier];
551+
});
552+
if (!transactionState) {
553+
NSLog(@"tryCommitTransaction for unknown ID %@", identifier);
554+
return;
555+
}
556+
dispatch_semaphore_t sema = [transactionState valueForKey:@"semaphore"];
557+
if (abort) {
558+
[transactionState setValue:@true forKey:@"abort"];
559+
} else {
560+
id newValue = [data valueForKey:@"value"];
561+
[transactionState setValue:newValue forKey:@"value"];
562+
}
563+
dispatch_semaphore_signal(sema);
564+
}
483565

484566
RCT_EXPORT_METHOD(on:(NSString *) path
485567
modifiersString:(NSString *) modifiersString
@@ -634,7 +716,7 @@ - (NSString *) getDBListenerKey:(NSString *) path
634716

635717
// Not sure how to get away from this... yet
636718
- (NSArray<NSString *> *)supportedEvents {
637-
return @[DATABASE_DATA_EVENT, DATABASE_ERROR_EVENT];
719+
return @[DATABASE_DATA_EVENT, DATABASE_ERROR_EVENT, DATABASE_TRANSACTION_EVENT];
638720
}
639721

640722

Diff for: ios/Firestack/FirestackEvents.h

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ static NSString *const DEBUG_EVENT = @"debug";
2929
// Database
3030
static NSString *const DATABASE_DATA_EVENT = @"database_event";
3131
static NSString *const DATABASE_ERROR_EVENT = @"database_error";
32+
static NSString *const DATABASE_TRANSACTION_EVENT = @"database_transaction_update";
3233

3334
static NSString *const DATABASE_VALUE_EVENT = @"value";
3435
static NSString *const DATABASE_CHILD_ADDED_EVENT = @"child_added";

Diff for: lib/modules/database/index.js

+36
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ export default class Database extends Base {
1919
constructor(firestack: Object, options: Object = {}) {
2020
super(firestack, options);
2121
this.subscriptions = {};
22+
23+
this.transactions = {};
2224
this.errorSubscriptions = {};
25+
2326
this.serverTimeOffset = 0;
2427
this.persistenceEnabled = false;
2528
this.namespace = 'firestack:database';
@@ -34,6 +37,11 @@ export default class Database extends Base {
3437
err => this._handleDatabaseError(err)
3538
);
3639

40+
this.transactionListener = FirestackDatabaseEvt.addListener(
41+
'database_transaction_update',
42+
event => this._handleDatabaseTransaction(event)
43+
);
44+
3745
this.offsetRef = this.ref('.info/serverTimeOffset');
3846

3947
this.offsetRef.on('value', (snapshot) => {
@@ -164,6 +172,34 @@ export default class Database extends Base {
164172
FirestackDatabase.goOffline();
165173
}
166174

175+
addTransaction(path, updateCallback, applyLocally) {
176+
let id = this._generateTransactionID();
177+
this.transactions[id] = updateCallback;
178+
return promisify('beginTransaction', FirestackDatabase)(path, id, applyLocally || false)
179+
.then((v) => {delete this.transactions[id]; return v;},
180+
(e) => {delete this.transactions[id]; throw e;});
181+
}
182+
183+
_generateTransactionID() {
184+
// 10 char random alphanumeric
185+
return Math.random().toString(36).substr(2, 10);
186+
}
187+
188+
_handleDatabaseTransaction(event) {
189+
const {id, originalValue} = event;
190+
let newValue;
191+
try {
192+
const updateCallback = this.transactions[id];
193+
newValue = updateCallback(originalValue);
194+
} finally {
195+
let abort = false;
196+
if (newValue === undefined) {
197+
abort = true;
198+
}
199+
FirestackDatabase.tryCommitTransaction(id, {value: newValue}, abort);
200+
}
201+
}
202+
167203
/**
168204
* INTERNALS
169205
*/

Diff for: lib/modules/database/reference.js

+13
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,19 @@ export default class Reference extends ReferenceBase {
137137
return this.db.off(path, modifiersString, eventName, origCB);
138138
}
139139

140+
transaction(transactionUpdate, onComplete, applyLocally) {
141+
const path = this._dbPath();
142+
return this.db.addTransaction(path, transactionUpdate, applyLocally)
143+
.then((({ snapshot, committed }) => {return {snapshot: new Snapshot(this, snapshot), committed}}).bind(this))
144+
.then(({ snapshot, committed }) => {
145+
if (isFunction(onComplete)) onComplete(null, snapshot);
146+
return {snapshot, committed};
147+
}).catch((e) => {
148+
if (isFunction(onComplete)) return onComplete(e, null);
149+
throw e;
150+
});
151+
}
152+
140153
/**
141154
* MODIFIERS
142155
*/

0 commit comments

Comments
 (0)