Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v3] [ios] transaction #215

Merged
merged 10 commits into from
Jan 26, 2017
8 changes: 8 additions & 0 deletions docs/api/database.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions ios/Firestack/FirestackDatabase.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
}

@property NSMutableDictionary *dbReferences;
@property NSMutableDictionary *transactions;
@property dispatch_queue_t transactionQueue;

@end

Expand Down
88 changes: 85 additions & 3 deletions ios/Firestack/FirestackDatabase.m
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ @interface FirestackDBReference : NSObject
@property FIRDatabaseHandle childRemovedHandler;
@property FIRDatabaseHandle childMovedHandler;
@property FIRDatabaseHandle childValueHandler;
+ (NSDictionary *) snapshotToDict:(FIRDataSnapshot *) snapshot;

@end

@implementation FirestackDBReference
Expand Down Expand Up @@ -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: @{
Expand Down Expand Up @@ -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"];
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -634,7 +716,7 @@ - (NSString *) getDBListenerKey:(NSString *) path

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


Expand Down
1 change: 1 addition & 0 deletions ios/Firestack/FirestackEvents.h
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
36 changes: 36 additions & 0 deletions lib/modules/database/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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) => {
Expand Down Expand Up @@ -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
*/
Expand Down
13 changes: 13 additions & 0 deletions lib/modules/database/reference.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down