Skip to content

Commit

Permalink
Added NSIncrementalStore+Async helpers
Browse files Browse the repository at this point in the history
  • Loading branch information
martijnthe committed Mar 1, 2012
1 parent ec1698b commit 607fe42
Show file tree
Hide file tree
Showing 2 changed files with 294 additions and 0 deletions.
106 changes: 106 additions & 0 deletions IncrementalStoreTest/NSIncrementalStore+Async.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//
// NSIncrementalStore+Async.h
//
// Created by Martijn Thé on 3/1/12.
// Copyright (c) 2012 martijnthe.nl All rights reserved.
//

/**
* NSIncrementalStore+Async
*
* The goal of this set of helpers is to make it easy to write an NSIncrementalStore subclasses that
* have slow backing stores, e.g. web services or remote databases.
*
* It helps you by abstracting away the details of executing NSFetchRequests in the background, faulting
* the results back into the caller's context, etc.
*
* NSFetchRequest (and later NSAsyncSaveChangesRequest) are subclassed and decorated with the
* protocol NSAsyncIncrementalStoreRequest. This protocol provides additional properties and methods like
* willStartBlock, didFinishBlock, -cancel, queue, etc.
* These 'async' requests return immediately when executed, either returning cached objects that match the request
* or an empty array, depending on the returnsCachedResultsImmediately property of the request and the store's cache itself.
*
*
* How-to
*
* In your NSIncrementalStore subclass, do all blocking work inside -executeRequestBlocking:withContext:error:
* (or rename your current -executeRequest:withContext:error: if you already had one).
* Avoid doing blocking work inside -newValuesForObjectWithID:withContext:error:, it is assumed you have already loaded
* the data in -executeRequestBlocking:.. and have cached the values.
* Call -executeRequestAsyncAndSync:withContext:error: inside the required -executeRequest:withContext:error: method.
* Just pass the arguments and return the result directly.
*
* Then use NSAsyncFetchRequest whereever you want to fetch objects asynchronically.
* Optionally set it's additional properties like didFinishBlock, returnsCachedResultsImmediately, etc.
* Optionally implement the NSIncrementalStoreExecuteRequestCached and NSIncrementalStoreRequestQueuing protocols on the store.
*
*
* Assumptions about your incremental store subclass
*
* - All blocking work is done inside -executeRequestBlocking:withContext:error:
* - Other required methods like, -newValuesForObjectWithID:withContext:error:, should not be blocking
* - Your incremental store needs to be able to handle fetch requests with resultType NSManagedObjectIDResultType.
*
*
* Todos:
*
* - Handle save requests
* - Add documentation to the methods, properties, etc.
* - Respect resultType of request when calling the didFinishBlock
* - Add a convenience method to dynamically replace the -executeRequest: with -executeRequestAsyncAndSync: and add -executeRequestBlocking:
* - Optimize the performance of the process of faulting objects into the caller's context
* - Add a block handler to report the progress?
*
**/


#import <CoreData/CoreData.h>

#pragma mark - General stuff, errors, etc.

extern NSString* NSAsyncIncrementalStoreErrorDomain;

typedef enum {
NSAsyncIncrementalStoreErrorUnimplemented,
NSAsyncIncrementalStoreErrorCancelled,
NSAsyncIncrementalStoreErrorExecutingRequest
} NSAsyncIncrementalStoreError;



#pragma mark - NSPersistentStoreRequest helpers, protocols, templates, etc.

@protocol NSAsyncIncrementalStoreRequest <NSObject>
@required // perhaps make some @optional later on
@property (nonatomic, readwrite, strong) void(^willStartBlock)(void);
@property (nonatomic, readwrite, strong) void(^didFinishBlock)(id results, NSError *error);
@property (nonatomic, readwrite, assign) BOOL returnsCachedResultsImmediately; // should default to YES
@property (nonatomic, readwrite, strong) NSOperationQueue* queue; // if nil, the -queueForRequest: will be called on the store
@property (atomic, readonly, assign) BOOL isCancelled;
- (void)cancel;
@end

@interface NSAsyncFetchRequest : NSFetchRequest <NSAsyncIncrementalStoreRequest> @end
//@interface NSAsyncSaveChangesRequest : NSSaveChangesRequest <NSAsyncIncrementalStoreRequest> @end // TODO



#pragma mark - NSIncrementalStore helpers, protocols, templates, etc.

@interface NSIncrementalStore (Async)
- (id)executeRequestAsyncAndSync:(NSPersistentStoreRequest*)request withContext:(NSManagedObjectContext*)context error:(NSError**)error;
@end

