pouchdb
Version:
PouchDB is a pocket-sized database
1,963 lines (1,707 loc) • 93.3 kB
JavaScript
'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_