pouchdb
Version:
PouchDB is a pocket-sized database
1,932 lines (1,717 loc) • 306 kB
JavaScript
'use strict';
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
var jsExtend = require('js-extend');
var debug = _interopDefault(require('debug'));
var inherits = _interopDefault(require('inherits'));
var lie = _interopDefault(require('lie'));
var events = require('events');
var getArguments = _interopDefault(require('argsarray'));
var scopedEval = _interopDefault(require('scope-eval'));
var Md5 = _interopDefault(require('spark-md5'));
var vuvuzela = _interopDefault(require('vuvuzela'));
var PromisePool = _interopDefault(require('es6-promise-pool'));
/* istanbul ignore next */
var PouchPromise = typeof Promise === 'function' ? Promise : lie;
// based on https://github.com/montagejs/collections
function mangle(key) {
return '$' + key;
}
function unmangle(key) {
return key.substring(1);
}
function _Map() {
this.store = {};
}
_Map.prototype.get = function (key) {
var mangled = mangle(key);
return this.store[mangled];
};
_Map.prototype.set = function (key, value) {
var mangled = mangle(key);
this.store[mangled] = value;
return true;
};
_Map.prototype.has = function (key) {
var mangled = mangle(key);
return mangled in this.store;
};
_Map.prototype.delete = function (key) {
var mangled = mangle(key);
var res = mangled in this.store;
delete this.store[mangled];
return res;
};
_Map.prototype.forEach = function (cb) {
var keys = Object.keys(this.store);
for (var i = 0, len = keys.length; i < len; i++) {
var key = keys[i];
var value = this.store[key];
key = unmangle(key);
cb(value, key);
}
};
function _Set(array) {
this.store = new _Map();
// init with an array
if (array && Array.isArray(array)) {
for (var i = 0, len = array.length; i < len; i++) {
this.add(array[i]);
}
}
}
_Set.prototype.add = function (key) {
return this.store.set(key, true);
};
_Set.prototype.has = function (key) {
return this.store.has(key);
};
function isBinaryObject(object) {
return (typeof ArrayBuffer !== 'undefined' && object instanceof ArrayBuffer) ||
(typeof Blob !== 'undefined' && object instanceof Blob);
}
function cloneArrayBuffer(buff) {
if (typeof buff.slice === 'function') {
return buff.slice(0);
}
// IE10-11 slice() polyfill
var target = new ArrayBuffer(buff.byteLength);
var targetArray = new Uint8Array(target);
var sourceArray = new Uint8Array(buff);
targetArray.set(sourceArray);
return target;
}
function cloneBinaryObject(object) {
if (object instanceof ArrayBuffer) {
return cloneArrayBuffer(object);
}
var size = object.size;
var type = object.type;
// Blob
if (typeof object.slice === 'function') {
return object.slice(0, size, type);
}
// PhantomJS slice() replacement
return object.webkitSlice(0, size, type);
}
// most of this is borrowed from lodash.isPlainObject:
// https://github.com/fis-components/lodash.isplainobject/
// blob/29c358140a74f252aeb08c9eb28bef86f2217d4a/index.js
var funcToString = Function.prototype.toString;
var objectCtorString = funcToString.call(Object);
function isPlainObject(value) {
var proto = Object.getPrototypeOf(value);
/* istanbul ignore if */
if (proto === null) { // not sure when this happens, but I guess it can
return true;
}
var Ctor = proto.constructor;
return (typeof Ctor == 'function' &&
Ctor instanceof Ctor && funcToString.call(Ctor) == objectCtorString);
}
function clone(object) {
var newObject;
var i;
var len;
if (!object || typeof object !== 'object') {
return object;
}
if (Array.isArray(object)) {
newObject = [];
for (i = 0, len = object.length; i < len; i++) {
newObject[i] = clone(object[i]);
}
return newObject;
}
// special case: to avoid inconsistencies between IndexedDB
// and other backends, we automatically stringify Dates
if (object instanceof Date) {
return object.toISOString();
}
if (isBinaryObject(object)) {
return cloneBinaryObject(object);
}
if (!isPlainObject(object)) {
return object; // don't clone objects like Workers
}
newObject = {};
for (i in object) {
/* istanbul ignore else */
if (Object.prototype.hasOwnProperty.call(object, i)) {
var value = clone(object[i]);
if (typeof value !== 'undefined') {
newObject[i] = value;
}
}
}
return newObject;
}
function once(fun) {
var called = false;
return getArguments(function (args) {
/* istanbul ignore if */
if (called) {
// this is a smoke test and should never actually happen
throw new Error('once called more than once');
} else {
called = true;
fun.apply(this, args);
}
});
}
function toPromise(func) {
//create the function we will be returning
return getArguments(function (args) {
// Clone arguments
args = clone(args);
var self = this;
var tempCB =
(typeof args[args.length - 1] === 'function') ? args.pop() : false;
// if the last argument is a function, assume its a callback
var usedCB;
if (tempCB) {
// if it was a callback, create a new callback which calls it,
// but do so async so we don't trap any errors
usedCB = function (err, resp) {
process.nextTick(function () {
tempCB(err, resp);
});
};
}
var promise = new PouchPromise(function (fulfill, reject) {
var resp;
try {
var callback = once(function (err, mesg) {
if (err) {
reject(err);
} else {
fulfill(mesg);
}
});
// create a callback for this invocation
// apply the function in the orig context
args.push(callback);
resp = func.apply(self, args);
if (resp && typeof resp.then === 'function') {
fulfill(resp);
}
} catch (e) {
reject(e);
}
});
// if there is a callback, call it back
if (usedCB) {
promise.then(function (result) {
usedCB(null, result);
}, usedCB);
}
return promise;
});
}
var log = debug('pouchdb:api');
function adapterFun(name, callback) {
function logApiCall(self, name, args) {
/* istanbul ignore if */
if (log.enabled) {
var logArgs = [self.name, name];
for (var i = 0; i < args.length - 1; i++) {
logArgs.push(args[i]);
}
log.apply(null, logArgs);
// override the callback itself to log the response
var origCallback = args[args.length - 1];
args[args.length - 1] = function (err, res) {
var responseArgs = [self.name, name];
responseArgs = responseArgs.concat(
err ? ['error', err] : ['success', res]
);
log.apply(null, responseArgs);
origCallback(err, res);
};
}
}
return toPromise(getArguments(function (args) {
if (this._closed) {
return PouchPromise.reject(new Error('database is closed'));
}
if (this._destroyed) {
return PouchPromise.reject(new Error('database is destroyed'));
}
var self = this;
logApiCall(self, name, args);
if (!this.taskqueue.isReady) {
return new PouchPromise(function (fulfill, reject) {
self.taskqueue.addTask(function (failed) {
if (failed) {
reject(failed);
} else {
fulfill(self[name].apply(self, args));
}
});
});
}
return callback.apply(this, args);
}));
}
// like underscore/lodash _.pick()
function pick(obj, arr) {
var res = {};
for (var i = 0, len = arr.length; i < len; i++) {
var prop = arr[i];
if (prop in obj) {
res[prop] = obj[prop];
}
}
return res;
}
// Most browsers throttle concurrent requests at 6, so it's silly
// to shim _bulk_get by trying to launch potentially hundreds of requests
// and then letting the majority time out. We can handle this ourselves.
var MAX_NUM_CONCURRENT_REQUESTS = 6;
function identityFunction(x) {
return x;
}
function formatResultForOpenRevsGet(result) {
return [{
ok: result
}];
}
// shim for P/CouchDB adapters that don't directly implement _bulk_get
function bulkGet(db, opts, callback) {
var requests = opts.docs;
// consolidate into one request per doc if possible
var requestsById = {};
requests.forEach(function (request) {
if (request.id in requestsById) {
requestsById[request.id].push(request);
} else {
requestsById[request.id] = [request];
}
});
var numDocs = Object.keys(requestsById).length;
var numDone = 0;
var perDocResults = new Array(numDocs);
function collapseResultsAndFinish() {
var results = [];
perDocResults.forEach(function (res) {
res.docs.forEach(function (info) {
results.push({
id: res.id,
docs: [info]
});
});
});
callback(null, {results: results});
}
function checkDone() {
if (++numDone === numDocs) {
collapseResultsAndFinish();
}
}
function gotResult(docIndex, id, docs) {
perDocResults[docIndex] = {id: id, docs: docs};
checkDone();
}
var allRequests = Object.keys(requestsById);
var i = 0;
function nextBatch() {
if (i >= allRequests.length) {
return;
}
var upTo = Math.min(i + MAX_NUM_CONCURRENT_REQUESTS, allRequests.length);
var batch = allRequests.slice(i, upTo);
processBatch(batch, i);
i += batch.length;
}
function processBatch(batch, offset) {
batch.forEach(function (docId, j) {
var docIdx = offset + j;
var docRequests = requestsById[docId];
// just use the first request as the "template"
// TODO: The _bulk_get API allows for more subtle use cases than this,
// but for now it is unlikely that there will be a mix of different
// "atts_since" or "attachments" in the same request, since it's just
// replicate.js that is using this for the moment.
// Also, atts_since is aspirational, since we don't support it yet.
var docOpts = pick(docRequests[0], ['atts_since', 'attachments']);
docOpts.open_revs = docRequests.map(function (request) {
// rev is optional, open_revs disallowed
return request.rev;
});
// remove falsey / undefined revisions
docOpts.open_revs = docOpts.open_revs.filter(identityFunction);
var formatResult = identityFunction;
if (docOpts.open_revs.length === 0) {
delete docOpts.open_revs;
// when fetching only the "winning" leaf,
// transform the result so it looks like an open_revs
// request
formatResult = formatResultForOpenRevsGet;
}
// globally-supplied options
['revs', 'attachments', 'binary', 'ajax'].forEach(function (param) {
if (param in opts) {
docOpts[param] = opts[param];
}
});
db.get(docId, docOpts, function (err, res) {
var result;
/* istanbul ignore if */
if (err) {
result = [{error: err}];
} else {
result = formatResult(res);
}
gotResult(docIdx, docId, result);
nextBatch();
});
});
}
nextBatch();
}
function isChromeApp() {
return (typeof chrome !== "undefined" &&
typeof chrome.storage !== "undefined" &&
typeof chrome.storage.local !== "undefined");
}
var hasLocal;
if (isChromeApp()) {
hasLocal = false;
} else {
try {
localStorage.setItem('_pouch_check_localstorage', 1);
hasLocal = !!localStorage.getItem('_pouch_check_localstorage');
} catch (e) {
hasLocal = false;
}
}
function hasLocalStorage() {
return hasLocal;
}
inherits(Changes$1, events.EventEmitter);
/* istanbul ignore next */
function attachBrowserEvents(self) {
if (isChromeApp()) {
chrome.storage.onChanged.addListener(function (e) {
// make sure it's event addressed to us
if (e.db_name != null) {
//object only has oldValue, newValue members
self.emit(e.dbName.newValue);
}
});
} else if (hasLocalStorage()) {
if (typeof addEventListener !== 'undefined') {
addEventListener("storage", function (e) {
self.emit(e.key);
});
} else { // old IE
window.attachEvent("storage", function (e) {
self.emit(e.key);
});
}
}
}
function Changes$1() {
events.EventEmitter.call(this);
this._listeners = {};
attachBrowserEvents(this);
}
Changes$1.prototype.addListener = function (dbName, id, db, opts) {
/* istanbul ignore if */
if (this._listeners[id]) {
return;
}
var self = this;
var inprogress = false;
function eventFunction() {
/* istanbul ignore if */
if (!self._listeners[id]) {
return;
}
if (inprogress) {
inprogress = 'waiting';
return;
}
inprogress = true;
var changesOpts = pick(opts, [
'style', 'include_docs', 'attachments', 'conflicts', 'filter',
'doc_ids', 'view', 'since', 'query_params', 'binary'
]);
/* istanbul ignore next */
function onError() {
inprogress = false;
}
db.changes(changesOpts).on('change', function (c) {
if (c.seq > opts.since && !opts.cancelled) {
opts.since = c.seq;
opts.onChange(c);
}
}).on('complete', function () {
if (inprogress === 'waiting') {
setTimeout(function (){
eventFunction();
},0);
}
inprogress = false;
}).on('error', onError);
}
this._listeners[id] = eventFunction;
this.on(dbName, eventFunction);
};
Changes$1.prototype.removeListener = function (dbName, id) {
/* istanbul ignore if */
if (!(id in this._listeners)) {
return;
}
events.EventEmitter.prototype.removeListener.call(this, dbName,
this._listeners[id]);
delete this._listeners[id];
};
/* istanbul ignore next */
Changes$1.prototype.notifyLocalWindows = function (dbName) {
//do a useless change on a storage thing
//in order to get other windows's listeners to activate
if (isChromeApp()) {
chrome.storage.local.set({dbName: dbName});
} else if (hasLocalStorage()) {
localStorage[dbName] = (localStorage[dbName] === "a") ? "b" : "a";
}
};
Changes$1.prototype.notify = function (dbName) {
this.emit(dbName);
this.notifyLocalWindows(dbName);
};
function guardedConsole(method) {
/* istanbul ignore else */
if (console !== 'undefined' && method in console) {
var args = Array.prototype.slice.call(arguments, 1);
console[method].apply(console, args);
}
}
function randomNumber(min, max) {
var maxTimeout = 600000; // Hard-coded default of 10 minutes
min = parseInt(min, 10) || 0;
max = parseInt(max, 10);
if (max !== max || max <= min) {
max = (min || 1) << 1; //doubling
} else {
max = max + 1;
}
// In order to not exceed maxTimeout, pick a random value between half of maxTimeout and maxTimeout
if(max > maxTimeout) {
min = maxTimeout >> 1; // divide by two
max = maxTimeout;
}
var ratio = Math.random();
var range = max - min;
return ~~(range * ratio + min); // ~~ coerces to an int, but fast.
}
function defaultBackOff(min) {
var max = 0;
if (!min) {
max = 2000;
}
return randomNumber(min, max);
}
// designed to give info to browser users, who are disturbed
// when they see http errors in the console
function explainError(status, str) {
guardedConsole('info', 'The above ' + status + ' is totally normal. ' + str);
}
inherits(PouchError, Error);
function PouchError(opts) {
Error.call(this, opts.reason);
this.status = opts.status;
this.name = opts.error;
this.message = opts.reason;
this.error = true;
}
PouchError.prototype.toString = function () {
return JSON.stringify({
status: this.status,
name: this.name,
message: this.message,
reason: this.reason
});
};
var UNAUTHORIZED = new PouchError({
status: 401,
error: 'unauthorized',
reason: "Name or password is incorrect."
});
var MISSING_BULK_DOCS = new PouchError({
status: 400,
error: 'bad_request',
reason: "Missing JSON list of 'docs'"
});
var MISSING_DOC = new PouchError({
status: 404,
error: 'not_found',
reason: 'missing'
});
var REV_CONFLICT = new PouchError({
status: 409,
error: 'conflict',
reason: 'Document update conflict'
});
var INVALID_ID = new PouchError({
status: 400,
error: 'bad_request',
reason: '_id field must contain a string'
});
var MISSING_ID = new PouchError({
status: 412,
error: 'missing_id',
reason: '_id is required for puts'
});
var RESERVED_ID = new PouchError({
status: 400,
error: 'bad_request',
reason: 'Only reserved document ids may start with underscore.'
});
var NOT_OPEN = new PouchError({
status: 412,
error: 'precondition_failed',
reason: 'Database not open'
});
var UNKNOWN_ERROR = new PouchError({
status: 500,
error: 'unknown_error',
reason: 'Database encountered an unknown error'
});
var BAD_ARG = new PouchError({
status: 500,
error: 'badarg',
reason: 'Some query argument is invalid'
});
var INVALID_REQUEST = new PouchError({
status: 400,
error: 'invalid_request',
reason: 'Request was invalid'
});
var QUERY_PARSE_ERROR = new PouchError({
status: 400,
error: 'query_parse_error',
reason: 'Some query parameter is invalid'
});
var DOC_VALIDATION = new PouchError({
status: 500,
error: 'doc_validation',
reason: 'Bad special document member'
});
var BAD_REQUEST = new PouchError({
status: 400,
error: 'bad_request',
reason: 'Something wrong with the request'
});
var NOT_AN_OBJECT = new PouchError({
status: 400,
error: 'bad_request',
reason: 'Document must be a JSON object'
});
var DB_MISSING = new PouchError({
status: 404,
error: 'not_found',
reason: 'Database not found'
});
var IDB_ERROR = new PouchError({
status: 500,
error: 'indexed_db_went_bad',
reason: 'unknown'
});
var WSQ_ERROR = new PouchError({
status: 500,
error: 'web_sql_went_bad',
reason: 'unknown'
});
var LDB_ERROR = new PouchError({
status: 500,
error: 'levelDB_went_went_bad',
reason: 'unknown'
});
var FORBIDDEN = new PouchError({
status: 403,
error: 'forbidden',
reason: 'Forbidden by design doc validate_doc_update function'
});
var INVALID_REV = new PouchError({
status: 400,
error: 'bad_request',
reason: 'Invalid rev format'
});
var FILE_EXISTS = new PouchError({
status: 412,
error: 'file_exists',
reason: 'The database could not be created, the file already exists.'
});
var MISSING_STUB = new PouchError({
status: 412,
error: 'missing_stub'
});
var INVALID_URL = new PouchError({
status: 413,
error: 'invalid_url',
reason: 'Provided URL is invalid'
});
function createError(error, reason) {
function CustomPouchError(reason) {
// inherit error properties from our parent error manually
// so as to allow proper JSON parsing.
/* jshint ignore:start */
for (var p in error) {
if (typeof error[p] !== 'function') {
this[p] = error[p];
}
}
/* jshint ignore:end */
if (reason !== undefined) {
this.reason = reason;
}
}
CustomPouchError.prototype = PouchError.prototype;
return new CustomPouchError(reason);
}
function generateErrorFromResponse(err) {
if (typeof err !== 'object') {
var data = err;
err = UNKNOWN_ERROR;
err.data = data;
}
if ('error' in err && err.error === 'conflict') {
err.name = 'conflict';
err.status = 409;
}
if (!('name' in err)) {
err.name = err.error || 'unknown';
}
if (!('status' in err)) {
err.status = 500;
}
if (!('message' in err)) {
err.message = err.message || err.reason;
}
return err;
}
function tryFilter(filter, doc, req) {
try {
return !filter(doc, req);
} catch (err) {
var msg = 'Filter function threw: ' + err.toString();
return createError(BAD_REQUEST, msg);
}
}
function filterChange(opts) {
var req = {};
var hasFilter = opts.filter && typeof opts.filter === 'function';
req.query = opts.query_params;
return function filter(change) {
if (!change.doc) {
// CSG sends events on the changes feed that don't have documents,
// this hack makes a whole lot of existing code robust.
change.doc = {};
}
var filterReturn = hasFilter && tryFilter(opts.filter, change.doc, req);
if (typeof filterReturn === 'object') {
return filterReturn;
}
if (filterReturn) {
return false;
}
if (!opts.include_docs) {
delete change.doc;
} else if (!opts.attachments) {
for (var att in change.doc._attachments) {
/* istanbul ignore else */
if (change.doc._attachments.hasOwnProperty(att)) {
change.doc._attachments[att].stub = true;
}
}
}
return true;
};
}
function flatten(arrs) {
var res = [];
for (var i = 0, len = arrs.length; i < len; i++) {
res = res.concat(arrs[i]);
}
return res;
}
// Determine id an ID is valid
// - invalid IDs begin with an underescore that does not begin '_design' or
// '_local'
// - any other string value is a valid id
// Returns the specific error object for each case
function invalidIdError(id) {
var err;
if (!id) {
err = createError(MISSING_ID);
} else if (typeof id !== 'string') {
err = createError(INVALID_ID);
} else if (/^_/.test(id) && !(/^_(design|local)/).test(id)) {
err = createError(RESERVED_ID);
}
if (err) {
throw err;
}
}
function listenerCount(ee, type) {
return 'listenerCount' in ee ? ee.listenerCount(type) :
events.EventEmitter.listenerCount(ee, type);
}
function parseDesignDocFunctionName(s) {
if (!s) {
return null;
}
var parts = s.split('/');
if (parts.length === 2) {
return parts;
}
if (parts.length === 1) {
return [s, s];
}
return null;
}
function normalizeDesignDocFunctionName(s) {
var normalized = parseDesignDocFunctionName(s);
return normalized ? normalized.join('/') : null;
}
// originally parseUri 1.2.2, now patched by us
// (c) Steven Levithan <stevenlevithan.com>
// MIT License
var keys = ["source", "protocol", "authority", "userInfo", "user", "password",
"host", "port", "relative", "path", "directory", "file", "query", "anchor"];
var qName ="queryKey";
var qParser = /(?:^|&)([^&=]*)=?([^&]*)/g;
// use the "loose" parser
/* jshint maxlen: false */
var parser = /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/;
function parseUri(str) {
var m = parser.exec(str);
var uri = {};
var i = 14;
while (i--) {
var key = keys[i];
var value = m[i] || "";
var encoded = ['user', 'password'].indexOf(key) !== -1;
uri[key] = encoded ? decodeURIComponent(value) : value;
}
uri[qName] = {};
uri[keys[12]].replace(qParser, function ($0, $1, $2) {
if ($1) {
uri[qName][$1] = $2;
}
});
return uri;
}
// this is essentially the "update sugar" function from daleharvey/pouchdb#1388
// the diffFun tells us what delta to apply to the doc. it either returns
// the doc, or false if it doesn't need to do an update after all
function upsert(db, docId, diffFun) {
return new PouchPromise(function (fulfill, reject) {
db.get(docId, function (err, doc) {
if (err) {
/* istanbul ignore next */
if (err.status !== 404) {
return reject(err);
}
doc = {};
}
// the user might change the _rev, so save it for posterity
var docRev = doc._rev;
var newDoc = diffFun(doc);
if (!newDoc) {
// if the diffFun returns falsy, we short-circuit as
// an optimization
return fulfill({updated: false, rev: docRev});
}
// users aren't allowed to modify these values,
// so reset them here
newDoc._id = docId;
newDoc._rev = docRev;
fulfill(tryAndPut(db, newDoc, diffFun));
});
});
}
function tryAndPut(db, doc, diffFun) {
return db.put(doc).then(function (res) {
return {
updated: true,
rev: res.rev
};
}, function (err) {
/* istanbul ignore next */
if (err.status !== 409) {
throw err;
}
return upsert(db, doc._id, diffFun);
});
}
// BEGIN Math.uuid.js
/*!
Math.uuid.js (v1.4)
http://www.broofa.com
mailto:robert@broofa.com
Copyright (c) 2010 Robert Kieffer
Dual licensed under the MIT and GPL licenses.
*/
/*
* Generate a random uuid.
*
* USAGE: Math.uuid(length, radix)
* length - the desired number of characters
* radix - the number of allowable values for each character.
*
* EXAMPLES:
* // No arguments - returns RFC4122, version 4 ID
* >>> Math.uuid()
* "92329D39-6F5C-4520-ABFC-AAB64544E172"
*
* // One argument - returns ID of the specified length
* >>> Math.uuid(15) // 15 character ID (default base=62)
* "VcydxgltxrVZSTV"
*
* // Two arguments - returns ID of the specified length, and radix.
* // (Radix must be <= 62)
* >>> Math.uuid(8, 2) // 8 character ID (base=2)
* "01001010"
* >>> Math.uuid(8, 10) // 8 character ID (base=10)
* "47473046"
* >>> Math.uuid(8, 16) // 8 character ID (base=16)
* "098F4D35"
*/
var chars = (
'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' +
'abcdefghijklmnopqrstuvwxyz'
).split('');
function getValue(radix) {
return 0 | Math.random() * radix;
}
function uuid(len, radix) {
radix = radix || chars.length;
var out = '';
var i = -1;
if (len) {
// Compact form
while (++i < len) {
out += chars[getValue(radix)];
}
return out;
}
// rfc4122, version 4 form
// Fill in random data. At i==19 set the high bits of clock sequence as
// per rfc4122, sec. 4.1.5
while (++i < 36) {
switch (i) {
case 8:
case 13:
case 18:
case 23:
out += '-';
break;
case 19:
out += chars[(getValue(16) & 0x3) | 0x8];
break;
default:
out += chars[getValue(16)];
}
}
return out;
}
// We fetch all leafs of the revision tree, and sort them based on tree length
// and whether they were deleted, undeleted documents with the longest revision
// tree (most edits) win
// The final sort algorithm is slightly documented in a sidebar here:
// http://guide.couchdb.org/draft/conflicts.html
function winningRev(metadata) {
var winningId;
var winningPos;
var winningDeleted;
var toVisit = metadata.rev_tree.slice();
var node;
while ((node = toVisit.pop())) {
var tree = node.ids;
var branches = tree[2];
var pos = node.pos;
if (branches.length) { // non-leaf
for (var i = 0, len = branches.length; i < len; i++) {
toVisit.push({pos: pos + 1, ids: branches[i]});
}
continue;
}
var deleted = !!tree[1].deleted;
var id = tree[0];
// sort by deleted, then pos, then id
if (!winningId || (winningDeleted !== deleted ? winningDeleted :
winningPos !== pos ? winningPos < pos : winningId < id)) {
winningId = id;
winningPos = pos;
winningDeleted = deleted;
}
}
return winningPos + '-' + winningId;
}
// Pretty much all below can be combined into a higher order function to
// traverse revisions
// The return value from the callback will be passed as context to all
// children of that node
function traverseRevTree(revs, callback) {
var toVisit = revs.slice();
var node;
while ((node = toVisit.pop())) {
var pos = node.pos;
var tree = node.ids;
var branches = tree[2];
var newCtx =
callback(branches.length === 0, pos, tree[0], node.ctx, tree[1]);
for (var i = 0, len = branches.length; i < len; i++) {
toVisit.push({pos: pos + 1, ids: branches[i], ctx: newCtx});
}
}
}
function sortByPos(a, b) {
return a.pos - b.pos;
}
function collectLeaves(revs) {
var leaves = [];
traverseRevTree(revs, function (isLeaf, pos, id, acc, opts) {
if (isLeaf) {
leaves.push({rev: pos + "-" + id, pos: pos, opts: opts});
}
});
leaves.sort(sortByPos).reverse();
for (var i = 0, len = leaves.length; i < len; i++) {
delete leaves[i].pos;
}
return leaves;
}
// returns revs of all conflicts that is leaves such that
// 1. are not deleted and
// 2. are different than winning revision
function collectConflicts(metadata) {
var win = winningRev(metadata);
var leaves = collectLeaves(metadata.rev_tree);
var conflicts = [];
for (var i = 0, len = leaves.length; i < len; i++) {
var leaf = leaves[i];
if (leaf.rev !== win && !leaf.opts.deleted) {
conflicts.push(leaf.rev);
}
}
return conflicts;
}
// compact a tree by marking its non-leafs as missing,
// and return a list of revs to delete
function compactTree(metadata) {
var revs = [];
traverseRevTree(metadata.rev_tree, function (isLeaf, pos,
revHash, ctx, opts) {
if (opts.status === 'available' && !isLeaf) {
revs.push(pos + '-' + revHash);
opts.status = 'missing';
}
});
return revs;
}
// build up a list of all the paths to the leafs in this revision tree
function rootToLeaf(revs) {
var paths = [];
var toVisit = revs.slice();
var node;
while ((node = toVisit.pop())) {
var pos = node.pos;
var tree = node.ids;
var id = tree[0];
var opts = tree[1];
var branches = tree[2];
var isLeaf = branches.length === 0;
var history = node.history ? node.history.slice() : [];
history.push({id: id, opts: opts});
if (isLeaf) {
paths.push({pos: (pos + 1 - history.length), ids: history});
}
for (var i = 0, len = branches.length; i < len; i++) {
toVisit.push({pos: pos + 1, ids: branches[i], history: history});
}
}
return paths.reverse();
}
// for a better overview of what this is doing, read:
// https://github.com/apache/couchdb-couch/blob/master/src/couch_key_tree.erl
//
// But for a quick intro, CouchDB uses a revision tree to store a documents
// history, A -> B -> C, when a document has conflicts, that is a branch in the
// tree, A -> (B1 | B2 -> C), We store these as a nested array in the format
//
// KeyTree = [Path ... ]
// Path = {pos: position_from_root, ids: Tree}
// Tree = [Key, Opts, [Tree, ...]], in particular single node: [Key, []]
function sortByPos$1(a, b) {
return a.pos - b.pos;
}
// classic binary search
function binarySearch(arr, item, comparator) {
var low = 0;
var high = arr.length;
var mid;
while (low < high) {
mid = (low + high) >>> 1;
if (comparator(arr[mid], item) < 0) {
low = mid + 1;
} else {
high = mid;
}
}
return low;
}
// assuming the arr is sorted, insert the item in the proper place
function insertSorted(arr, item, comparator) {
var idx = binarySearch(arr, item, comparator);
arr.splice(idx, 0, item);
}
// Turn a path as a flat array into a tree with a single branch.
// If any should be stemmed from the beginning of the array, that's passed
// in as the second argument
function pathToTree(path, numStemmed) {
var root;
var leaf;
for (var i = numStemmed, len = path.length; i < len; i++) {
var node = path[i];
var currentLeaf = [node.id, node.opts, []];
if (leaf) {
leaf[2].push(currentLeaf);
leaf = currentLeaf;
} else {
root = leaf = currentLeaf;
}
}
return root;
}
// compare the IDs of two trees
function compareTree(a, b) {
return a[0] < b[0] ? -1 : 1;
}
// Merge two trees together
// The roots of tree1 and tree2 must be the same revision
function mergeTree(in_tree1, in_tree2) {
var queue = [{tree1: in_tree1, tree2: in_tree2}];
var conflicts = false;
while (queue.length > 0) {
var item = queue.pop();
var tree1 = item.tree1;
var tree2 = item.tree2;
if (tree1[1].status || tree2[1].status) {
tree1[1].status =
(tree1[1].status === 'available' ||
tree2[1].status === 'available') ? 'available' : 'missing';
}
for (var i = 0; i < tree2[2].length; i++) {
if (!tree1[2][0]) {
conflicts = 'new_leaf';
tree1[2][0] = tree2[2][i];
continue;
}
var merged = false;
for (var j = 0; j < tree1[2].length; j++) {
if (tree1[2][j][0] === tree2[2][i][0]) {
queue.push({tree1: tree1[2][j], tree2: tree2[2][i]});
merged = true;
}
}
if (!merged) {
conflicts = 'new_branch';
insertSorted(tree1[2], tree2[2][i], compareTree);
}
}
}
return {conflicts: conflicts, tree: in_tree1};
}
function doMerge(tree, path, dontExpand) {
var restree = [];
var conflicts = false;
var merged = false;
var res;
if (!tree.length) {
return {tree: [path], conflicts: 'new_leaf'};
}
for (var i = 0, len = tree.length; i < len; i++) {
var branch = tree[i];
if (branch.pos === path.pos && branch.ids[0] === path.ids[0]) {
// Paths start at the same position and have the same root, so they need
// merged
res = mergeTree(branch.ids, path.ids);
restree.push({pos: branch.pos, ids: res.tree});
conflicts = conflicts || res.conflicts;
merged = true;
} else if (dontExpand !== true) {
// The paths start at a different position, take the earliest path and
// traverse up until it as at the same point from root as the path we
// want to merge. If the keys match we return the longer path with the
// other merged After stemming we dont want to expand the trees
var t1 = branch.pos < path.pos ? branch : path;
var t2 = branch.pos < path.pos ? path : branch;
var diff = t2.pos - t1.pos;
var candidateParents = [];
var trees = [];
trees.push({ids: t1.ids, diff: diff, parent: null, parentIdx: null});
while (trees.length > 0) {
var item = trees.pop();
if (item.diff === 0) {
if (item.ids[0] === t2.ids[0]) {
candidateParents.push(item);
}
continue;
}
var elements = item.ids[2];
for (var j = 0, elementsLen = elements.length; j < elementsLen; j++) {
trees.push({
ids: elements[j],
diff: item.diff - 1,
parent: item.ids,
parentIdx: j
});
}
}
var el = candidateParents[0];
if (!el) {
restree.push(branch);
} else {
res = mergeTree(el.ids, t2.ids);
el.parent[2][el.parentIdx] = res.tree;
restree.push({pos: t1.pos, ids: t1.ids});
conflicts = conflicts || res.conflicts;
merged = true;
}
} else {
restree.push(branch);
}
}
// We didnt find
if (!merged) {
restree.push(path);
}
restree.sort(sortByPos$1);
return {
tree: restree,
conflicts: conflicts || 'internal_node'
};
}
// To ensure we dont grow the revision tree infinitely, we stem old revisions
function stem(tree, depth) {
// First we break out the tree into a complete list of root to leaf paths
var paths = rootToLeaf(tree);
var maybeStem = {};
var result;
for (var i = 0, len = paths.length; i < len; i++) {
// Then for each path, we cut off the start of the path based on the
// `depth` to stem to, and generate a new set of flat trees
var path = paths[i];
var stemmed = path.ids;
var numStemmed = Math.max(0, stemmed.length - depth);
var stemmedNode = {
pos: path.pos + numStemmed,
ids: pathToTree(stemmed, numStemmed)
};
for (var s = 0; s < numStemmed; s++) {
var rev = (path.pos + s) + '-' + stemmed[s].id;
maybeStem[rev] = true;
}
// Then we remerge all those flat trees together, ensuring that we dont
// connect trees that would go beyond the depth limit
if (result) {
result = doMerge(result, stemmedNode, true).tree;
} else {
result = [stemmedNode];
}
}
traverseRevTree(result, function (isLeaf, pos, revHash) {
// some revisions may have been removed in a branch but not in another
delete maybeStem[pos + '-' + revHash];
});
return {
tree: result,
revs: Object.keys(maybeStem)
};
}
function merge(tree, path, depth) {
var newTree = doMerge(tree, path);
var stemmed = stem(newTree.tree, depth);
return {
tree: stemmed.tree,
stemmedRevs: stemmed.revs,
conflicts: newTree.conflicts
};
}
// return true if a rev exists in the rev tree, false otherwise
function revExists(revs, rev) {
var toVisit = revs.slice();
var splitRev = rev.split('-');
var targetPos = parseInt(splitRev[0], 10);
var targetId = splitRev[1];
var node;
while ((node = toVisit.pop())) {
if (node.pos === targetPos && node.ids[0] === targetId) {
return true;
}
var branches = node.ids[2];
for (var i = 0, len = branches.length; i < len; i++) {
toVisit.push({pos: node.pos + 1, ids: branches[i]});
}
}
return false;
}
function getTrees(node) {
return node.ids;
}
// check if a specific revision of a doc has been deleted
// - metadata: the metadata object from the doc store
// - rev: (optional) the revision to check. defaults to winning revision
function isDeleted(metadata, rev) {
if (!rev) {
rev = winningRev(metadata);
}
var id = rev.substring(rev.indexOf('-') + 1);
var toVisit = metadata.rev_tree.map(getTrees);
var tree;
while ((tree = toVisit.pop())) {
if (tree[0] === id) {
return !!tree[1].deleted;
}
toVisit = toVisit.concat(tree[2]);
}
}
function isLocalId(id) {
return (/^_local/).test(id);
}
function evalFilter(input) {
return scopedEval('"use strict";\nreturn ' + input + ';', {});
}
function evalView(input) {
var code = [
'return function(doc) {',
' "use strict";',
' var emitted = false;',
' var emit = function (a, b) {',
' emitted = true;',
' };',
' var view = ' + input + ';',
' view(doc);',
' if (emitted) {',
' return true;',
' }',
'};'
].join('\n');
return scopedEval(code, {});
}
inherits(Changes, events.EventEmitter);
function tryCatchInChangeListener(self, change) {
// isolate try/catches to avoid V8 deoptimizations
try {
self.emit('change', change);
} catch (e) {
guardedConsole('error', 'Error in .on("change", function):', e);
}
}
function Changes(db, opts, callback) {
events.EventEmitter.call(this);
var self = this;
this.db = db;
opts = opts ? clone(opts) : {};
var complete = opts.complete = once(function (err, resp) {
if (err) {
if (listenerCount(self, 'error') > 0) {
self.emit('error', err);
}
} else {
self.emit('complete', resp);
}
self.removeAllListeners();
db.removeListener('destroyed', onDestroy);
});
if (callback) {
self.on('complete', function (resp) {
callback(null, resp);
});
self.on('error', callback);
}
function onDestroy() {
self.cancel();
}
db.once('destroyed', onDestroy);
opts.onChange = function (change) {
/* istanbul ignore if */
if (opts.isCancelled) {
return;
}
tryCatchInChangeListener(self, change);
};
var promise = new PouchPromise(function (fulfill, reject) {
opts.complete = function (err, res) {
if (err) {
reject(err);
} else {
fulfill(res);
}
};
});
self.once('cancel', function () {
db.removeListener('destroyed', onDestroy);
opts.complete(null, {status: 'cancelled'});
});
this.then = promise.then.bind(promise);
this['catch'] = promise['catch'].bind(promise);
this.then(function (result) {
complete(null, result);
}, complete);
if (!db.taskqueue.isReady) {
db.taskqueue.addTask(function (failed) {
if (failed) {
opts.complete(failed);
} else if (self.isCancelled) {
self.emit('cancel');
} else {
self.doChanges(opts);
}
});
} else {
self.doChanges(opts);
}
}
Changes.prototype.cancel = function () {
this.isCancelled = true;
if (this.db.taskqueue.isReady) {
this.emit('cancel');
}
};
function processChange(doc, metadata, opts) {
var changeList = [{rev: doc._rev}];
if (opts.style === 'all_docs') {
changeList = collectLeaves(metadata.rev_tree)
.map(function (x) { return {rev: x.rev}; });
}
var change = {
id: metadata.id,
changes: changeList,
doc: doc
};
if (isDeleted(metadata, doc._rev)) {
change.deleted = true;
}
if (opts.conflicts) {
change.doc._conflicts = collectConflicts(metadata);
if (!change.doc._conflicts.length) {
delete change.doc._conflicts;
}
}
return change;
}
Changes.prototype.doChanges = function (opts) {
var self = this;
var callback = opts.complete;
opts = clone(opts);
if ('live' in opts && !('continuous' in opts)) {
opts.continuous = opts.live;
}
opts.processChange = processChange;
if (opts.since === 'latest') {
opts.since = 'now';
}
if (!opts.since) {
opts.since = 0;
}
if (opts.since === 'now') {
this.db.info().then(function (info) {
/* istanbul ignore if */
if (self.isCancelled) {
callback(null, {status: 'cancelled'});
return;
}
opts.since = info.update_seq;
self.doChanges(opts);
}, callback);
return;
}
if (opts.view && !opts.filter) {
opts.filter = '_view';
}
if (opts.filter && typeof opts.filter === 'string') {
if (opts.filter === '_view') {
opts.view = normalizeDesignDocFunctionName(opts.view);
} else {
opts.filter = normalizeDesignDocFunctionName(opts.filter);
}
if (this.db.type() !== 'http' && !opts.doc_ids) {
return this.filterChanges(opts);
}
}
if (!('descending' in opts)) {
opts.descending = false;
}
// 0 and 1 should return 1 document
opts.limit = opts.limit === 0 ? 1 : opts.limit;
opts.complete = callback;
var newPromise = this.db._changes(opts);
/* istanbul ignore else */
if (newPromise && typeof newPromise.cancel === 'function') {
var cancel = self.cancel;
self.cancel = getArguments(function (args) {
newPromise.cancel();
cancel.apply(this, args);
});
}
};
Changes.prototype.filterChanges = function (opts) {
var self = this;
var callback = opts.complete;
if (opts.filter === '_view') {
if (!opts.view || typeof opts.view !== 'string') {
var err = createError(BAD_REQUEST,
'`view` filter parameter not found or invalid.');
return callback(err);
}
// fetch a view from a design doc, make it behave like a filter
var viewName = parseDesignDocFunctionName(opts.view);
this.db.get('_design/' + viewName[0], function (err, ddoc) {
/* istanbul ignore if */
if (self.isCancelled) {
return callback(null, {status: 'cancelled'});
}
/* istanbul ignore next */
if (err) {
return callback(generateErrorFromResponse(err));
}
var mapFun = ddoc && ddoc.views && ddoc.views[viewName[1]] &&
ddoc.views[viewName[1]].map;
if (!mapFun) {
return callback(createError(MISSING_DOC,
(ddoc.views ? 'missing json key: ' + viewName[1] :
'missing json key: views')));
}
opts.filter = evalView(mapFun);
self.doChanges(opts);
});
} else {
// fetch a filter from a design doc
var filterName = parseDesignDocFunctionName(opts.filter);
if (!filterName) {
return self.doChanges(opts);
}
this.db.get('_design/' + filterName[0], function (err, ddoc) {
/* istanbul ignore if */
if (self.isCancelled) {
return callback(null, {status: 'cancelled'});
}
/* istanbul ignore next */
if (err) {
return callback(generateErrorFromResponse(err));
}
var filterFun = ddoc && ddoc.filters && ddoc.filters[filterName[1]];
if (!filterFun) {
return callback(createError(MISSING_DOC,
((ddoc && ddoc.filters) ? 'missing json key: ' + filterName[1]
: 'missing json key: filters')));
}
opts.filter = evalFilter(filterFun);
self.doChanges(opts);
});
}
};
/*
* A generic pouch adapter
*/
function compare(left, right) {
return left < right ? -1 : left > right ? 1 : 0;
}
// returns first element of arr satisfying callback predicate
function arrayFirst(arr, callback) {
for (var i = 0; i < arr.length; i++) {
if (callback(arr[i], i) === true) {
return arr[i];
}
}
}
// Wrapper for functions that call the bulkdocs api with a single doc,
// if the first result is an error, return an error
function yankError(callback) {
return function (err, results) {
if (err || (results[0] && results[0].error)) {
callback(err || results[0]);
} else {
callback(null, results.length ? results[0] : results);
}
};
}
// clean docs given to us by the user
function cleanDocs(docs) {
for (var i = 0; i < docs.length; i++) {
var doc = docs[i];
if (doc._deleted) {
delete doc._attachments; // ignore atts for deleted docs
} else if (doc._attachments) {
// filter out extraneous keys from _attachments
var atts = Object.keys(doc._attachments);
for (var j = 0; j < atts.length; j++) {
var att = atts[j];
doc._attachments[att] = pick(doc._attachments[att],
['data', 'digest', 'content_type', 'length', 'revpos', 'stub']);
}
}
}
}
// compare two docs, first by _id then by _rev
function compareByIdThenRev(a, b) {
var idCompare = compare(a._id, b._id);
if (idCompare !== 0) {
return idCompare;
}
var aStart = a._revisions ? a._revisions.start : 0;
var bStart = b._revisions ? b._revisions.start : 0;
return compare(aStart, bStart);
}
// for every node in a revision tree computes its distance from the closest
// leaf
function computeHeight(revs) {
var height = {};
var edges = [];
traverseRevTree(revs, function (isLeaf, pos, id, prnt) {
var rev = pos + "-" + id;
if (isLeaf) {
height[rev] = 0;
}
if (prnt !== undefined) {
edges.push({from: prnt, to: rev});
}
return rev;
});
edges.reverse();
edges.forEach(function (edge) {
if (height[edge.from] === undefined) {
height[edge.from] = 1 + height[edge.to];
} else {
height[edge.from] = Math.min(height[edge.from], 1 + height[edge.to]);
}
});
return height;
}
function allDocsKeysQuery(api, opts, callback) {
var keys = ('limit' in opts) ?
opts.keys.slice(opts.skip, opts.limit + opts.skip) :
(opts.skip > 0) ? opts.keys.slice(opts.skip) : opts.keys;
if (opts.descending) {
keys.reverse();
}
if (!keys.length) {
return api._allDocs({limit: 0}, callback);
}
var finalResults = {
offset: opts.skip
};
return PouchPromise.all(keys.map(function (key) {
var subOpts = jsExtend.extend({key: key, deleted: 'ok'}, opts);
['limit', 'skip', 'keys'].forEach(function (optKey) {
delete subOpts[optKey];
});
return new PouchPromise(function (resolve, reject) {
api._allDocs(subOpts, function (err, res) {
/* istanbul ignore if */
if (err) {
return reject(err);
}
finalResults.total_rows = res.total_rows;
resolve(res.rows[0] || {key: key, error: 'not_found'});
});
});
})).then(function (results) {
finalResults.rows = results;
return finalResults;
});
}
// all compaction is done in a queue, to avoid attaching
// too many listeners at once
function doNextCompaction(self) {
var task = self._compactionQueue[0];
var opts = task.opts;
var callback = task.callback;
self.get('_local/compaction').catch(function () {
return false;
}).then(function (doc) {
if (doc && doc.last_seq) {
opts.last_seq = doc.last_seq;
}
self._compact(opts, function (err, res) {
/* istanbul ignore if */
if (err) {
callback(err);
} else {
callback(null, res);
}
process.nextTick(function () {
self._compactionQueue.shift();
if (self._compactionQueue.length) {
doNextCompaction(self);
}
});
});
});
}
function attachmentNameError(name) {
if (name.charAt(0) === '_') {
return name + 'is not a valid attachment name, attachment ' +
'names cannot start with \'_\'';
}
return false;
}
inherits(AbstractPouchDB, events.EventEmitter);
function AbstractPouchDB() {
events.EventEmitter.call(this);
}
AbstractPouchDB.prototype.post =
adapterFun('post', function (doc, opts, callback) {
if (typeof opts === 'function') {
callback = opts;
opts = {};
}
if (typeof doc !== 'object' || Array.isArray(doc)) {
return callback(createError(NOT_AN_OBJECT));
}
this.bulkDocs({docs: [doc]}, opts, yankError(callback));
});
AbstractPouchDB.prototype.put = adapterFun('put', function (doc, opts, cb) {
if (typeof opts === 'function') {
cb = opts;
opts = {};
}
if (typeof doc !== 'object' || Array.isArray(doc)) {
return cb(createError(NOT_AN_OBJECT));
}
invalidIdError(doc._id);
if (isLocalId(doc._id) && typeof this._putLocal === 'function') {
if (doc._deleted) {
return this._removeLocal(doc, cb);
} else {
return this._putLocal(doc, cb);
}
}
if (typeof this._put === 'function' && opts.new_edits !== false) {
this._put(doc, opts, cb);
} else {
this.bulkDocs({docs: [doc]}, opts, yankError(cb));
}
});
AbstractPouchDB.prototype.putAttachment =
adapterFun('putAttachment', function (docId, attachmentId, rev,
blob, type) {
var api = this;
if (typeof type === 'function') {
type = blob;
blob = rev;
rev = null;
}
// Lets fix in https://github.com/pouchdb/pouchdb/issues/3267
/* istanbul ignore if */
if (typeof type === 'undefined') {
type = blob;
blob = rev;
rev = null;
}
function createAttachment(doc) {
var prevrevpos = '_rev'