@protocol NSIncrementalStoreExecuteRequestBlocking <NSObject>
@required
- (id)executeRequestBlocking:(NSPersistentStoreRequest*)request withContext:(NSManagedObjectContext*)context error:(NSError**)error;
@end

@protocol NSIncrementalStoreExecuteRequestCached <NSObject>
@required
- (id)executeRequestCached:(NSPersistentStoreRequest*)request withContext:(NSManagedObjectContext*)context error:(NSError**)error;
@end

@protocol NSIncrementalStoreRequestQueuing <NSObject>
- (NSOperationQueue*)queueForRequest:(NSPersistentStoreRequest*)request; // if nil or not implemented, a new queue will be used for each request
@end
188 changes: 188 additions & 0 deletions IncrementalStoreTest/NSIncrementalStore+Async.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
//
// NSIncrementalStore+Async.m
//
// Created by Martijn Thé on 3/1/12.
// Copyright (c) 2012 martijnthe.nl All rights reserved.
//

#import "NSIncrementalStore+Async.h"

NSString* NSAsyncIncrementalStoreErrorDomain = @"nl.martijnthe.asyncincrementalstore";

@implementation NSAsyncFetchRequest
@synthesize willStartBlock;
@synthesize didFinishBlock;
@synthesize returnsCachedResultsImmediately;
@synthesize queue;
@synthesize isCancelled;

- (void)cancel {
@synchronized(self) {
isCancelled = YES;
}
}

@end


#pragma mark - NSIncrementalStore helpers, protocols, templates, etc.

@implementation NSIncrementalStore (Async)

#define callDidFinishBlock(callerQueue, asyncRequest, results, error) \
if (asyncRequest.didFinishBlock) { \
NSOperationQueue* _callerQueue = callerQueue; \
if (_callerQueue) { \
[_callerQueue addOperationWithBlock:^{ \
asyncRequest.didFinishBlock(results, error); \
asyncRequest.didFinishBlock = nil; \
}]; \
} else { \
asyncRequest.didFinishBlock(results, error); \
asyncRequest.didFinishBlock = nil; \
} \
}

