Skip to content
This repository has been archived by the owner on Mar 9, 2022. It is now read-only.

Commit

Permalink
Added CouchLiveQuery
Browse files Browse the repository at this point in the history
CouchLiveQuery's .rows property is a live result set -- the instance watches for changes to the database, re-runs the query, and posts KVO notifications to .rows when the results change.
Updated the demo apps (particularly DemoQuery) to use this, which simplifies their code.
  • Loading branch information
snej committed Aug 1, 2011
1 parent 5091e94 commit 38f6a6a
Show file tree
Hide file tree
Showing 7 changed files with 162 additions and 63 deletions.
27 changes: 22 additions & 5 deletions Couch/CouchQuery.h
Expand Up @@ -14,11 +14,8 @@
// and limitations under the License.

#import "CouchResource.h"
@class CouchDatabase;
@class CouchDocument;
@class CouchDesignDocument;
@class CouchQueryEnumerator;
@class CouchQueryRow;
@class CouchDatabase, CouchDocument, CouchDesignDocument;
@class CouchLiveQuery, CouchQueryEnumerator, CouchQueryRow;


/** Represents a CouchDB 'view', or a view-like resource like _all_documents. */
Expand Down Expand Up @@ -72,6 +69,26 @@
/** Same as -rows, except returns nil if the query results have not changed since the last time it was evaluated (Synchronous). */
- (CouchQueryEnumerator*) rowsIfChanged;


/** Returns a live query with the same parameters. */
- (CouchLiveQuery*) asLiveQuery;

@end


/** A CouchQuery subclass that automatically refreshes the result rows every time the database changes.
All you need to do is watch for changes to the .rows property. */
@interface CouchLiveQuery : CouchQuery
{
@private
BOOL _observing;
RESTOperation* _op;
CouchQueryEnumerator* _rows;
}

/** In CouchLiveQuery the -rows accessor is now a non-blocking property that can be observed using KVO. Its value will be nil until the initial query finishes. */
@property (readonly, retain) CouchQueryEnumerator* rows;

@end


Expand Down
109 changes: 108 additions & 1 deletion Couch/CouchQuery.m
Expand Up @@ -35,6 +35,22 @@ - (id) initWithQuery: (CouchQuery*)query result: (id)result;
@implementation CouchQuery


- (id) initWithQuery: (CouchQuery*)query {
self = [super initWithParent: query.parent relativePath: query.relativePath];
if (self) {
_limit = query.limit;
_skip = query.skip;
self.startKey = query.startKey;
self.endKey = query.endKey;
_descending = query.descending;
_prefetch = query.prefetch;
self.keys = query.keys;
_groupLevel = query.groupLevel;
}
return self;
}


@synthesize limit=_limit, skip=_skip, descending=_descending, startKey=_startKey, endKey=_endKey,
prefetch=_prefetch, keys=_keys, groupLevel=_groupLevel;

Expand Down Expand Up @@ -116,6 +132,11 @@ - (NSError*) operation: (RESTOperation*)op willCompleteWithError: (NSError*)erro
}


- (CouchLiveQuery*) asLiveQuery {
return [[[CouchLiveQuery alloc] initWithQuery: self] autorelease];
}


@end


Expand Down Expand Up @@ -154,6 +175,82 @@ - (NSDictionary*) jsonToPost {



@implementation CouchLiveQuery

- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver: self];
[_op release];
[super dealloc];
}


- (CouchQueryEnumerator*) rows {
if (!_observing)
[self start];
return _rows;
}


- (void) setRows:(CouchQueryEnumerator *)rows {
[_rows autorelease];
_rows = [rows retain];
}


- (RESTOperation*) start {
if (!_op) {
if (!_observing) {
_observing = YES;
self.database.tracksChanges = YES;
[[NSNotificationCenter defaultCenter] addObserver: self
selector: @selector(databaseChanged)
name: kCouchDatabaseChangeNotification
object: self.database];
}
NSLog(@"CouchLiveQuery: Starting...");
_op = [[super start] retain];
[_op start];
}
return _op;
}


- (void) updateRows {
if (_op)
return; // TODO: Should probably re-run query after current _op completes, instead
if (_rows)
self.prefetch = NO; // (prefetch disables conditional GET shortcut)
[self start];
}


