diff --git a/lib/cradle.js b/lib/cradle.js index 269f1ab..6c68c34 100644 --- a/lib/cradle.js +++ b/lib/cradle.js @@ -26,7 +26,8 @@ cradle.options = { retries: 0, retryTimeout: 10e3, forceSave: true, - headers: {} + headers: {}, + forceReconnect: false, }; cradle.setup = function (settings) { @@ -155,13 +156,14 @@ cradle.Connection.prototype.rawRequest = function (options, callback) { options.query[k] = String(options.query[k]); } } - options.path += '?' + querystring.stringify(options.query); + options.uri = this._url(options.path + '?' + querystring.stringify(options.query)); + } + else { + options.uri = this._url(options.path); } options.headers['Connection'] = options.headers['Connection'] || 'keep-alive'; options.agent = this.agent; - options.uri = this._url(options.path); - delete options.path; return request(options, callback || function () { }); }; @@ -214,6 +216,12 @@ cradle.Connection.prototype.request = function (options, callback) { return this.rawRequest(options, function _onResponse(err, res, body) { attempts++; if (err) { + if (self.options.forceReconnect && String(err.code).startsWith('ECONN')) { + return setTimeout( + self.rawRequest.bind(self, options, _onResponse), + self.options.retryTimeout + ); + } if (self.options.retries && (!options.method || options.method.toLowerCase() === 'get' || options.body) && String(err.code).indexOf('ECONN') === 0 && attempts <= self.options.retries @@ -253,8 +261,8 @@ cradle.Connection.prototype.request = function (options, callback) { // We return an object with database functions, // closing around the `name` argument. // -cradle.Connection.prototype.database = function (name) { - return new cradle.Database(name, this) +cradle.Connection.prototype.database = function (name, opts) { + return new cradle.Database(name, this, opts) }; // diff --git a/lib/cradle/database/changes.js b/lib/cradle/database/changes.js index fe0ba8f..999f86c 100644 --- a/lib/cradle/database/changes.js +++ b/lib/cradle/database/changes.js @@ -50,7 +50,21 @@ Database.prototype.changes = function (options, callback) { response.emit.apply(response, ['data'].concat(Array.prototype.slice.call(arguments))); }); - + + var self = this; + // Keep a consistent object for return to the client, even if + // this feed is restarted due to error. + feed.on('error', function (err) { + if (feed.dead && options.follow !== false) { + console.error(self.name, 'ERROR: Cradle changes feed died, restarting', err.message || err); + setTimeout(function() { + console.error(self.name, 'RECOVERY: Restarting feed that died with', err.message || err); + feed.restart(); + feed.emit('recover', err); + }, 1000); + } + }); + if (options.follow !== false) { feed.follow(); } diff --git a/lib/cradle/database/index.js b/lib/cradle/database/index.js index 9effb77..68c8f4b 100644 --- a/lib/cradle/database/index.js +++ b/lib/cradle/database/index.js @@ -2,15 +2,60 @@ var querystring = require('querystring'), Args = require('vargs').Constructor, cradle = require('../../cradle'); -var Database = exports.Database = function (name, connection) { +var Database = exports.Database = function (name, connection, opts) { this.connection = connection; this.name = encodeURIComponent(name); - this.cache = new (cradle.Cache)(connection.options); + this.opts = {...connection.options}; + if (opts && opts.disableCache) { + this.opts.cache = false; + this.opts.cacheSize = 0; + } + + // Workaround https://issues.apache.org/jira/browse/COUCHDB-1415 by default + this.workaroundBug1415 = true; + + this.cache = new (cradle.Cache)(this.opts); + this.cacheFeed = null; + if (this.opts.cache === 'follow') { + var self = this; + this.exists(function(err, result) { + if (result === true) + self.configureCacheFeed(); + }); + } +}; + +Database.prototype.configureCacheFeed = function () { + if (this.cacheFeed) { + this.cacheFeed.stop(); + this.cacheFeed = null; + } + // For any entry already in the cache, update it if it changes + // remotely. + if (this.opts.cache === 'follow') { + var self = this; + this.changes({descending: true, limit: 0}, function (err, list) { + var lastSeq = 0; + if (list && 'last_seq' in list) + lastSeq = list.last_seq; + self.cacheFeed = self.changes({ since: lastSeq, include_docs: true }); + self.cacheFeed.on('change', function (change) { + var id = change["id"]; + if (id && 'doc' in change && self.cache.has(id)) + self.cache.save(id, change["doc"]); + }); + }); + } }; // A wrapper around `Connection.request`, // which prepends the database name. Database.prototype.query = function (options, callback) { + + // XXX: Workaround https://issues.apache.org/jira/browse/COUCHDB-1415 + if (options.body && options.method === 'POST' && this.workaroundBug1415) + options.body.$ts = Date.now(); + options.path = [this.name, options.path].filter(Boolean).join('/'); return this.connection.request(options, callback); }; @@ -39,7 +84,11 @@ Database.prototype.info = function (callback) { }; Database.prototype.create = function (callback) { - this.query({ method: 'PUT' }, callback); + var self = this; + this.query({ method: 'PUT' }, function () { + self.configureCacheFeed(); + callback.apply(this, arguments); + }); }; // Destroys a database with 'DELETE' @@ -48,12 +97,12 @@ Database.prototype.create = function (callback) { Database.prototype.destroy = function (callback) { if (arguments.length > 1) { throw new(Error)("destroy() doesn't take any additional arguments"); - } - + } + this.query({ - method: 'DELETE', - path: '/', - }, callback); + method: 'DELETE', + path: '/', + }, callback); }; // @@ -62,4 +111,4 @@ Database.prototype.destroy = function (callback) { require('./attachments'); require('./changes'); require('./documents'); -require('./views'); \ No newline at end of file +require('./views');