- (id)executeRequestAsyncAndSync:(NSPersistentStoreRequest*)request withContext:(NSManagedObjectContext*)callerMoc error:(NSError**)error {
if ([request conformsToProtocol:@protocol(NSAsyncIncrementalStoreRequest)] == NO) {
// request is not an async request, just go down the regular path:
return [(id<NSIncrementalStoreExecuteRequestBlocking>)self executeRequestBlocking:request withContext:callerMoc error:error];
} else {
NSPersistentStoreRequest<NSAsyncIncrementalStoreRequest>* asyncRequest = (NSPersistentStoreRequest<NSAsyncIncrementalStoreRequest>*)request;

// Keep a reference to the original queue, we want to run the handler ('delegate') blocks on this queue.
NSOperationQueue* callerQueue = [NSOperationQueue currentQueue];

if (asyncRequest.isCancelled) {
NSError* cancelError = [NSError errorWithDomain:NSAsyncIncrementalStoreErrorDomain code:NSAsyncIncrementalStoreErrorCancelled userInfo:nil];
if (error) {
*error = cancelError;
}
callDidFinishBlock(nil, asyncRequest, nil, cancelError);
return nil;
}

// We're dealing with an async request,
// so 2 things need to be done: 1) execute the request on a queue, 2) return cached results immediately (optionally)

// 1)
// Figure out which queue to use by cascading:
NSOperationQueue* requestQueue;
if (asyncRequest.queue) {
requestQueue = asyncRequest.queue;
}
if (requestQueue == nil && [self conformsToProtocol:@protocol(NSIncrementalStoreRequestQueuing)]) {
requestQueue = [(id<NSIncrementalStoreRequestQueuing>)self queueForRequest:asyncRequest];
}
if (requestQueue == nil) {
requestQueue = [[NSOperationQueue alloc] init];
}

[requestQueue addOperationWithBlock:^{

// Check if cancelled:
if (asyncRequest.isCancelled) {
callDidFinishBlock(callerQueue, asyncRequest, nil, [NSError errorWithDomain:NSAsyncIncrementalStoreErrorDomain code:NSAsyncIncrementalStoreErrorCancelled userInfo:nil]);
return;
}

if (asyncRequest.willStartBlock) {
[callerQueue addOperationWithBlock:^{
asyncRequest.willStartBlock();
asyncRequest.willStartBlock = nil;
}];
}

// Prepare background request + moc:
NSManagedObjectContext* bgMoc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
// bgMoc.parentContext = self.moc; // <-- this will cause a call executeRequest:withContext:error: to the store on the CALLER's (MAIN) THREAD... :-S
bgMoc.persistentStoreCoordinator = callerMoc.persistentStoreCoordinator;
NSPersistentStoreRequest<NSAsyncIncrementalStoreRequest>* bgRequest;
switch (asyncRequest.requestType) {
case NSFetchRequestType: {
NSFetchRequest<NSAsyncIncrementalStoreRequest>* bgFetchRequest = [asyncRequest copy];
bgFetchRequest.entity = [NSEntityDescription entityForName:bgFetchRequest.entityName inManagedObjectContext:bgMoc];
bgFetchRequest.resultType = NSManagedObjectIDResultType; // get objectIDs, we fault the objects into the callerMoc later on
bgRequest = bgFetchRequest;
break;
}
case NSSaveRequestType: {

// TODO:
// NSSaveChangesRequests contain references to the inserted, updated and deleted objects that need to be saved.
// We cannot use these objects on this queue (thread/queue confinement).
// Faulting them into this context is not going to work either, because then they won't contain the changes.
// So at some point in time, the changed objects faulted into the bg context and
// their changed properties need to be copied from the objects in the caller moc.

// break; // for now, fall-thru
}
default: {
// Finish with 'unimplemented' error:
callDidFinishBlock(callerQueue, asyncRequest, nil, [NSError errorWithDomain:NSAsyncIncrementalStoreErrorDomain code:NSAsyncIncrementalStoreErrorUnimplemented userInfo:nil]);
return;
}
}

// Finally, run the (blocking) request:
NSError* executeError = nil;
NSArray* objectIDs = [(id<NSIncrementalStoreExecuteRequestBlocking>)self executeRequestBlocking:bgRequest withContext:callerMoc error:error];

// Handle executeError:
if (objectIDs == nil) {
NSDictionary* userInfo = nil;
if (error) {
userInfo = [NSDictionary dictionaryWithObjectsAndKeys:executeError, NSUnderlyingErrorKey, nil];
}
NSError* wrappedError = [NSError errorWithDomain:NSAsyncIncrementalStoreErrorDomain code:NSAsyncIncrementalStoreErrorExecutingRequest userInfo:userInfo];
callDidFinishBlock(callerQueue, asyncRequest, nil, wrappedError);
return;
}

// Check if cancelled:
if (asyncRequest.isCancelled) {
callDidFinishBlock(callerQueue, asyncRequest, nil, [NSError errorWithDomain:NSAsyncIncrementalStoreErrorDomain code:NSAsyncIncrementalStoreErrorCancelled userInfo:nil]);
return;
}

// Fault objects into callerMoc & refresh objects:
[callerQueue addOperationWithBlock:^{
// At this point in time, the newly fetched objects are not registered in callerMoc, but only in bgMoc

NSError* lookupError = nil;
NSManagedObject* mObject;
NSMutableArray* results = [NSMutableArray arrayWithCapacity:[objectIDs count]];
for (NSManagedObjectID *objectID in objectIDs) {
// -existingObjectWithID:error: causes the object to "register" in the callerMoc (-managedObjectContextDidRegisterObjectsWithIDs: is also called on the IncrementalStore).
// It also fires the fault, which is important, because the objects like NSFetchedResultsController will filter the registeredObjects in-memory based on its the fetchRequest.
mObject = [callerMoc existingObjectWithID:objectID error:&lookupError];
// Interestingly, even though existingObjectWithID:error: will cause objects to be "registered" in the context, its does not cause NSManagedObjectContextObjectsDidChangeNotifications to fire...
if (mObject) {
// -refreshObject:mergeChanges: trigger the NSManagedObjectContextObjectsDidChangeNotification which drives the change-monitoring features of objects like NSFetchedResultsController
// NOTE: I suspect this could be optimized to avoid triggering change notifications for objects that were already in the callerMoc AND have not been changed in the mean while.
[callerMoc refreshObject:mObject mergeChanges:YES];
[results addObject:mObject];
} else {
NSLog(@"Error trying to look up object in moc: %@", lookupError);
}
}

// TODO: respect the resultType of a fetch request (now we're always returning managed objects)

// Done without errors:
callDidFinishBlock(nil, asyncRequest, results, nil);
}];
}];

// 2)
// If cache is requested immediately and the store implements the method to do that, return cache:
if (asyncRequest.returnsCachedResultsImmediately && [self conformsToProtocol:@protocol(NSIncrementalStoreExecuteRequestCached)]) {
return [(id<NSIncrementalStoreExecuteRequestCached>)self executeRequestCached:request withContext:callerMoc error:error];
} else {
// Else, always return an empty array, otherwise an error will be assumed by the caller:
return [NSArray array];
}
}
}

@end

0 comments on commit 607fe42

Please sign in to comment.