- (void) databaseChanged {
[self updateRows];
}


- (NSError*) operation: (RESTOperation*)op willCompleteWithError: (NSError*)error {
error = [super operation: op willCompleteWithError: error];

if (op == _op) {
NSLog(@"CouchLiveQuery: ...Finished (status=%i)", op.httpStatus);
[_op release];
_op = nil;
CouchQueryEnumerator* rows = op.resultObject;
if (rows && ![rows isEqual: _rows]) {
NSLog(@"CouchLiveQuery: ...Rows changed! (now %lu)", (unsigned long)rows.count);
self.rows = rows; // Triggers KVO notification
}
}

return error;
}


@end




@implementation CouchQueryEnumerator

Expand All @@ -166,7 +263,7 @@ - (id) initWithQuery: (CouchQuery*)query op: (RESTOperation*)op {
if (self) {
NSDictionary* result = $castIf(NSDictionary, op.responseBody.fromJSON);
_query = [query retain];
_rows = [$castIf(NSArray, [result objectForKey: @"rows"]) retain]; // BLOCKING
_rows = [$castIf(NSArray, [result objectForKey: @"rows"]) retain];
if (!_rows) {
[self release];
return nil;
Expand All @@ -186,6 +283,16 @@ - (void) dealloc
}


- (BOOL) isEqual:(id)object {
if (object == self)
return YES;
if (![object isKindOfClass: [CouchQueryEnumerator class]])
return NO;
CouchQueryEnumerator* otherEnum = object;
return [otherEnum->_rows isEqual: _rows];
}


- (NSUInteger) count {
return _rows.count;
}
Expand Down
1 change: 0 additions & 1 deletion Couch/CouchRevision.h
Expand Up @@ -20,7 +20,6 @@
@interface CouchRevision : CouchResource
{
@private
NSDictionary* _contents;
NSDictionary* _properties;
BOOL _isDeleted;
}
Expand Down
42 changes: 14 additions & 28 deletions Demo/DemoAppController.m
Expand Up @@ -52,11 +52,9 @@ - (void) applicationDidFinishLaunching: (NSNotification*)n {
NSAssert(op.error.code == 412, @"Error creating db: %@", op.error);
}

self.query = [[[DemoQuery alloc] initWithQuery: [_database getAllDocuments]] autorelease];

[[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(databaseChanged:)
name: kCouchDatabaseChangeNotification
object: _database];
CouchQuery* q = [_database getAllDocuments];
q.descending = YES;
self.query = [[[DemoQuery alloc] initWithQuery: q] autorelease];

// Enable continuous sync:
NSString* otherDbURL = [bundleInfo objectForKey: @"SyncDatabaseURL"];
Expand Down Expand Up @@ -85,29 +83,9 @@ - (void) startContinuousSyncWith: (NSURL*)otherDbURL {
#pragma mark HIGHLIGHTING NEW ITEMS:


- (void) databaseChanged: (NSNotification*)n {
if (!_glowing) {
// Wait to redraw the table, else there is a race condition where if the
// DemoItem gets notified after I do, it won't have updated timeSinceExternallyChanged yet.
_glowing = YES;
[self performSelector: @selector(updateTableGlows) withObject: nil afterDelay:0.0];
}
}


- (void) updateTableGlows {
BOOL glowing = NO;
for (DemoItem* item in _tableController.arrangedObjects) {
if (item.timeSinceExternallyChanged < kChangeGlowDuration) {
glowing = YES;
break;
}
}
if (glowing || _glowing)
[_table setNeedsDisplay: YES];
_glowing = glowing;
if (glowing)
[self performSelector: @selector(updateTableGlows) withObject: nil afterDelay: 0.1];
_glowing = NO;
[_table setNeedsDisplay: YES];
}


Expand All @@ -118,7 +96,10 @@ - (void)tableView:(NSTableView *)tableView
{
NSColor* bg = nil;

DemoItem* item = [_tableController.arrangedObjects objectAtIndex: row];
NSArray* items = _tableController.arrangedObjects;
if (row >= items.count)
return; // Don't know why I get called on illegal rows, but it happens...
DemoItem* item = [items objectAtIndex: row];
NSTimeInterval changedFor = item.timeSinceExternallyChanged;
if (changedFor > 0 && changedFor < kChangeGlowDuration) {
float fraction = 1.0 - changedFor / kChangeGlowDuration;
Expand All @@ -127,6 +108,11 @@ - (void)tableView:(NSTableView *)tableView
ofColor: [NSColor yellowColor]];
else
bg = [[NSColor yellowColor] colorWithAlphaComponent: fraction];

if (!_glowing) {
_glowing = YES;
[self performSelector: @selector(updateTableGlows) withObject: nil afterDelay: 0.1];
}
}

[cell setBackgroundColor: bg];
Expand Down
6 changes: 2 additions & 4 deletions Demo/DemoQuery.h
Expand Up @@ -14,7 +14,7 @@
// and limitations under the License.

#import <Cocoa/Cocoa.h>
@class CouchQuery, RESTOperation;
@class CouchQuery, CouchLiveQuery, RESTOperation;


/** Simple controller for CouchDB demo apps.
Expand All @@ -23,7 +23,7 @@
without needing any code. */
@interface DemoQuery : NSObject
{
CouchQuery* _query;
CouchLiveQuery* _query;
RESTOperation* _op;
NSMutableArray* _entries;
Class _modelClass;
Expand All @@ -34,8 +34,6 @@
/** Class to instantiate for entries. Defaults to DemoItem. */
@property (assign) Class modelClass;

- (void) updateEntries;

/** The documents returned by the query, wrapped in DemoItem objects.
An NSArrayController can be bound to this property. */
//@property (readonly) NSMutableArray* entries;
Expand Down
38 changes: 14 additions & 24 deletions Demo/DemoQuery.m
Expand Up @@ -33,27 +33,22 @@ - (id) initWithQuery: (CouchQuery*)query
self = [super init];
if (self != nil) {
_modelClass = [DemoItem class];
_query = [query retain];
_query = [[query asLiveQuery] retain];

_query.prefetch = YES; // for efficiency, include docs on first load
[self loadEntriesFrom: [_query rows]];

// Listen for external changes:
_query.database.tracksChanges = YES;
[[NSNotificationCenter defaultCenter] addObserver: self
selector: @selector(updateEntries)
name: kCouchDatabaseChangeNotification
object: nil];
[_query start];

// Observe changes to _query.rows:
[_query addObserver: self forKeyPath: @"rows" options: 0 context: NULL];
}
return self;
}


- (void) dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver: self];
[_op cancel];
[_entries release];
[_query removeObserver: self forKeyPath: @"rows"];
[_query release];
[super dealloc];
}
Expand All @@ -76,7 +71,8 @@ - (void) loadEntriesFrom: (CouchQueryEnumerator*)rows {
}

if (![entries isEqual:_entries]) {
NSLog(@" ...entries changed!");
NSLog(@" ...entries changed! (was %u, now %u)",
(unsigned)_entries.count, (unsigned)entries.count);
[self willChangeValueForKey: @"entries"];
[_entries release];
_entries = [entries mutableCopy];
Expand All @@ -85,18 +81,12 @@ - (void) loadEntriesFrom: (CouchQueryEnumerator*)rows {
}


- (void) updateEntries {
if (_op)
return;
NSLog(@"Updating the query...");
_query.prefetch = NO; // prefetch disables rowsIfChanged optimization
_op = [_query start];
[_op onCompletion: ^{
CouchQueryEnumerator* rows = _op.resultObject;
_op = nil;
if (rows)
[self loadEntriesFrom: rows];
}];
- (void)observeValueForKeyPath: (NSString*)keyPath ofObject: (id)object
change: (NSDictionary*)change context: (void*)context
{
if (object == _query) {
[self loadEntriesFrom: _query.rows];
}
}


Expand Down
2 changes: 2 additions & 0 deletions README.md
Expand Up @@ -30,6 +30,8 @@ There are two simple Mac demo apps included in the Demo/ subfolder. One lets you

### Building The Framework

(You only need to do this if you checked out the CouchCocoa source code and want to build it yourself. If you downloaded a precompiled framework, just go onto the next section.)

1. Open CouchDemo.xcodeproj
2. Select "Mac Framework" or "iOS Framework" from the scheme pop-up in the toolbar
3. Product > Build
Expand Down

0 comments on commit 38f6a6a

Please sign in to comment.