Permalink
Browse files

Introduce dirty tracking to CMObjects for optimized network traffic use

  • Loading branch information...
1 parent a5bba5a commit d3adc9aaf6e5e58a6b48e9492e07e5d876f527fe Conrad Kramer committed with marcweil Jul 6, 2012
View
@@ -1,3 +1,7 @@
+v1.1
+* Track modifications made to objects and only send dirty objects back to server.
+
+
v0.2
* Remove erroneous user-related constructors from CMObject. All user-related things are on CMStore.
* Fix bug that would fail to remove private `__type__` fields from serialized NSDictonary objects.
@@ -38,6 +38,11 @@
@property (nonatomic, readonly) CMObjectOwnershipLevel ownershipLevel;
/**
+ * The object is dirty if any changes have been made locally that have not yet been persisted to the server.
+ */
+@property (readonly, getter = isDirty) BOOL dirty;
+
+/**
* Initializes this app-level object by generating a UUID as the default value for <tt>objectId</tt>.
*/
- (id)init;
@@ -10,10 +10,19 @@
#import "CMObject.h"
#import "NSString+UUID.h"
#import "CMObjectSerialization.h"
+#import "CMObjectDecoder.h"
+
+#import "MARTNSObject.h"
+#import "RTProperty.h"
+
+@interface CMObject ()
+@property (readwrite, getter = isDirty) BOOL dirty;
+@end
@implementation CMObject
@synthesize objectId;
@synthesize store;
+@synthesize dirty;
#pragma mark - Initializers
@@ -25,18 +34,59 @@ - (id)initWithObjectId:(NSString *)theObjectId {
if (self = [super init]) {
objectId = theObjectId;
store = nil;
+ dirty = YES;
+ [self registerAllPropertiesForKVO];
}
return self;
}
- (id)initWithCoder:(NSCoder *)aDecoder {
id deserializedObjectId = [aDecoder decodeObjectForKey:CMInternalObjectIdKey];
-
- if (![deserializedObjectId isKindOfClass:[NSString class]]) {
+ if (![deserializedObjectId isKindOfClass:[NSString class]])
deserializedObjectId = [deserializedObjectId stringValue];
+
+ if (self = [self initWithObjectId:deserializedObjectId]) {
+ if ([aDecoder isKindOfClass:[CMObjectDecoder class]]) {
+ dirty = NO;
+ }
}
+ return self;
+}
+
+- (void)dealloc {
+ [self deregisterAllPropertiesForKVO];
+}
- return [self initWithObjectId:deserializedObjectId];
+#pragma mark - Dirty tracking
+
+- (void)executeBlockForAllUserDefinedProperties:(void (^)(RTProperty *property))block {
+ NSArray *allProperties = [[self class] rt_properties];
+ NSArray *superclassProperties = [[CMObject class] rt_properties];
+ [allProperties enumerateObjectsUsingBlock:^(RTProperty *property, NSUInteger idx, BOOL *stop) {
+ if (![superclassProperties containsObject:property]) {
+ block(property);
+ }
+ }];
+}
+
+- (void)registerAllPropertiesForKVO {
+ [self executeBlockForAllUserDefinedProperties:^(RTProperty *property) {
+ [self addObserver:self forKeyPath:[property name] options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];
+ }];
+}
+
+- (void)deregisterAllPropertiesForKVO {
+ [self executeBlockForAllUserDefinedProperties:^(RTProperty *property) {
+ [self removeObserver:self forKeyPath:[property name]];
+ }];
+}
+
+- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
+ id oldValue = [change objectForKey:NSKeyValueChangeOldKey];
+ id newValue = [change objectForKey:NSKeyValueChangeNewKey];
+ if (![oldValue isEqual:newValue]) {
+ dirty = YES;
+ }
}
#pragma mark - Serialization
@@ -51,7 +101,7 @@ - (void)save:(CMStoreObjectUploadCallback)callback {
if ([self.store objectOwnershipLevel:self] == CMObjectOwnershipUndefinedLevel) {
[self.store addObject:self];
}
-
+
switch ([self.store objectOwnershipLevel:self]) {
case CMObjectOwnershipAppLevel:
[self.store saveObject:self callback:callback];
@@ -35,6 +35,10 @@
#pragma mark -
+@interface CMObject (Private)
+@property (getter = isDirty) BOOL dirty;
+@end
+
@interface CMStore ()
- (void)_allObjects:(CMStoreObjectFetchCallback)callback userLevel:(BOOL)userLevel additionalOptions:(CMStoreOptions *)options;
- (void)_allObjects:(CMStoreObjectFetchCallback)callback ofClass:(Class)klass userLevel:(BOOL)userLevel additionalOptions:(CMStoreOptions *)options;
@@ -352,7 +356,7 @@ - (void)saveUserObject:(CMObject *)theObject callback:(CMStoreObjectUploadCallba
- (void)saveUserObject:(CMObject *)theObject additionalOptions:(CMStoreOptions *)options callback:(CMStoreObjectUploadCallback)callback {
_CMAssertUserConfigured;
[self _ensureUserLoggedInWithCallback:^{
- [self _saveObjects:$set(theObject) userLevel:YES callback:callback additionalOptions:options];
+ [self _saveObjects:$array(theObject) userLevel:YES callback:callback additionalOptions:options];
}];
}
@@ -361,19 +365,33 @@ - (void)saveObject:(CMObject *)theObject callback:(CMStoreObjectUploadCallback)c
}
- (void)saveObject:(CMObject *)theObject additionalOptions:(CMStoreOptions *)options callback:(CMStoreObjectUploadCallback)callback {
- [self _saveObjects:$set(theObject) userLevel:NO callback:callback additionalOptions:options];
+ [self _saveObjects:$array(theObject) userLevel:NO callback:callback additionalOptions:options];
}
- (void)_saveObjects:(NSArray *)objects userLevel:(BOOL)userLevel callback:(CMStoreObjectUploadCallback)callback additionalOptions:(CMStoreOptions *)options {
NSParameterAssert(objects);
_CMAssertAPICredentialsInitialized;
[self cacheObjectsInMemory:objects atUserLevel:userLevel];
-
- [webService updateValuesFromDictionary:[CMObjectEncoder encodeObjects:objects]
+
+ NSMutableArray *cleanObjects = [NSMutableArray array];
+ NSMutableArray *dirtyObjects = [NSMutableArray array];
+ [objects enumerateObjectsUsingBlock:^(CMObject* obj, NSUInteger idx, BOOL *stop) {
+ obj.dirty ? [dirtyObjects addObject:obj] : [cleanObjects addObject:obj];
+ }];
+
+ // Only send the dirty objects to the servers
+ [webService updateValuesFromDictionary:[CMObjectEncoder encodeObjects:dirtyObjects]
serverSideFunction:_CMTryMethod(options, serverSideFunction)
user:_CMUserOrNil
extraParameters:_CMTryMethod(options, buildExtraParameters)
successHandler:^(NSDictionary *results, NSDictionary *errors, NSDictionary *meta, NSDictionary *snippetResult, NSNumber *count, NSDictionary *headers) {
+ // Add clean objects that were omitted from the request into the response as pseudo-updated
+ NSMutableDictionary *mutResults = [results mutableCopy];
+ [cleanObjects enumerateObjectsUsingBlock:^(CMObject *obj, NSUInteger idx, BOOL *stop) {
+ [mutResults setObject:@"updated" forKey:obj.objectId];
+ }];
+ results = [mutResults copy];
+
CMResponseMetadata *metadata = [[CMResponseMetadata alloc] initWithMetadata:meta];
CMSnippetResult *result = [[CMSnippetResult alloc] initWithData:snippetResult];
CMObjectUploadResponse *response = [[CMObjectUploadResponse alloc] initWithUploadStatuses:results snippetResult:result responseMetadata:metadata];
@@ -382,7 +400,15 @@ - (void)_saveObjects:(NSArray *)objects userLevel:(BOOL)userLevel callback:(CMSt
if (expirationDate && userLevel) {
user.tokenExpiration = expirationDate;
}
-
+
+ // If the dirty objects were successfully uploaded, mark them as clean
+ [dirtyObjects enumerateObjectsUsingBlock:^(CMObject *object, NSUInteger idx, BOOL *stop) {
+ NSString *status = [response.uploadStatuses objectForKey:object.objectId];
+ if ([status isEqualToString:@"updated"] || [status isEqualToString:@"created"]) {
+ object.dirty = NO;
+ }
+ }];
+
if (callback) {
callback(response);
}
@@ -11,9 +11,20 @@
#import "CMStore.h"
#import "CMNullStore.h"
#import "CMObject.h"
+#import "CMObjectDecoder.h"
+#import "CMObjectEncoder.h"
#import "CMAPICredentials.h"
#import "CMUser.h"
+@interface CustomObject : CMObject
+@property (nonatomic, retain) NSString *something;
+@property (nonatomic, retain) NSString *somethingElse;
+@end
+
+@implementation CustomObject
+@synthesize something, somethingElse;
+@end
+
SPEC_BEGIN(CMObjectSpec)
describe(@"CMObject", ^{
@@ -94,6 +105,63 @@
[[obj.store should] equal:[CMNullStore nullStore]];
});
});
+
+
+ context(@"given an object that is a custom subclass", ^{
+ __block CustomObject *object = nil;
+ __block CMStore *store = store;
+
+ beforeAll(^{
+ object = [[CustomObject alloc] init];
+ store = [CMStore defaultStore];
+ store.webService = [CMWebService nullMock];
+ });
+
+ it(@"should be dirty, seeing as I am the one who initialized it", ^{
+ [[theValue(object.dirty) should] beYes];
+ });
+
+ it(@"should become clean if it is encoded and then decoded with CMObjectDecoder", ^{
+ [[theValue(object.dirty) should] beYes];
+
+ // Encode and decode the object, and ensure it went correctly
+ NSString *objectId = object.objectId;
+ object = [[CMObjectDecoder decodeObjects:[CMObjectEncoder encodeObjects:[NSArray arrayWithObject:object]]] lastObject];
+ [[object.objectId should] equal:objectId];
+
+ // It should be clean, because it was decoded with CMObjectDecoder
+ [[theValue(object.dirty) should] beNo];
+ });
+
+
+ it(@"should become dirty if properties are changed and no other object changes have occured server-side", ^{
+ [[theValue(object.dirty) should] beNo];
+
+ // Changing the value of a property should make the object dirty
+ object.something = @"Something important!";
+
+ [[theValue(object.dirty) should] beYes];
+ });
+
+ it(@"should clean itself after it is successfully uploaded by CMStore", ^{
+ [[theValue(object.dirty) should] beYes];
+
+ // Prepare spy and wait for message
+ [[store should] receive:@selector(saveObject:additionalOptions:callback:)];
+ KWCaptureSpy *spy = [(KWMock *)store.webService captureArgument:@selector(updateValuesFromDictionary:serverSideFunction:user:extraParameters:successHandler:errorHandler:) atIndex:4];
+
+ [object save:^(CMObjectUploadResponse *response) { }];
+
+ // Fabricate a successful upload response
+ [object setValue:@"SomeIDReturnedByTheServer" forKey:@"objectId"];
+ CMWebServiceObjectFetchSuccessCallback callback = (CMWebServiceObjectFetchSuccessCallback)spy.argument;
+ callback([NSDictionary dictionaryWithObject:@"updated" forKey:object.objectId], nil, nil, nil, nil, nil);
+
+ // The object should be marked as clean
+ [[theValue(object.dirty) should] beNo];
+ });
+
+ });
});
SPEC_END

0 comments on commit d3adc9a

Please sign in to comment.