UNPKG

pouchdb

Version:

PouchDB is a pocket-sized database

1,963 lines (1,707 loc) 93.3 kB
'use strict'; function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } var levelup = _interopDefault(require('levelup')); var ltgt = _interopDefault(require('ltgt')); var events = require('events'); var events__default = _interopDefault(events); var inherits = _interopDefault(require('inherits')); var Codec = _interopDefault(require('level-codec')); var ReadableStreamCore = _interopDefault(require('readable-stream')); var through2 = require('through2'); var getArguments = _interopDefault(require('argsarray')); var Deque = _interopDefault(require('double-ended-queue')); var lie = _interopDefault(require('lie')); var debug = _interopDefault(require('debug')); var Md5 = _interopDefault(require('spark-md5')); var vuvuzela = _interopDefault(require('vuvuzela')); var jsExtend = require('js-extend'); var memdown = _interopDefault(require('memdown')); function isFunction(f) { return 'function' === typeof f; } function getPrefix(db) { if (isFunction(db.prefix)) { return db.prefix(); } return db; } function clone(_obj) { var obj = {}; for(var k in _obj) { obj[k] = _obj[k]; } return obj; } function nut(db, precodec, codec) { function encodePrefix(prefix, key, opts1, opts2) { return precodec.encode([ prefix, codec.encodeKey(key, opts1, opts2 ) ]); } function addEncodings(op, prefix) { if(prefix && prefix.options) { op.keyEncoding = op.keyEncoding || prefix.options.keyEncoding; op.valueEncoding = op.valueEncoding || prefix.options.valueEncoding; } return op; } db.open(function () { /* no-op */}); return { apply: function (ops, opts, cb) { opts = opts || {}; var batch = []; var i = -1; var len = ops.length; while (++i < len) { var op = ops[i]; addEncodings(op, op.prefix); op.prefix = getPrefix(op.prefix); batch.push({ key: encodePrefix(op.prefix, op.key, opts, op), value: op.type !== 'del' && codec.encodeValue(op.value, opts, op), type: op.type }); } db.db.batch(batch, opts, cb); }, get: function (key, prefix, opts, cb) { opts.asBuffer = codec.valueAsBuffer(opts); return db.db.get( encodePrefix(prefix, key, opts), opts, function (err, value) { if (err) { cb(err); } else { cb(null, codec.decodeValue(value, opts)); } } ); }, createDecoder: function (opts) { return function (key, value) { return { key: codec.decodeKey(precodec.decode(key)[1], opts), value: codec.decodeValue(value, opts) }; }; }, isClosed: function isClosed() { return db.isClosed(); }, close: function close(cb) { return db.close(cb); }, iterator: function (_opts) { var opts = clone(_opts || {}); var prefix = _opts.prefix || []; function encodeKey(key) { return encodePrefix(prefix, key, opts, {}); } ltgt.toLtgt(_opts, opts, encodeKey, precodec.lowerBound, precodec.upperBound); // if these legacy values are in the options, remove them opts.prefix = null; //************************************************ //hard coded defaults, for now... //TODO: pull defaults and encoding out of levelup. opts.keyAsBuffer = opts.valueAsBuffer = false; //************************************************ //this is vital, otherwise limit: undefined will //create an empty stream. /* istanbul ignore next */ if ('number' !== typeof opts.limit) { opts.limit = -1; } opts.keyAsBuffer = precodec.buffer; opts.valueAsBuffer = codec.valueAsBuffer(opts); function wrapIterator(iterator) { return { next: function (cb) { return iterator.next(cb); }, end: function (cb) { iterator.end(cb); } }; } return wrapIterator(db.db.iterator(opts)); } }; } function NotFoundError(reason) { Error.call(this, reason); } inherits(NotFoundError, Error); NotFoundError.prototype.name = 'NotFoundError'; var EventEmitter$1 = events__default.EventEmitter; var version = "6.5.4"; var sublevel = function (nut, prefix, createStream, options) { var emitter = new EventEmitter$1(); emitter.sublevels = {}; emitter.options = options; emitter.version = version; emitter.methods = {}; prefix = prefix || []; function mergeOpts(opts) { var o = {}; var k; if (options) { for (k in options) { if (typeof options[k] !== 'undefined') { o[k] = options[k]; } } } if (opts) { for (k in opts) { if (typeof opts[k] !== 'undefined') { o[k] = opts[k]; } } } return o; } emitter.put = function (key, value, opts, cb) { if ('function' === typeof opts) { cb = opts; opts = {}; } nut.apply([{ key: key, value: value, prefix: prefix.slice(), type: 'put' }], mergeOpts(opts), function (err) { /* istanbul ignore next */ if (err) { return cb(err); } emitter.emit('put', key, value); cb(null); }); }; emitter.prefix = function () { return prefix.slice(); }; emitter.batch = function (ops, opts, cb) { if ('function' === typeof opts) { cb = opts; opts = {}; } ops = ops.map(function (op) { return { key: op.key, value: op.value, prefix: op.prefix || prefix, keyEncoding: op.keyEncoding, // * valueEncoding: op.valueEncoding, // * (TODO: encodings on sublevel) type: op.type }; }); nut.apply(ops, mergeOpts(opts), function (err) { /* istanbul ignore next */ if (err) { return cb(err); } emitter.emit('batch', ops); cb(null); }); }; emitter.get = function (key, opts, cb) { /* istanbul ignore else */ if ('function' === typeof opts) { cb = opts; opts = {}; } nut.get(key, prefix, mergeOpts(opts), function (err, value) { if (err) { cb(new NotFoundError(err)); } else { cb(null, value); } }); }; emitter.sublevel = function (name, opts) { return emitter.sublevels[name] = emitter.sublevels[name] || sublevel(nut, prefix.concat(name), createStream, mergeOpts(opts)); }; emitter.readStream = emitter.createReadStream = function (opts) { opts = mergeOpts(opts); opts.prefix = prefix; var stream; var it = nut.iterator(opts); stream = createStream(opts, nut.createDecoder(opts)); stream.setIterator(it); return stream; }; emitter.close = function (cb) { nut.close(cb); }; emitter.isOpen = nut.isOpen; emitter.isClosed = nut.isClosed; return emitter; }; /* Copyright (c) 2012-2014 LevelUP contributors * See list at <https://github.com/rvagg/node-levelup#contributing> * MIT License <https://github.com/rvagg/node-levelup/blob/master/LICENSE.md> */ // NOTE: we are fixed to readable-stream@1.0.x for now // for pure Streams2 across Node versions var Readable = ReadableStreamCore.Readable; function ReadStream(options, makeData) { if (!(this instanceof ReadStream)) { return new ReadStream(options, makeData); } Readable.call(this, { objectMode: true, highWaterMark: options.highWaterMark }); // purely to keep `db` around until we're done so it's not GCed if the user doesn't keep a ref this._waiting = false; this._options = options; this._makeData = makeData; } inherits(ReadStream, Readable); ReadStream.prototype.setIterator = function (it) { this._iterator = it; /* istanbul ignore if */ if (this._destroyed) { return it.end(function () {}); } /* istanbul ignore if */ if (this._waiting) { this._waiting = false; return this._read(); } return this; }; ReadStream.prototype._read = function read() { var self = this; /* istanbul ignore if */ if (self._destroyed) { return; } /* istanbul ignore if */ if (!self._iterator) { return this._waiting = true; } self._iterator.next(function (err, key, value) { if (err || (key === undefined && value === undefined)) { if (!err && !self._destroyed) { self.push(null); } return self._cleanup(err); } value = self._makeData(key, value); if (!self._destroyed) { self.push(value); } }); }; ReadStream.prototype._cleanup = function (err) { if (this._destroyed) { return; } this._destroyed = true; var self = this; /* istanbul ignore if */ if (err) { self.emit('error', err); } /* istanbul ignore else */ if (self._iterator) { self._iterator.end(function () { self._iterator = null; self.emit('close'); }); } else { self.emit('close'); } }; ReadStream.prototype.destroy = function () { this._cleanup(); }; var precodec = { encode: function (decodedKey) { return '\xff' + decodedKey[0] + '\xff' + decodedKey[1]; }, decode: function (encodedKeyAsBuffer) { var str = encodedKeyAsBuffer.toString(); var idx = str.indexOf('\xff', 1); return [str.substring(1, idx), str.substring(idx + 1)]; }, lowerBound: '\x00', upperBound: '\xff' }; var codec = new Codec(); function sublevelPouch(db) { return sublevel(nut(db, precodec, codec), [], ReadStream, db.options); } // 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); }; // in the browser, LevelAlt doesn't need the // pre-2.2.0 LevelDB-specific migrations var toSublevel = function (name, db, callback) { process.nextTick(function () { callback(); }); }; var localAndMetaStores = function (db, stores, callback) { process.nextTick(function () { callback(); }); }; var migrate = { toSublevel: toSublevel, localAndMetaStores: localAndMetaStores }; /* istanbul ignore next */ var PouchPromise = typeof Promise === 'function' ? Promise : lie; 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$1(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$1(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$1(object[i]); if (typeof value !== 'undefined') { newObject[i] = value; } } } return newObject; } var log = debug('pouchdb:api'); // 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; } 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, 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() { events.EventEmitter.call(this); this._listeners = {}; attachBrowserEvents(this); } Changes.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.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.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.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); } } 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 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; }; } // shim for Function.prototype.name, // for browsers that don't support it like IE /* istanbul ignore next */ function f() {} var hasName = f.name; var res; // We dont run coverage in IE /* istanbul ignore else */ if (hasName) { res = function (fun) { return fun.name; }; } else { res = function (fun) { return fun.toString().match(/^\s*function\s*(\S*)\s*\(/)[1]; }; } var functionName = 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; } } // 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; } function toObject(array) { return array.reduce(function (obj, item) { obj[item] = true; return obj; }, {}); } // List of top level reserved words for doc var reservedWords = toObject([ '_id', '_rev', '_attachments', '_deleted', '_revisions', '_revs_info', '_conflicts', '_deleted_conflicts', '_local_seq', '_rev_tree', //replication documents '_replication_id', '_replication_state', '_replication_state_time', '_replication_state_reason', '_replication_stats', // Specific to Couchbase Sync Gateway '_removed' ]); // List of reserved words that should end up the document var dataWords = toObject([ '_attachments', //replication documents '_replication_id', '_replication_state', '_replication_state_time', '_replication_state_reason', '_replication_stats' ]); function parseRevisionInfo(rev) { if (!/^\d+\-./.test(rev)) { return createError(INVALID_REV); } var idx = rev.indexOf('-'); var left = rev.substring(0, idx); var right = rev.substring(idx + 1); return { prefix: parseInt(left, 10), id: right }; } function makeRevTreeFromRevisions(revisions, opts) { var pos = revisions.start - revisions.ids.length + 1; var revisionIds = revisions.ids; var ids = [revisionIds[0], opts, []]; for (var i = 1, len = revisionIds.length; i < len; i++) { ids = [revisionIds[i], {status: 'missing'}, [ids]]; } return [{ pos: pos, ids: ids }]; } // Preprocess documents, parse their revisions, assign an id and a // revision for new writes that are missing them, etc function parseDoc(doc, newEdits) { var nRevNum; var newRevId; var revInfo; var opts = {status: 'available'}; if (doc._deleted) { opts.deleted = true; } if (newEdits) { if (!doc._id) { doc._id = uuid(); } newRevId = uuid(32, 16).toLowerCase(); if (doc._rev) { revInfo = parseRevisionInfo(doc._rev); if (revInfo.error) { return revInfo; } doc._rev_tree = [{ pos: revInfo.prefix, ids: [revInfo.id, {status: 'missing'}, [[newRevId, opts, []]]] }]; nRevNum = revInfo.prefix + 1; } else { doc._rev_tree = [{ pos: 1, ids : [newRevId, opts, []] }]; nRevNum = 1; } } else { if (doc._revisions) { doc._rev_tree = makeRevTreeFromRevisions(doc._revisions, opts); nRevNum = doc._revisions.start; newRevId = doc._revisions.ids[0]; } if (!doc._rev_tree) { revInfo = parseRevisionInfo(doc._rev); if (revInfo.error) { return revInfo; } nRevNum = revInfo.prefix; newRevId = revInfo.id; doc._rev_tree = [{ pos: nRevNum, ids: [newRevId, opts, []] }]; } } invalidIdError(doc._id); doc._rev = nRevNum + '-' + newRevId; var result = {metadata : {}, data : {}}; for (var key in doc) { /* istanbul ignore else */ if (Object.prototype.hasOwnProperty.call(doc, key)) { var specialKey = key[0] === '_'; if (specialKey && !reservedWords[key]) { var error = createError(DOC_VALIDATION, key); error.message = DOC_VALIDATION.message + ': ' + key; throw error; } else if (specialKey && !dataWords[key]) { result.metadata[key.slice(1)] = doc[key]; } else { result.data[key] = doc[key]; } } } return result; } // 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); } var atob$1 = function (str) { return atob(str); }; var btoa$1 = function (str) { return btoa(str); }; // Abstracts constructing a Blob object, so it also works in older // browsers that don't support the native Blob constructor (e.g. // old QtWebKit versions, Android < 4.4). function createBlob(parts, properties) { /* global BlobBuilder,MSBlobBuilder,MozBlobBuilder,WebKitBlobBuilder */ parts = parts || []; properties = properties || {}; try { return new Blob(parts, properties); } catch (e) { if (e.name !== "TypeError") { throw e; } var Builder = typeof BlobBuilder !== 'undefined' ? BlobBuilder : typeof MSBlobBuilder !== 'undefined' ? MSBlobBuilder : typeof MozBlobBuilder !== 'undefined' ? MozBlobBuilder : WebKitBlobBuilder; var builder = new Builder(); for (var i = 0; i < parts.length; i += 1) { builder.append(parts[i]); } return builder.getBlob(properties.type); } } // From http://stackoverflow.com/questions/14967647/ (continues on next line) // encode-decode-image-with-base64-breaks-image (2013-04-21) function binaryStringToArrayBuffer(bin) { var length = bin.length; var buf = new ArrayBuffer(length); var arr = new Uint8Array(buf); for (var i = 0; i < length; i++) { arr[i] = bin.charCodeAt(i); } return buf; } function binStringToBluffer(binString, type) { return createBlob([binaryStringToArrayBuffer(binString)], {type: type}); } //Can't find original post, but this is close //http://stackoverflow.com/questions/6965107/ (continues on next line) //converting-between-strings-and-arraybuffers function arrayBufferToBinaryString(buffer) { var binary = ''; var bytes = new Uint8Array(buffer); var length = bytes.byteLength; for (var i = 0; i < length; i++) { binary += String.fromCharCode(bytes[i]); } return binary; } // shim for browsers that don't support it function readAsBinaryString(blob, callback) { if (typeof FileReader === 'undefined') { // fix for Firefox in a web worker // https://bugzilla.mozilla.org/show_bug.cgi?id=901097 return callback(arrayBufferToBinaryString( new FileReaderSync().readAsArrayBuffer(blob))); } var reader = new FileReader(); var hasBinaryString = typeof reader.readAsBinaryString === 'function'; reader.onloadend = function (e) { var result = e.target.result || ''; if (hasBinaryString) { return callback(result); } callback(arrayBufferToBinaryString(result)); }; if (hasBinaryString) { reader.readAsBinaryString(blob); } else { reader.readAsArrayBuffer(blob); } } // simplified API. universal browser support is assumed function readAsArrayBuffer(blob, callback) { if (typeof FileReader === 'undefined') { // fix for Firefox in a web worker: // https://bugzilla.mozilla.org/show_bug.cgi?id=901097 return callback(new FileReaderSync().readAsArrayBuffer(blob)); } var reader = new FileReader(); reader.onloadend = function (e) { var result = e.target.result || new ArrayBuffer(0); callback(result); }; reader.readAsArrayBuffer(blob); } var setImmediateShim = global.setImmediate || global.setTimeout; var MD5_CHUNK_SIZE = 32768; function rawToBase64(raw) { return btoa$1(raw); } function sliceBlob(blob, start, end) { if (blob.webkitSlice) { return blob.webkitSlice(start, end); } return blob.slice(start, end); } function appendBlob(buffer, blob, start, end, callback) { if (start > 0 || end < blob.size) { // only slice blob if we really need to blob = sliceBlob(blob, start, end); } readAsArrayBuffer(blob, function (arrayBuffer) { buffer.append(arrayBuffer); callback(); }); } function appendString(buffer, string, start, end, callback) { if (start > 0 || end < string.length) { // only create a substring if we really need to string = string.substring(start, end); } buffer.appendBinary(string); callback(); } function binaryMd5(data, callback) { var inputIsString = typeof data === 'string'; var len = inputIsString ? data.length : data.size; var chunkSize = Math.min(MD5_CHUNK_SIZE, len); var chunks = Math.ceil(len / chunkSize); var currentChunk = 0; var buffer = inputIsString ? new Md5() : new Md5.ArrayBuffer(); var append = inputIsString ? appendString : appendBlob; function next() { setImmediateShim(loadNextChunk); } function done() { var raw = buffer.end(true); var base64 = rawToBase64(raw); callback(base64); buffer.destroy(); } function loadNextChunk() { var start = currentChunk * chunkSize; var end = start + chunkSize; currentChunk++; if (currentChunk < chunks) { append(buffer, data, start, end, next); } else { append(buffer, data, start, end, done); } } loadNextChunk(); } function updateDoc(revLimit, prev, docInfo, results, i, cb, writeDoc, newEdits) { if (revExists(prev.rev_tree, docInfo.metadata.rev)) { results[i] = docInfo; return cb(); } // sometimes this is pre-calculated. historically not always var previousWinningRev = prev.winningRev || winningRev(prev); var previouslyDeleted = 'deleted' in prev ? prev.deleted : isDeleted(prev, previousWinningRev); var deleted = 'deleted' in docInfo.metadata ? docInfo.metadata.deleted : isDeleted(docInfo.metadata); var isRoot = /^1-/.test(docInfo.metadata.rev); if (previouslyDeleted && !deleted && newEdits && isRoot) { var newDoc = docInfo.data; newDoc._rev = previousWinningRev; newDoc._id = docInfo.metadata.id; docInfo = parseDoc(newDoc, newEdits); } var merged = merge(prev.rev_tree, docInfo.metadata.rev_tree[0], revLimit); var inConflict = newEdits && (((previouslyDeleted && deleted) || (!previouslyDeleted && merged.conflicts !== 'new_leaf') || (previouslyDeleted && !deleted && merged.conflicts === 'new_branch'))); if (inConflict) { var err = createError(REV_CONFLICT); results[i] = err; return cb(); } var newRev = docInfo.metadata.rev; docInfo.metadata.rev_tree = merged.tree; docInfo.stemmedRevs = merged.stemmedRevs || []; /* istanbul ignore else */ if (prev.rev_map) { docInfo.metadata.rev_map = prev.rev_map; // used only by leveldb } // recalculate var winningRev$$ = winningRev(docInfo.metadata); var winningRevIsDeleted = isDeleted(docInfo.metadata, winningRev$$); // calculate the total number of documents that were added/removed, // from the perspective of total_rows/doc_count var delta = (previouslyDeleted === winningRevIsDeleted) ? 0 : previouslyDeleted < winningRevIsDeleted ? -1 : 1; var newRevIsDeleted; if (newRev === winningRev$$) { // if the new rev is the same as the winning rev, we can reuse that value newRevIsDeleted = winningRevIsDeleted; } else { // if they're not the same, then we need to recalculate newRevIsDeleted = isDeleted(docInfo.metadata, newRev); } writeDoc(docInfo, winningRev$$, winningRevIsDeleted, newRevIsDeleted, true, delta, i, cb); } function rootIsMissing(docInfo) { return docInfo.metadata.rev_tree[0].ids[1].status === 'missing'; } function processDocs(revLimit, docInfos, api, fetchedDocs, tx, results, writeDoc, opts, overallCallback) { // Default to 1000 locally revLimit = revLimit || 1000; function insertDoc(docInfo, resultsIdx, callback) { // Cant insert new deleted documents var winningRev$$ = winningRev(docInfo.metadata); var deleted = isDeleted(docInfo.metadata, winningRev$$); if ('was_delete' in opts && deleted) { results[resultsIdx] = createError(MISSING_DOC, 'deleted'); return callback(); } // 4712 - detect whether a new document was inserted with a _rev var inConflict = newEdits && rootIsMissing(docInfo); if (inConflict) { var err = createError(REV_CONFLICT); results[resultsIdx] = err; return callback(); } var delta = deleted ? 0 : 1; writeDoc(docInfo, winningRev$$, deleted, deleted, false, delta, resultsIdx, callback); } var newEdits = opts.new_edits; var idsToDocs = new _Map(); var docsDone = 0; var docsToDo = docInfos.length; function checkAllDocsDone() { if (++docsDone === docsToDo && overallCallback) { overallCallback(); } } docInfos.forEach(function (currentDoc, resultsIdx) { if (currentDoc._id && isLocalId(currentDoc._id)) { var fun = currentDoc._deleted ? '_removeLocal' : '_putLocal'; api[fun](currentDoc, {ctx: tx}, function (err, res) { results[resultsIdx] = err || res; checkAllDocsDone(); }); return; } var id = currentDoc.metadata.id; if (idsToDocs.has(id)) { docsToDo--; // duplicate idsToDocs.get(id).push([currentDoc, resultsIdx]); } else { idsToDocs.set(id, [[currentDoc, resultsIdx]]); } }); // in the case of new_edits, the user can provide multiple docs // with the same id. these need to be processed sequentially idsToDocs.forEach(function (docs, id) { var numDone = 0; function docWritten() { if (++numDone < docs.length) { nextDoc(); } else { checkAllDocsDone(); } } function nextDoc() { var value = docs[numDone]; var currentDoc = value[0]; var resultsIdx = value[1]; if (fetchedDocs.has(id)) { updateDoc(revLimit, fetchedDocs.get(id), currentDoc, results, resultsIdx, docWritten, writeDoc, newEdits); } else { // Ensure stemming applies to new writes as well var merged = merge([], currentDoc.metadata.rev_tree[0], revLimit); currentDoc.metadata.rev_tree = merged.tree; currentDoc.stemmedRevs = merged.stemmedRevs || []; insertDoc(currentDoc, resultsIdx, docWritten); } } nextDoc(); }); } function slowJsonParse(str) { try { return JSON.parse(str); } catch (e) { /* istanbul ignore next */ return vuvuzela.parse(str); } } function safeJsonParse(str) { // try/catch is deoptimized in V8, leading to slower // times than we'd like to have. Most documents are _not_