Rollback in the kingdom

I use Realm in my recent project - and I'm struggling to find an elegant solution to the problem that I have. To maintain responsiveness of the user interface, I try to act in most operations instantly. The entire user interface is disconnected from RLMResults, so in order to act something, I need to change the objects in Realm.

The fact is that these seemingly instant changes (I think the preference in the Twitter application) may fail, and I must undo the changes that I made if this happens.

I use ReactiveCocoa throughout the application, so continuing the explanation with my preferred example, I assume that for this I need something like the following:

- (RACSignal *)favoriteTweet:(Tweet *)tweet {
    RACSignal *favoriteOnDatabase = [RACSignal createSignal:^RACDisposable *(id <RACSubscriber> *subscriber) {
        [self.realm beginWriteTransaction];
        tweet.favorited = YES;
        [self.realm commitWriteTransaction];

        [subscriber sendNext: tweet];
        [subscriber sendCompleted];

        return nil;
    }];

    RACSignal *syncChangesToAPI = [[[self.apiClient favoriteTweet:tweet] ignoreValues]
                                    onError:^(NSError *error) {
                                        [self.realm beginWriteTransaction];
                                        // Do rollback here. How?
                                        [self.realm commitWriteTransaction];
                                    }]
                                    catch:^RACSignal *(NSError *error) {
                                        return [RACSignal createSignal:^RACDisposable *(id <RACSubscriber> subscriber) {
                                            [subscriber sendNext: tweet]; // Any changes have already been rolled back at this point.
                                            [subscriber sendError: error];

                                            return nil;
                                        }];
                                    }];

    return [favoriteOnDatabase concat: syncChangesToAPI];
}

:

Favorited tweet - [Error in API request] -> Unfavorited tweet -> Error

Realm AFAIK, , , , , favoriteOnDatabase, Realm, - , .


, , , , - , . ( ?)

/*
    Important: 
    - This method only works as intended with changes on the main properties on the object given. (i.e., the object is copied `shallowly` for rollback purposes)
    - This method breaks any existing explicit relations to the object in rollback if the changeBlock deletes an object.

    - [DataManager autoRollbackSyncSignalWithObject:apiSignal:changeBlock] is for changes in persisted objects that needs instant UI response instead of showing a loading indicator while syncing the change with the API.

    This signal sends values in following order:
    - An updated object with the requested changes made. (changeBlock(object))

    ...after this value send, the network request to sync the change is made:
    - If the operation is successful, it does not send any new values and completes the signal.
    - If the operation is unsuccessful, it rolls back the changes and sends the original value and the error object.
 */
- (RACSignal *)autoRollbackSyncSignalWithObject:(RLMObject *)object
                                      apiSignal:(RACSignal *)apiSignal
                                    changeBlock:(DataManagerAutoRollbackSyncSignalChangeBlock)changeBlock {
    RLMObject *originalObject = [object two_shallowCopy];
    __block RLMObject *modifiedObject = object;

    RACSignal *localChange = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        [self.realm beginWriteTransaction];
        changeBlock(modifiedObject);
        [self.realm commitWriteTransaction];

        [subscriber sendNext:modifiedObject];
        [subscriber sendCompleted];

        return nil;
    }];

    RACSignal *syncOperation = [[[apiSignal ignoreValues] doError:^(NSError *error) {
        [self.realm beginWriteTransaction];
        /*
            Rollback.
        */
        if (object.invalidated) { // If the object was deleted in the change block
            modifiedObject = originalObject;
            [self.realm addObject:modifiedObject];
        } else if (!originalObject && modifiedObject) { // If an object was created in the change block
            [self.realm deleteObject:modifiedObject];
        } else { // If the object was modified in the change block
            [modifiedObject two_mergePropertiesFromObject:originalObject];
        }
        [self.realm commitWriteTransaction];
    }] catch:^RACSignal *(NSError *error) {
        return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
            [subscriber sendNext:modifiedObject];
            [subscriber sendError:error];

            return nil;
        }];
    }];

    return [localChange concat:syncOperation];
}

? - , ?

!

+4

All Articles