Browse files

Big Sunday afternoon refactor. I fixed issues regarding conflict mana…

…gement on bulk updates, HTTP basic auth and error handling. CouchDB error objects are just handed over to the caller now (I don't see a need for using custom errors here). CouchClient#save no longer writes over previous documents (Fixes Issue #9). You need to specify {force: true} in order to force an update. Finally there's a new testsuite.
  • Loading branch information...
1 parent 7b1484c commit 011698b10a5d21ab43d86aa277884f92b48e24ed Michael Aufreiter committed Feb 27, 2011
Showing with 161 additions and 416 deletions.
  1. +4 −93 README.markdown
  2. +0 −159 basicTests.js
  3. +1 −0 index.js
  4. +0 −12 insertTest.js
  5. +44 −53 lib/couch-client.js
  6. +6 −6 package.json
  7. +0 −16 saveTest.js
  8. +0 −77 test.js
  9. +106 −0 test/test.js
View
97 README.markdown
@@ -1,97 +1,8 @@
# Couch Client
-Couch Client is a simple wrapper around CouchDB's http interface.
+Couch Client is a simple wrapper around CouchDB's http interface.
-> "CouchDB is a document-oriented, Non-Relational Database Management Server (NRDBMS)."
+This fork fixes some issues, and adds API improvements. It is used internally by [Data.js](http://github.com/michael/data), a Javascript Graph Persistence Framework.
-This wrapper/driver implements some nice things like request batching and automatic revision and key generation. Usage should be simple and straight-forward. For anything advanced, a simple http client is exposed that already connected to the couch server.
-
-## `CouchClient(url)` - A client factory
-
-This module exports a single factory function called `CouchClient`. Usage is simple, it takes a single url for the connection to the database.
-
- var CouchClient = require('couch-client');
- var Users = CouchClient("http://username:password@yourhost:5984/users");
-
-Since this assumes many defaults, for local testing on a stock CouchDB build, you can connect with simply `CouchCLient("/users")`.
-
-This object will internally keep track of revisions it's seen before and batches http requests that happen in the same "tick" to go out at the same time using CouchDB's batch processing APIs.
-
-The resulting object has the following four functions (`save`, `get`, `remove`, `request`);
-
-The default port is 5984.
-
-### `CouchClient.save(doc, callback)` - Save a document
-
-Pass in a document and it will save it to the database. If the document happens to have a `_id` property that that will be used as the key. Also if the document has a `_rev` property, that will be passed to the server. If they are missing, CouchClient will provide an automatic UUID using CouchDB's services and look up the latest revision. Revisions are remembered.
-
- Users.save({_id: "creationix", name: "Tim Caswell", age: 28}, function ( err, doc) {
- // You know know if there was an error and have an updated version
- // of the document (with `_id` and `_rev`).
- });
-
-### `CouchClient.get(key, callback)` - Load a document
-
-Once you put data in the database, it's nice to be able to load it back out. The `.get()` function takes a key as a string and returns the document in a callback.
-
- Users.get("creationix", function (err, doc) {
- // Now you have the document or error if there was trouble
- });
-
-### `CouchClient.view(path, obj, callback)` - Call a view
-
-`view` takes a path to a view, an object containing GET parameters, and a callback
-
- Users.view('/users/_design/design_name/_view/usernames', {key: "creationix"}, function(err, doc) {
- // Now you have the document(s) or error if there was trouble
- });
-
-If `obj` is missing then it returns all the items in the view.
-
-### `CouchClient.remove(key/doc, callback)` - Remove a document
-
-Sometimes you want to remove documents from the database. This function takes either a key as a string or a document with an `_id` property. It will tell couch to delete it and give you back the modified document with the `_deleted` property.
-
- Users.remove("creationix", function (err, doc) {
- // If there was no error, it's gone
- });
-
-### `CouchClient.request(method, path, body, callback)` - Arbitrary HTTP request.
-
-This is the helper used internally to execute HTTP requests against the CouchDB database. It's exposed here because CouchDB provides a very rich interface, and it can't all be easily wrapped. The `body` parameter is optional and should be passed in as a raw JavaScript object.
-
- Users.request("GET", "/some_database/_design/company/_view/all", function (err, result) {
- // result is a javascript object of CouchDB's response JSON
- });
-
- // Manually insert a single document
- Users.request("PUT", "/foo/bar", {Foo:"Bar"}, function (err, result) {
- // result is a javascript object of CouchDB's response JSON
- });
-
-Also if you omit the callback an `EventEmitter` object that emits `"data"`, `"end"`, and `"error"` events. Note that the data events are not parsed. They are raw `utf8` strings.
-
-### `CouchClient.changes(since, callback)` - Watch for changes to the database.
-
-CouchDB provides a neat feature known as the `_changes` feed. This allows you do watch a database and be notified when it's changed. The `changes()` function is a wrapper. You give it the `since` parameter and a callback. The callback is called once per JSON document in the response stream.
-
-
-## Testing
-
-The basicTests file currently tests a;
-* local couchdb instance, with no authorization setup.
-* couchone instance with authorization at https://couch-client.couchone.com/_utils couch-client:testingonly
-
-
-### Adding Tests
-
-Add tests to the basicTests.js file. Test function names need to start with 'test'.
-
- testSomething:function() {
- // testing something
- },
-
-
-## Contributing
-
-* please include tests with your pull requests.
+* <a href="substance.io/#michael/couch-client">Documentation</a>
+* <a href="github.com/creationix/couch-client">Official Repository</a>
View
159 basicTests.js
@@ -1,159 +0,0 @@
-var CouchClient = require('./lib/couch-client');
-var assert = require('assert');
-
-({
- run:function() {
- for (name in this) {
- if (name.slice(0,4) == 'test')
- {
- console.log("Running: " + name + '\r');
- this[name]();
- }
- }
- },
-
- testLocalDefaults:function()
- {
- var db = CouchClient('/test');
-
- db.request("PUT", "/test", function (err, result) {
- if (err) throw err;
- db.save({_id:'testid0', testvalue:'atest'}, function (err, result) {
- if (err) throw err;
- db.get('testid0', function (err, result) {
- if (err) throw err;
-
- if (!result || !(result.testvalue === 'atest'))
- throw 'Value does not match';
-
- db.save({_id:'testid0', testvalue:'btest'}, function(err, result) {
- if (err) throw err;
-
- console.log(result);
-
- if (!result || !(result.testvalue === 'btest'))
- throw 'Value does not match';
-
- db.remove('testid0', function(err, result) {
- if (err) throw err;
- });
- });
- });
- });
- });
- },
-
- testRemoveObject:function()
- {
- var db = CouchClient('/test');
-
- db.request("PUT", "/test", function (err, result) {
- if (err) throw err;
- db.save({_id:'testid3', testvalue:'atest'}, function (err, result) {
- if (err) throw err;
- db.get('testid3', function (err, result) {
- if (err) throw err;
-
- if (!result || !(result.testvalue === 'atest'))
- throw 'Value does not match';
-
- console.log(result);
-
- db.remove(result, function(err, result) {
- if (err) throw err;
-
- db.get('testid3', function(err, result) {
- if (result) throw "should be removed";
- if (!err && err.errno != 2) throw err;
- });
- });
- });
- });
- });
- },
-
- testCouchOne:function()
- {
- var db = CouchClient('http://couch-client.couchone.com:80/test');
-
- db.request("PUT", "/test", function (err, result) {
- if (err) throw err;
- db.save({_id:'testid1', testvalue:'atest'}, function (err, result) {
- if (err) throw err;
- db.get('testid1', function (err, result) {
- if (err) throw err;
-
- if (!result || !(result.testvalue === 'atest'))
- throw 'Value does not match';
-
- db.save({_id:'testid1', testvalue:'btest'}, function(err, result) {
- if (err) throw err;
-
- console.log(result);
-
- if (!result || !(result.testvalue === 'btest'))
- throw 'Value does not match';
-
- db.remove('testid1', function(err, result) {
- if (err) throw err;
- });
- });
- });
- });
- });
- },
-
-
- testCouchOneSSL:function()
- {
- var db = CouchClient('https://couch-client.couchone.com:443/test');
-
- db.request("PUT", "/test", function (err, result) {
- if (err) throw err;
- db.save({_id:'testid2', testvalue:'atest'}, function (err, result) {
- if (err) throw err;
- db.get('testid2', function (err, result) {
- if (err) throw err;
-
- if (!result || !(result.testvalue === 'atest'))
- throw 'Value does not match';
-
- db.save({_id:'testid2', testvalue:'btest'}, function(err, result) {
- if (err) throw err;
-
- console.log(result);
-
- if (!result || !(result.testvalue === 'btest'))
- throw 'Value does not match';
-
- db.remove('testid2', function(err, result) {
- if (err) throw err;
- });
- });
- });
- });
- });
- },
-
- testBasicAuthFailsWhenUsingHttp: function() {
- var db = CouchClient('http://couch-client:testingonly@couch-client.couchone.com:80/');
- db.view('/secure/does/not/exist', function (err, result) {
- if (err) throw err;
- assert.deepEqual(
- {error: 'unauthorized', reason: 'You are not authorized to access this db.'},
- result
- );
- });
- },
-
- testBasicAuthWorksWhenUsingHttps: function() {
- var db = CouchClient('https://couch-client:testingonly@couch-client.couchone.com:443/');
- db.view('/secure/does/not/exist', function (err, result) {
- if (err) throw err;
- assert.deepEqual(
- {error: 'not_found', reason: 'missing'},
- result
- );
- });
- }
-}).run();
View
1 index.js
@@ -0,0 +1 @@
+module.exports = require("./lib/couch-client");
View
12 insertTest.js
@@ -1,12 +0,0 @@
-var CouchClient = require('./lib/couch-client');
-var db = CouchClient("http://localhost:5984/test");
-var i = 100000;
-var start;
-db.request("PUT", "/test", function (err, result) {
- while (i--) {
- db.save({_id: "0x" + i.toString(16), name:"Tim", age: 28}, function (err, result) {
- if (err) throw err;
- });
- }
-});
-
View
97 lib/couch-client.js
@@ -1,17 +1,13 @@
-/*global Buffer */
+/* global Buffer */
var http = require('http'),
https = require('https'),
Url = require('url'),
EventEmitter = require('events').EventEmitter,
querystring = require('querystring');
-
-// Handles changes made in node v0.3.0
-var NOT_FOUND_ERR_NO = process.ENOENT ? process.ENOENT : require('constants').ENOENT;
var MAX_DOCS = 1000; // The maximum number of docs to send in a single batch
-
-function noOp(err) { if (err) { throw err; } }
+var noOp = function(err) { if (err) { throw err; } }
var CONNECTION_DEFAULTS = {
host: '127.0.0.1:5984',
@@ -35,6 +31,7 @@ function CouchClient(url) {
callback = body;
body = undefined;
}
+
// Return a stream if no callback is specified
if (!callback) {
stream = new EventEmitter();
@@ -51,8 +48,9 @@ function CouchClient(url) {
var headers = {
"Host": uri.hostname
};
+
// add the authorization header if provided and using https
- if (uri.auth && uri.secure) {
+ if (uri.auth) {
headers["Authorization"] = "Basic " + new Buffer(uri.auth, "ascii").toString("base64");
}
@@ -69,6 +67,7 @@ function CouchClient(url) {
port: uri.port,
headers: headers
};
+
var request = uri.protocolHandler.request(options, function (response) {
response.setEncoding('utf8');
var body = "";
@@ -86,7 +85,6 @@ function CouchClient(url) {
if (body) { request.write(body, 'utf8'); }
request.end();
-
return stream;
}
@@ -98,7 +96,6 @@ function CouchClient(url) {
function consumeQueue() {
var pending = uuidQueue.splice(0, MAX_DOCS);
if (uuidQueue.length) { process.nextTick(consumeQueue); }
- // console.log("Bulk getting UUIDs %s", pending.length);
request("GET", "/_uuids?count=" + pending.length, function (err, result) {
if (err) {
pending.forEach(function (callback) {
@@ -125,16 +122,13 @@ function CouchClient(url) {
saveValues.push(doc);
saveQueue.push(callback);
if (saveQueue.length > 1) { return; }
+
function consumeQueue() {
var pending = saveQueue.splice(0, MAX_DOCS);
var body = saveValues.splice(0, MAX_DOCS);
if (saveQueue.length) { process.nextTick(consumeQueue); }
- // console.log("Bulk saving %s", body.length);
request("POST", uri.pathname + "/_bulk_docs", {docs: body}, function (err, results) {
- if (results.error) {
- err = new Error("CouchDB Error: " + JSON.stringify(results));
- if (results.error === 'not_found') { err.errno = NOT_FOUND_ERR_NO; }
- }
+ if (!err && results && results.error) err = results;
if (err) {
pending.forEach(function (callback) {
callback(err);
@@ -143,10 +137,14 @@ function CouchClient(url) {
}
results.forEach(function (result, i) {
var doc = body[i];
- doc._id = result.id;
- doc._rev = result.rev;
- revCache[result.id] = result.rev;
- pending[i](null, doc);
+ if (result.error && result.error === "conflict") {
+ // handle update conflict
+ pending[i](result);
+ } else {
+ doc._id = result.id;
+ doc._rev = result.rev;
+ pending[i](null, doc);
+ }
});
});
}
@@ -165,11 +163,8 @@ function CouchClient(url) {
if (getQueue.length) { process.nextTick(consumeQueue); }
var path = uri.pathname + "/_all_docs";
if (includeDoc) { path += "?include_docs=true"; }
- // console.log("Bulk Getting %s documents", keys.length);
request("POST", path, {keys: keys}, function (err, results) {
- if (!results.rows) {
- err = new Error("CouchDB Error: " + JSON.stringify(results));
- }
+ if (!results.rows) err = results;
if (err) {
pending.forEach(function (callback) {
callback(err);
@@ -180,14 +175,7 @@ function CouchClient(url) {
var err;
if (includeDoc) {
if (result.error) {
- err = new Error("CouchDB Error: " + JSON.stringify(result));
- if (result.error === 'not_found') { err.errno = NOT_FOUND_ERR_NO; }
- pending[i](err);
- return;
- }
- if (!result.doc) {
- err = new Error("Document not found for " + JSON.stringify(result.key));
- err.errno = NOT_FOUND_ERR_NO;
+ err = result;
pending[i](err);
return;
}
@@ -201,11 +189,14 @@ function CouchClient(url) {
process.nextTick(consumeQueue);
}
-
- function save(doc, callback) {
+ function save(doc, options, callback) {
+ if (typeof options === 'function') {
+ callback = options;
+ options = null;
+ }
if (!callback) { callback = noOp; }
if (doc._id) {
- if (!doc._rev) {
+ if (!doc._rev && options && options.force) {
if (!revCache.hasOwnProperty(doc._id)) {
realGet(doc._id, false, function (err, result) {
if (err) { return callback(err); }
@@ -249,6 +240,7 @@ function CouchClient(url) {
checkData();
}
}
+
stream.on('error', callback);
stream.on('data', function (chunk) {
data += chunk;
@@ -259,37 +251,36 @@ function CouchClient(url) {
});
}
- function view(viewName, obj, callback) {
- var method = "GET";
- var body = null;
+ function view(path, obj, callback) {
+ var method = "GET";
+ var body = null;
+
if (typeof obj === 'function') {
callback = obj;
obj = null;
}
- if ( viewName.substr(0,1) != '/' ) {
- // assume viewname is designdocname/viewname
- var parts = viewName.split("/",2);
- if ( parts.length == 2 ) {
- viewName = uri.pathname+"/_design/"+parts[0]+"/_view/"+parts[1];
- }
- }
+ path = path.split('/');
+ path = [uri.pathname, '_design', path[0], '_view', path[1]].join('/');
if (obj && typeof obj === 'object') {
Object.keys(obj).forEach(function(key){
- if ( key === 'keys' ) {
- body = { keys: obj[key] }; // body is json stringified in request fn
- method='POST';
- } else {
- obj[key] = JSON.stringify(obj[key]);
- }
+ if ( key === 'keys' ) {
+ body = { keys: obj[key] }; // body is json stringified in request fn
+ method='POST';
+ } else {
+ obj[key] = JSON.stringify(obj[key]);
+ }
});
+
var getParams = querystring.stringify(obj);
- if (getParams){
- viewName = viewName + '?' + getParams;
+ if (getParams) {
+ path = path + '?' + getParams;
}
}
- request(method, viewName, body, callback);
+ request(method, path, body, function(err, result) {
+ result && result.error ? callback(result) : callback(err, result);
+ });
}
// Expose the public API
@@ -304,4 +295,4 @@ function CouchClient(url) {
};
}
-module.exports = CouchClient;
+module.exports = CouchClient;
View
12 package.json
@@ -6,11 +6,11 @@
"author": "Tim Caswell <tim@creationix.com>",
"repository": {
"type": "git",
- "url": "http://github.com/creationix/couch-client.git"
+ "url": "http://github.com/michael/couch-client.git"
},
- "bugs": "http://github.com/creationix/couch-client/issues",
- "engines": ["node >= 0.2.0"],
+ "bugs": "http://github.com/michael/couch-client/issues",
+ "engines": ["node >= 0.4.0"],
"modules": {
- "index": "./lib/couch-client.js",
- },
-}
+ "index": "./lib/couch-client.js"
+ }
+}
View
16 saveTest.js
@@ -1,16 +0,0 @@
-var CouchClient = require('./lib/couch-client');
-process.title = "couch-client saveTest"
-var db = CouchClient("/test");
-
-// Insert 100,000 documents with specified ids in a single sync block of code
-// The driver should automatically chunk these into parallel requests for best
-// performance.
-db.request("PUT", "/test", function (err, result) {
- console.dir(arguments);
- var i = 100000;
- while (i--) {
- db.save({_id:(i+1).toString()}, function (err, result) {
- if (err) throw err;
- });
- }
-});
View
77 test.js
@@ -1,77 +0,0 @@
-var CouchClient = require('./lib/couch-client');
-
-var Meetings = CouchClient("http://hack:me@localhost:5984/meetings");
-
-// Meetings.changes(54, function (err, change) {
-// if (err) throw err;
-// console.dir(change);
-// });
-//
-// setInterval(function () {
-// Meetings.save({FOO:false});
-// }, 500);
-//
-// Meetings.save({name:"Tim2",hacker:true}, function (err, result) {
-// if (err) throw err;
-// });
-// var Step = require('step');
-// Step(
-// function () {
-// Meetings.save({_id: "creationix", name:"Tim",age:28}, this);
-// },
-// function (err, doc) {
-// if (err) throw err;
-// console.log("Saved %s", JSON.stringify(doc));
-// doc.age = 100;
-// Meetings.save(doc, this);
-// },
-// function (err, doc) {
-// if (err) throw err;
-// Meetings.remove(doc._id, this);
-// },
-// function (err, doc) {
-// if (err) throw err;
-// Meetings.get(doc._id, this);
-// },
-// function (err, doc) {
-// if (err) throw err;
-// console.dir(doc);
-// }
-// )
-
-
-// Meetings.get("creationix", function (err, doc) {
-// console.dir(arguments);
-// });
-// Meetings.get("creationix", function (err, doc) {
-// console.dir(arguments);
-// });
-//
-const NUM = 1000;
-const LOOPS = 1000;
-var start = Date.now();
-var value = {Foo:"Bar"};
-// var interval = setInterval(function () {
-// console.log("Tick");
-// }, 500);
-
-
-count = NUM * LOOPS;
-function test() {
- var i = NUM;
- while (i--) {
- Meetings.save(value, function (err, doc) {
- // console.log(doc);
- if (err) throw err;
- if (!--count) {
- console.log("Inserted %s items in %sms", NUM * LOOPS, Date.now() - start);
- // clearInterval(interval);
- }
- });
- }
-}
-for (var i = 0; i < LOOPS; i++) {
- setTimeout(test, i*30);
-}
-
-
View
106 test/test.js
@@ -0,0 +1,106 @@
+var assert = require('assert');
+var CouchClient = require('../lib/couch-client');
+var db = CouchClient("http://127.0.0.1:5984/test");
+var async = require('async');
+
+// CouchClient#save
+// --------------
+
+// Insert 3,000 documents with specified ids in a single sync block of code
+// The driver should automatically chunk these into parallel requests for best
+// performance.
+
+function testSave(callback) {
+ var i = 3000;
+ var fns = []
+ while (i--) {
+ fns.push(function(callback) {
+ db.save({content: "Foo"}, function (err, result) {
+ assert.ok(!err);
+ callback();
+ });
+ });
+ }
+
+ async.parallel(fns, function(err, results) {
+ // Conflict handling, some save operations may fail due to conflicts
+ db.save({_id:"foo", content: "Foo"}, function (err, result) {
+ assert.ok(!err);
+
+ db.save({_id:"bar", content: "Bar"}, function (err, result) {
+ assert.ok(!err); // no conflict here
+
+ db.save({_id:"foo", content: "Conflicting"}, function (err, result) {
+ assert.ok(err.error == 'conflict'); // detect conflict!
+
+ // Force update (ignore conflicts and overwrite)
+ db.save({_id:"foo", content: "Forced update"}, {force: true}, function (err, doc) {
+ assert.ok(!err);
+ assert.ok(doc);
+ callback();
+ });
+ });
+ });
+ });
+ });
+}
+
+
+// CouchClient#get
+// --------------
+
+function testGet(callback) {
+ db.get('foo', function(err, doc) {
+ assert.ok(!err);
+ assert.ok(doc);
+
+ db.get('the_doc_that_wasnt_there', function(err, doc) {
+ assert.ok(err);
+ assert.ok(!doc);
+ callback();
+ });
+ });
+}
+
+// CouchClient#view
+// --------------
+
+function testView(callback) {
+ var view = {
+ _id: '_design/queries',
+ views: {
+ "by_content": {
+ "map": "function(doc) { emit(doc.content, doc); }"
+ }
+ }
+ };
+
+ db.save(view, function (err, doc) {
+ db.view('queries/by_content', {limit: 10}, function(err, result) {
+ assert.ok(result.rows.length === 10);
+ assert.ok(!err);
+
+ db.view('queries/not_existing_view', function(err, result) {
+ assert.ok(err);
+ assert.ok(!result);
+ callback();
+ });
+ });
+ });
+}
+
+
+// Flush DB and perform tests
+db.request("DELETE", db.uri.pathname, function (err) {
+ if (err) console.log(err);
+ db.request("PUT", db.uri.pathname, function(err) {
+ if (err) console.log(err);
+ async.series([
+ testSave,
+ testGet,
+ testView
+ ], function(err) {
+ console.log('Tests completed.');
+ });
+ });
+});

7 comments on commit 011698b

@candland

Wow, thanks for all this work. I'll take a look soon.

@candland

Hi Michael, I'm pulling in some of these changes, I have a couple of questions, first why add the force to save? This will be a breaking change for a lot of people.

Second, the force to save breaks remove and save w/out a _rev, is that intentional?

@michael
Owner

Hi Dusty, yes that's intensional. To handle conflicts CouchDB itself requires a _rev on every update to an existing document, and so CouchClient should do. {force: true} then is a convenient way to ignore conflicts and overwrite (but you need to be explicit, what I think is important). This was already discussed and confirmed by Tim. Even a patch was there, it just wasn't pulled in. https://github.com/creationix/couch-client/issues#issue/9

Certainly we can discuss API details, and we should do some more testing. What about calling this the 0.1.0 series and putting it in a branch for further discussion? When ready, we can do the npm release.

I'm going to use this in production soon employing the following stack:

Substance — The Application — https://github.com/michael/substance
Data.js — For higher level data manipulation / object mapping — https://github.com/michael/data
CouchClient — For talking to Couch (safely ;))

This should give me enough confidence that everything runs smoothly.

-- Michael

@candland

Hi Michael, I see what you're saying after reading issue 9. I think moving to another branch and major version change is a good way to go. I'm pretty new to Couch so I just want to understand how things should work w/ these changes.

  1. the revision cache was removed, so updates and removes need the _rev, which really means they need an object to update, not just a string for _id in the case of remove.

  2. force=true forces an update w/ out the revision - causing an unsafe update or forces the update w/out revision checking?

@michael
Owner
  1. I'll have a look at remove() soon. I haven't tested it yet. Not sure if Couch itself accepts a remove aka an update with _deleted: true without a _rev. I guess not. We have two options here regarding the API:

We could keep it, and skip revision checking for delete operations (same behavior like with force: true)
We could use a rev property as a second parameter remove(id, rev)
Or we completely remove it from the API, suggesting that users should add a _deleted property and call save().

  1. Subtle difference, you're right. Hmm.. I'd say if one specifes force: true, we always fetch the latest rev, then do the update. Then the user can be sure it gets updated with his changes (regardless which revision was specified). Would you agree with that?
@candland

The remove is just adding a _deleted now, so it mostly just boils down to what we do with updates.

I think that makes sense.

Was the rev cache not a good idea?

BTW, I added your changes to a new 0.1.0 branch.

@michael
Owner

Now that we suggest save operations with our API I think a rev cache no longer makes sense (at least within in the library). A CouchClient user however, could manage his own _rev cache, but that doesn't concern the lib.

Seen. Thanks! I'll build further work upon the 0.1.0 branch. CouchClient/Data.js tasks are a few more days away...

Thanks for your help, reviewing my patches!

Please sign in to comment.