UNPKG

pouchdb

Version:

PouchDB is a pocket-sized database

1,932 lines (1,717 loc) 306 kB
'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'