text: 'use strict';
  
  function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
  
  var levelup = _interopDefault(require('levelup'));
  var sublevel = _interopDefault(require('sublevel-pouchdb'));
  var through2 = require('through2');
  var getArguments = _interopDefault(require('argsarray'));
  var pouchdbCollections = require('pouchdb-collections');
  var Deque = _interopDefault(require('double-ended-queue'));
  var lie = _interopDefault(require('lie'));
  var debug = _interopDefault(require('debug'));
  var events = require('events');
  var inherits = _interopDefault(require('inherits'));
  var Md5 = _interopDefault(require('spark-md5'));
  var vuvuzela = _interopDefault(require('vuvuzela'));
  var jsExtend = require('js-extend');
  var localstoragedown = _interopDefault(require('localstorage-down'));
  
  // 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 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) {
      if (Object.prototype.hasOwnProperty.call(object, i)) {
        var value = clone(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]);
  };
  
  
  /* 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) {
    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();
  }
  
  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 pouchdbCollections.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_
    // huge, and do not require a slower code path just to parse them.
    // We can be pretty sure that a document under 50000 characters
    // will not be so deeply nested as to throw a stack overflow error
    // (depends on the engine and available memory, though, so this is
    // just a hunch). 50000 was chosen based on the average length
    // of this string in our test suite, to try to find a number that covers
    // most of our test cases (26 over this size, 26378 under it).
    if (str.length < 50000) {
      return JSON.parse(str);
    }
    return slowJsonParse(str);
  }
  
  function safeJsonStringify(json) {
    try {
      return JSON.stringify(json);
    } catch (e) {
      /* istanbul ignore next */
      return vuvuzela.stringify(json);
    }
  }
  
  function readAsBlobOrBuffer(storedObject, type) {
    // In the browser, we've stored a binary string. This now comes back as a
    // browserified Node-style Buffer (implemented as a typed array),
    // but we want a Blob instead.
    var byteArray = new Uint8Array(storedObject);
    return createBlob([byteArray], {type: type});
  }
  
  // In the browser, we store a binary string
  function prepareAttachmentForStorage(attData, cb) {
    readAsBinaryString(attData, cb);
  }
  
  function createEmptyBlobOrBuffer(type) {
    return createBlob([''], {type: type});
  }
  
  function getCacheFor(transaction, store) {
    var prefix = store.prefix()[0];
    var cache = transaction._cache;
    var subCache = cache.get(prefix);
    if (!subCache) {
      subCache = new pouchdbCollections.Map();
      cache.set(prefix, subCache);
    }
    return subCache;
  }
  
  function LevelTransaction() {
    this._batch = [];
    this._cache = new pouchdbCollections.Map();
  }
  
  LevelTransaction.prototype.get = function (store, key, callback) {
    var cache = getCacheFor(this, store);
    var exists = cache.get(key);
    if (exists) {
      return process.nextTick(function () {
        callback(null, exists);
      });
    } else if (exists === null) { // deleted marker
      /* istanbul ignore next */
      return process.nextTick(function () {
        callback({name: 'NotFoundError'});
      });
    }
    store.get(key, function (err, res) {
      if (err) {
        /* istanbul ignore else */
        if (err.name === 'NotFoundError') {
          cache.set(key, null);
        }
        return callback(err);
      }
      cache.set(key, res);
      callback(null, res);
    });
  };
  
  LevelTransaction.prototype.batch = function (batch) {
    for (var i = 0, len = batch.length; i < len; i++) {
      var operation = batch[i];
  
      var cache = getCacheFor(this, operation.prefix);
  
      if (operation.type === 'put') {
        cache.set(operation.key, operation.value);
      } else {
        cache.set(operation.key, null);
      }
    }
    this._batch = this._batch.concat(batch);
  };
  
  LevelTransaction.prototype.execute = function (db, callback) {
  
    var keys = new pouchdbCollections.Set();
    var uniqBatches = [];
  
    // remove duplicates; last one wins
    for (var i = this._batch.length - 1; i >= 0; i--) {
      var operation = this._batch[i];
      var lookupKey = operation.prefix.prefix()[0] + '\xff' + operation.key;
      if (keys.has(lookupKey)) {
        continue;
      }
      keys.add(lookupKey);
      uniqBatches.push(operation);
    }
  
    db.batch(uniqBatches, callback);
  };
  
  var DOC_STORE = 'document-store';
  var BY_SEQ_STORE = 'by-sequence';
  var ATTACHMENT_STORE = 'attach-store';
  var BINARY_STORE = 'attach-binary-store';
  var LOCAL_STORE = 'local-store';
  var META_STORE = 'meta-store';
  
  // leveldb barks if we try to open a db multiple times
  // so we cache opened connections here for initstore()
  var dbStores = new pouchdbCollections.Map();
  
  // store the value of update_seq in the by-sequence store the key name will
  // never conflict, since the keys in the by-sequence store are integers
  var UPDATE_SEQ_KEY = '_local_last_update_seq';
  var DOC_COUNT_KEY = '_local_doc_count';
  var UUID_KEY = '_local_uuid';
  
  var MD5_PREFIX = 'md5-';
  
  var safeJsonEncoding = {
    encode: safeJsonStringify,
    decode: safeJsonParse,
    buffer: false,
    type: 'cheap-json'
  };
  
  var levelChanges = new Changes();
  
  // winningRev and deleted are performance-killers, but
  // in newer versions of PouchDB, they are cached on the metadata
  function getWinningRev(metadata) {
    return 'winningRev' in metadata ?
      metadata.winningRev : winningRev(metadata);
  }
  
  function getIsDeleted(metadata, winningRev) {
    return 'deleted' in metadata ?
      metadata.deleted : isDeleted(metadata, winningRev);
  }
  
  function fetchAttachment(att, stores, opts) {
    var type = att.content_type;
    return new PouchPromise(function (resolve, reject) {
      stores.binaryStore.get(att.digest, function (err, buffer) {
        var data;
        if (err) {
          /* istanbul ignore if */
          if (err.name !== 'NotFoundError') {
            return reject(err);
          } else {
            // empty
            if (!opts.binary) {
              data = '';
            } else {
              data = binStringToBluffer('', type);
            }
          }
        } else { // non-empty
          if (opts.binary) {
            data = readAsBlobOrBuffer(buffer, type);
          } else {
            data = buffer.toString('base64');
          }
        }
        delete att.stub;
        delete att.length;
        att.data = data;
        resolve();
      });
    });
  }
  
  function fetchAttachments(results, stores, opts) {
    var atts = [];
    results.forEach(function (row) {
      if (!(row.doc && row.doc._attachments)) {
        return;
      }
      var attNames = Object.keys(row.doc._attachments);
      attNames.forEach(function (attName) {
        var att = row.doc._attachments[attName];
        if (!('data' in att)) {
          atts.push(att);
        }
      });
    });
  
    return PouchPromise.all(atts.map(function (att) {
      return fetchAttachment(att, stores, opts);
    }));
  }
  
  function LevelPouch(opts, callback) {
    opts = clone(opts);
    var api = this;
    var instanceId;
    var stores = {};
    var revLimit = opts.revs_limit;
    var db;
    var name = opts.name;
    // TODO: this is undocumented and unused probably
    /* istanbul ignore else */
    if (typeof opts.createIfMissing === 'undefined') {
      opts.createIfMissing = true;
    }
  
    var leveldown = opts.db;
  
    var dbStore;
    var leveldownName = functionName(leveldown);
    if (dbStores.has(leveldownName)) {
      dbStore = dbStores.get(leveldownName);
    } else {
      dbStore = new pouchdbCollections.Map();
      dbStores.set(leveldownName, dbStore);
    }
    if (dbStore.has(name)) {
      db = dbStore.get(name);
      afterDBCreated();
    } else {
      dbStore.set(name, sublevel(levelup(name, opts, function (err) {
        /* istanbul ignore if */
        if (err) {
          dbStore.delete(name);
          return callback(err);
        }
        db = dbStore.get(name);
        db._docCount  = -1;
        db._queue = new Deque();
        /* istanbul ignore else */
        if (opts.migrate) { // migration for leveldown
          migrate.toSublevel(name, db, afterDBCreated);
        } else {
          afterDBCreated();
        }
      })));
    }
  
    function afterDBCreated() {
      stores.docStore = db.sublevel(DOC_STORE, {valueEncoding: safeJsonEncoding});
      stores.bySeqStore = db.sublevel(BY_SEQ_STORE, {valueEncoding: 'json'});
      stores.attachmentStore =
        db.sublevel(ATTACHMENT_STORE, {valueEncoding: 'json'});
      stores.binaryStore = db.sublevel(BINARY_STORE, {valueEncoding: 'binary'});
      stores.localStore = db.sublevel(LOCAL_STORE, {valueEncoding: 'json'});
      stores.metaStore = db.sublevel(META_STORE, {valueEncoding: 'json'});
      migrate.localAndMetaStores(db, stores, function () {
        stores.metaStore.get(UPDATE_SEQ_KEY, function (err, value) {
          if (typeof db._updateSeq === 'undefined') {
            db._updateSeq = value || 0;
          }
          stores.metaStore.get(DOC_COUNT_KEY, function (err, value) {
            db._docCount = !err ? value : 0;
            stores.metaStore.get(UUID_KEY, function (err, value) {
              instanceId = !err ? value : uuid();
              stores.metaStore.put(UUID_KEY, instanceId, function () {
                process.nextTick(function () {
                  callback(null, api);
                });
              });
            });
          });
        });
      });
    }
  
    function countDocs(callback) {
      /* istanbul ignore if */
      if (db.isClosed()) {
        return callback(new Error('database is closed'));
      }
      return callback(null, db._docCount); // use cached value
    }
  
    api.type = function () {
      return 'leveldb';
    };
  
    api._id = function (callback) {
      callback(null, instanceId);
    };
  
    api._info = function (callback) {
      var res = {
        doc_count: db._docCount,
        update_seq: db._updateSeq,
        backend_adapter: functionName(leveldown)
      };
      return process.nextTick(function () {
        callback(null, res);
      });
    };
  
    function tryCode(fun, args) {
      try {
        fun.apply(null, args);
      } catch (err) {
        args[args.length - 1](err);
      }
    }
  
    function executeNext() {
      var firstTask = db._queue.peekFront();
  
      if (firstTask.type === 'read') {
        runReadOperation(firstTask);
      } else { // write, only do one at a time
        runWriteOperation(firstTask);
      }
    }
  
    function runReadOperation(firstTask) {
      // do multiple reads at once simultaneously, because it's safe
  
      var readTasks = [firstTask];
      var i = 1;
      var nextTask = db._queue.get(i);
      while (typeof nextTask !== 'undefined' && nextTask.type === 'read') {
        readTasks.push(nextTask);
        i++;
        nextTask = db._queue.get(i);
      }
  
      var numDone = 0;
  
      readTasks.forEach(function (readTask) {
        var args = readTask.args;
        var callback = args[args.length - 1];
        args[args.length - 1] = getArguments(function (cbArgs) {
          callback.apply(null, cbArgs);
          if (++numDone === readTasks.length) {
            process.nextTick(function () {
              // all read tasks have finished
              readTasks.forEach(function () {
                db._queue.shift();
              });
              if (db._queue.length) {
                executeNext();
              }
            });
          }
        });
        tryCode(readTask.fun, args);
      });
    }
  
    function runWriteOperation(firstTask) {
      var args = firstTask.args;
      var callback = args[args.length - 1];
      args[args.length - 1] = getArguments(function (cbArgs) {
        callback.apply(null, cbArgs);
        process.nextTick(function () {
          db._queue.shift();
          if (db._queue.length) {
            executeNext();
          }
        });
      });
      tryCode(firstTask.fun, args);
    }
  
    // all read/write operations to the database are done in a queue,
    // similar to how websql/idb works. this avoids problems such
    // as e.g. compaction needing to have a lock on the database while
    // it updates stuff. in the future we can revisit this.
    function writeLock(fun) {
      return getArguments(function (args) {
        db._queue.push({
          fun: fun,
          args: args,
          type: 'write'
        });
  
        if (db._queue.length === 1) {
          process.nextTick(executeNext);
        }
      });
    }
  
    // same as the writelock, but multiple can run at once
    function readLock(fun) {
      return getArguments(function (args) {
        db._queue.push({
          fun: fun,
          args: args,
          type: 'read'
        });
  
        if (db._queue.length === 1) {
          process.nextTick(executeNext);
        }
      });
    }
  
    function formatSeq(n) {
      return ('0000000000000000' + n).slice(-16);
    }
  
    function parseSeq(s) {
      return parseInt(s, 10);
    }
  
    api._get = readLock(function (id, opts, callback) {
      opts = clone(opts);
  
      stores.docStore.get(id, function (err, metadata) {
  
        if (err || !metadata) {
          return callback(createError(MISSING_DOC, 'missing'));
        }
  
        var rev = getWinningRev(metadata);
        var deleted = getIsDeleted(metadata, rev);
        if (deleted && !opts.rev) {
          return callback(createError(MISSING_DOC, "deleted"));
        }
  
        rev = opts.rev ? opts.rev : rev;
  
        var seq = metadata.rev_map[rev];
  
        stores.bySeqStore.get(formatSeq(seq), function (err, doc) {
          if (!doc) {
            return callback(createError(MISSING_DOC));
          }
          /* istanbul ignore if */
          if ('_id' in doc && doc._id !== metadata.id) {
            // this failing implies something very wrong
            return callback(new Error('wrong doc returned'));
          }
          doc._id = metadata.id;
          if ('_rev' in doc) {
            /* istanbul ignore if */
            if (doc._rev !== rev) {
              // this failing implies something very wrong
              return callback(new Error('wrong doc returned'));
            }
          } else {
            // we didn't always store this
            doc._rev = rev;
          }
          return callback(null, {doc: doc, metadata: metadata});
        });
      });
    });
  
    // not technically part of the spec, but if putAttachment has its own
    // method...
    api._getAttachment = function (docId, attachId, attachment, opts, callback) {
      var digest = attachment.digest;
      var type = attachment.content_type;
  
      stores.binaryStore.get(digest, function (err, attach) {
        if (err) {
          /* istanbul ignore if */
          if (err.name !== 'NotFoundError') {
            return callback(err);
          }
          // Empty attachment
          return callback(null, opts.binary ? createEmptyBlobOrBuffer(type) : '');
        }
  
        if (opts.binary) {
          callback(null, readAsBlobOrBuffer(attach, type));
        } else {
          callback(null, attach.toString('base64'));
        }
      });
    };
  
    api._bulkDocs = writeLock(function (req, opts, callback) {
      var newEdits = opts.new_edits;
      var results = new Array(req.docs.length);
      var fetchedDocs = new pouchdbCollections.Map();
      var stemmedRevs = new pouchdbCollections.Map();
  
      var txn = new LevelTransaction();
      var docCountDelta = 0;
      var newUpdateSeq = db._updateSeq;
  
      // parse the docs and give each a sequence number
      var userDocs = req.docs;
      var docInfos = userDocs.map(function (doc) {
        if (doc._id && isLocalId(doc._id)) {
          return doc;
        }
        var newDoc = parseDoc(doc, newEdits);
  
        if (newDoc.metadata && !newDoc.metadata.rev_map) {
          newDoc.metadata.rev_map = {};
        }
  
        return newDoc;
      });
      var infoErrors = docInfos.filter(function (doc) {
        return doc.error;
      });
  
      if (infoErrors.length) {
        return callback(infoErrors[0]);
      }
  
      // verify any stub attachments as a precondition test
  
      function verifyAttachment(digest, callback) {
        txn.get(stores.attachmentStore, digest, function (levelErr) {
          if (levelErr) {
            var err = createError(MISSING_STUB,
                                  'unknown stub attachment with digest ' +
                                  digest);
            callback(err);
          } else {
            callback();
          }
        });
      }
  
      function verifyAttachments(finish) {
        var digests = [];
        userDocs.forEach(function (doc) {
          if (doc && doc._attachments) {
            Object.keys(doc._attachments).forEach(function (filename) {
              var att = doc._attachments[filename];
              if (att.stub) {
                digests.push(att.digest);
              }
            });
          }
        });
        if (!digests.length) {
          return finish();
        }
        var numDone = 0;
        var err;
  
        digests.forEach(function (digest) {
          verifyAttachment(digest, function (attErr) {
            if (attErr && !err) {
              err = attErr;
            }
  
            if (++numDone === digests.length) {
              finish(err);
            }
          });
        });
      }
  
      function fetchExistingDocs(finish) {
        var numDone = 0;
        var overallErr;
        function checkDone() {
          if (++numDone === userDocs.length) {
            return finish(overallErr);
          }
        }
  
        userDocs.forEach(function (doc) {
          if (doc._id && isLocalId(doc._id)) {
            // skip local docs
            return checkDone();
          }
          txn.get(stores.docStore, doc._id, function (err, info) {
            if (err) {
              /* istanbul ignore if */
              if (err.name !== 'NotFoundError') {
                overallErr = err;
              }
            } else {
              fetchedDocs.set(doc._id, info);
            }
            checkDone();
          });
        });
      }
  
      function compact(revsMap, callback) {
        var promise = PouchPromise.resolve();
        revsMap.forEach(function (revs, docId) {
          // TODO: parallelize, for now need to be sequential to
          // pass orphaned attachment tests
          promise = promise.then(function () {
            return new PouchPromise(function (resolve, reject) {
              api._doCompactionNoLock(docId, revs, {ctx: txn}, function (err) {
                /* istanbul ignore if */
                if (err) {
                  return reject(err);
                }
                resolve();
              });
            });
          });
        });
  
        promise.then(function () {
          callback();
        }, callback);
      }
  
      function autoCompact(callback) {
        var revsMap = new pouchdbCollections.Map();
        fetchedDocs.forEach(function (metadata, docId) {
          revsMap.set(docId, compactTree(metadata));
        });
        compact(revsMap, callback);
      }
  
      function finish() {
        compact(stemmedRevs, function (error) {
          /* istanbul ignore if */
          if (error) {
            complete(error);
          }
          if (api.auto_compaction) {
            return autoCompact(complete);
          }
          complete();
        });
      }
  
      function writeDoc(docInfo, winningRev, winningRevIsDeleted, newRevIsDeleted,
                        isUpdate, delta, resultsIdx, callback2) {
        docCountDelta += delta;
  
        var err = null;
        var recv = 0;
  
        docInfo.metadata.winningRev = winningRev;
        docInfo.metadata.deleted = winningRevIsDeleted;
  
        docInfo.data._id = docInfo.metadata.id;
        docInfo.data._rev = docInfo.metadata.rev;
  
        if (newRevIsDeleted) {
          docInfo.data._deleted = true;
        }
  
        if (docInfo.stemmedRevs.length) {
          stemmedRevs.set(docInfo.metadata.id, docInfo.stemmedRevs);
        }
  
        var attachments = docInfo.data._attachments ?
          Object.keys(docInfo.data._attachments) :
          [];
  
        function attachmentSaved(attachmentErr) {
          recv++;
          if (!err) {
            /* istanbul ignore if */
            if (attachmentErr) {
              err = attachmentErr;
              callback2(err);
            } else if (recv === attachments.length) {
              finish();
            }
          }
        }
  
        function onMD5Load(doc, key, data, attachmentSaved) {
          return function (result) {
            saveAttachment(doc, MD5_PREFIX + result, key, data, attachmentSaved);
          };
        }
  
        function doMD5(doc, key, attachmentSaved) {
          return function (data) {
            binaryMd5(data, onMD5Load(doc, key, data, attachmentSaved));
          };
        }
  
        for (var i = 0; i < attachments.length; i++) {
          var key = attachments[i];
          var att = docInfo.data._attachments[key];
  
          if (att.stub) {
            // still need to update the refs mapping
            var id = docInfo.data._id;
            var rev = docInfo.data._rev;
            saveAttachmentRefs(id, rev, att.digest, attachmentSaved);
            continue;
          }
          var data;
          if (typeof att.data === 'string') {
            // input is assumed to be a base64 string
            try {
              data = atob$1(att.data);
            } catch (e) {
              callback(createError(BAD_ARG,
                       'Attachment is not a valid base64 string'));
              return;
            }
            doMD5(docInfo, key, attachmentSaved)(data);
          } else {
            prepareAttachmentForStorage(att.data,
              doMD5(docInfo, key, attachmentSaved));
          }
        }
  
        function finish() {
          var seq = docInfo.metadata.rev_map[docInfo.metadata.rev];
          /* istanbul ignore if */
          if (seq) {
            // check that there aren't any existing revisions with the same
            // revision id, else we shouldn't do anything
            return callback2();
          }
          seq = ++newUpdateSeq;
          docInfo.metadata.rev_map[docInfo.metadata.rev] =
            docInfo.metadata.seq = seq;
          var seqKey = formatSeq(seq);
          var batch = [{
            key: seqKey,
            value: docInfo.data,
            prefix: stores.bySeqStore,
            type: 'put'
          }, {
            key: docInfo.metadata.id,
            value: docInfo.metadata,
            prefix: stores.docStore,
            type: 'put'
          }];
          txn.batch(batch);
          results[resultsIdx] = {
            ok: true,
            id: docInfo.metadata.id,
            rev: winningRev
          };
          fetchedDocs.set(docInfo.metadata.id, docInfo.metadata);
          callback2();
        }
  
        if (!attachments.length) {
          finish();
        }
      }
  
      // attachments are queued per-digest, otherwise the refs could be
      // overwritten by concurrent writes in the same bulkDocs session
      var attachmentQueues = {};
  
      function saveAttachmentRefs(id, rev, digest, callback) {
  
        function fetchAtt() {
          return new PouchPromise(function (resolve, reject) {
            txn.get(stores.attachmentStore, digest, function (err, oldAtt) {
              /* istanbul ignore if */
              if (err && err.name !== 'NotFoundError') {
                return reject(err);
              }
              resolve(oldAtt);
            });
          });
        }
  
        function saveAtt(oldAtt) {
          var ref = [id, rev].join('@');
          var newAtt = {};
  
          if (oldAtt) {
            if (oldAtt.refs) {
              // only update references if this attachment already has them
              // since we cannot migrate old style attachments here without
              // doing a full db scan for references
              newAtt.refs = oldAtt.refs;
              newAtt.refs[ref] = true;
            }
          } else {
            newAtt.refs = {};
            newAtt.refs[ref] = true;
          }
  
          return new PouchPromise(function (resolve) {
            txn.batch([{
              type: 'put',
              prefix: stores.attachmentStore,
              key: digest,
              value: newAtt
            }]);
            resolve(!oldAtt);
          });
        }
  
        // put attachments in a per-digest queue, to avoid two docs with the same
        // attachment overwriting each other
        var queue = attachmentQueues[digest] || PouchPromise.resolve();
        attachmentQueues[digest] = queue.then(function () {
          return fetchAtt().then(saveAtt).then(function (isNewAttachment) {
            callback(null, isNewAttachment);
          }, callback);
        });
      }
  
      function saveAttachment(docInfo, digest, key, data, callback) {
        var att = docInfo.data._attachments[key];
        delete att.data;
        att.digest = digest;
        att.length = data.length;
        var id = docInfo.metadata.id;
        var rev = docInfo.metadata.rev;
        att.revpos = parseInt(rev, 10);
  
        saveAttachmentRefs(id, rev, digest, function (err, isNewAttachment) {
          /* istanbul ignore if */
          if (err) {
            return callback(err);
          }
          // do not try to store empty attachments
          if (data.length === 0) {
            return callback(err);
          }
          if (!isNewAttachment) {
            // small optimization - don't bother writing it again
            return callback(err);
          }
          txn.batch([{
            type: 'put',
            prefix: stores.binaryStore,
            key: digest,
            value: new Buffer(data, 'binary')
          }]);
          callback();
        });
      }
  
      function complete(err) {
        /* istanbul ignore if */
        if (err) {
          return process.nextTick(function () {
            callback(err);
          });
        }
        txn.batch([
          {
            prefix: stores.metaStore,
            type: 'put',
            key: UPDATE_SEQ_KEY,
            value: newUpdateSeq
          },
          {
            prefix: stores.metaStore,
            type: 'put',
            key: DOC_COUNT_KEY,
            value: db._docCount + docCountDelta
          }
        ]);
        txn.execute(db, function (err) {
          /* istanbul ignore if */
          if (err) {
            return callback(err);
          }
          db._docCount += docCountDelta;
          db._updateSeq = newUpdateSeq;
          levelChanges.notify(name);
          process.nextTick(function () {
            callback(null, results);
          });
        });
      }
  
      if (!docInfos.length) {
        return callback(null, []);
      }
  
      verifyAttachments(function (err) {
        if (err) {
          return callback(err);
        }
        fetchExistingDocs(function (err) {
          /* istanbul ignore if */
          if (err) {
            return callback(err);
          }
          processDocs(revLimit, docInfos, api, fetchedDocs, txn, results,
                      writeDoc, opts, finish);
        });
      });
    });
    api._allDocs = readLock(function (opts, callback) {
      opts = clone(opts);
      countDocs(function (err, docCount) {
        /* istanbul ignore if */
        if (err) {
          return callback(err);
        }
        var readstreamOpts = {};
        var skip = opts.skip || 0;
        if (opts.startkey) {
          readstreamOpts.gte = opts.startkey;
        }
        if (opts.endkey) {
          readstreamOpts.lte = opts.endkey;
        }
        if (opts.key) {
          readstreamOpts.gte = readstreamOpts.lte = opts.key;
        }
        if (opts.descending) {
          readstreamOpts.reverse = true;
          // switch start and ends
          var tmp = readstreamOpts.lte;
          readstreamOpts.lte = readstreamOpts.gte;
          readstreamOpts.gte = tmp;
        }
        var limit;
        if (typeof opts.limit === 'number') {
          limit = opts.limit;
        }
        if (limit === 0 ||
            ('start' in readstreamOpts && 'end' in readstreamOpts &&
            readstreamOpts.start > readstreamOpts.end)) {
          // should return 0 results when start is greater than end.
          // normally level would "fix" this for us by reversing the order,
          // so short-circuit instead
          return callback(null, {
            total_rows: docCount,
            offset: opts.skip,
            rows: []
          });
        }
        var results = [];
        var docstream = stores.docStore.readStream(readstreamOpts);
  
        var throughStream = through2.obj(function (entry, _, next) {
          var metadata = entry.value;
          // winningRev and deleted are performance-killers, but
          // in newer versions of PouchDB, they are cached on the metadata
          var winningRev = getWinningRev(metadata);
          var deleted = getIsDeleted(metadata, winningRev);
          if (!deleted) {
            if (skip-- > 0) {
              next();
              return;
            } else if (typeof limit === 'number' && limit-- <= 0) {
              docstream.unpipe();
              docstream.destroy();
              next();
              return;
            }
          } else if (opts.deleted !== 'ok') {
            next();
            return;
          }
          function allDocsInner(data) {
            var doc = {
              id: metadata.id,
              key: metadata.id,
              value: {
                rev: winningRev
              }
            };
            if (opts.include_docs) {
              doc.doc = data;
              doc.doc._rev = doc.value.rev;
              if (opts.conflicts) {
                doc.doc._conflicts = collectConflicts(metadata);
              }
              for (var att in doc.doc._attachments) {
                if (doc.doc._attachments.hasOwnProperty(att)) {
                  doc.doc._attachments[att].stub = true;
                }
              }
            }
            if (opts.inclusive_end === false && metadata.id === opts.endkey) {
              return next();
            } else if (deleted) {
              if (opts.deleted === 'ok') {
                doc.value.deleted = true;
                doc.doc = null;
              } else {
                /* istanbul ignore next */
                return next();
              }
            }
            results.push(doc);
            next();
          }
          if (opts.include_docs) {
            var seq = metadata.rev_map[winningRev];
            stores.bySeqStore.get(formatSeq(seq), function (err, data) {
              allDocsInner(data);
            });
          }
          else {
            allDocsInner();
          }
        }, function (next) {
          PouchPromise.resolve().then(function () {
            if (opts.include_docs && opts.attachments) {
              return fetchAttachments(results, stores, opts);
            }
          }).then(function () {
            callback(null, {
              total_rows: docCount,
              offset: opts.skip,
              rows: results
            });
          }, callback);
          next();
        }).on('unpipe', function () {
          throughStream.end();
        });
  
        docstream.on('error', callback);
  
        docstream.pipe(throughStream);
      });
    });
  
    api._changes = function (opts) {
      opts = clone(opts);
  
      if (opts.continuous) {
        var id = name + ':' + uuid();
        levelChanges.addListener(name, id, api, opts);
        levelChanges.notify(name);
        return {
          cancel: function () {
            levelChanges.removeListener(name, id);
          }
        };
      }
  
      var descending = opts.descending;
      var results = [];
      var lastSeq = opts.since || 0;
      var called = 0;
      var streamOpts = {
        reverse: descending
      };
      var limit;
      if ('limit' in opts && opts.limit > 0) {
        limit = opts.limit;
      }
      if (!streamOpts.reverse) {
        streamOpts.start = formatSeq(opts.since || 0);
      }
  
      var docIds = opts.doc_ids && new pouchdbCollections.Set(opts.doc_ids);
      var filter = filterChange(opts);
      var docIdsToMetadata = new pouchdbCollections.Map();
  
      var returnDocs;
      if ('return_docs' in opts) {
        returnDocs = opts.return_docs;
      } else if ('returnDocs' in opts) {
        // TODO: Remove 'returnDocs' in favor of 'return_docs' in a future release
        returnDocs = opts.returnDocs;
      } else {
        returnDocs = true;
      }
  
      function complete() {
        opts.done = true;
        if (returnDocs && opts.limit) {
          /* istanbul ignore if */
          if (opts.limit < results.length) {
            results.length = opts.limit;
          }
        }
        changeStream.unpipe(throughStream);
        changeStream.destroy();
        if (!opts.continuous && !opts.cancelled) {
          if (opts.include_docs && opts.attachments) {
            fetchAttachments(results, stores, opts).then(function () {
              opts.complete(null, {results: results, last_seq: lastSeq});
            });
          } else {
            opts.complete(null, {results: results, last_seq: lastSeq});
          }
        }
      }
      var changeStream = stores.bySeqStore.readStream(streamOpts);
      var throughStream = through2.obj(function (data, _, next) {
        if (limit && called >= limit) {
          complete();
          return next();
        }
        if (opts.cancelled || opts.done) {
          return next();
        }
  
        var seq = parseSeq(data.key);
        var doc = data.value;
  
        if (seq === opts.since && !descending) {
          // couchdb ignores `since` if descending=true
          return next();
        }
  
        if (docIds && !docIds.has(doc._id)) {
          return next();
        }
  
        var metadata;
  
        function onGetMetadata(metadata) {
          var winningRev = getWinningRev(metadata);
  
          function onGetWinningDoc(winningDoc) {
  
            var change = opts.processChange(winningDoc, metadata, opts);
            change.seq = metadata.seq;
  
            var filtered = filter(change);
            if (typeof filtered === 'object') {
              return opts.complete(filtered);
            }
  
            if (filtered) {
              called++;
  
              if (opts.attachments && opts.include_docs) {
                // fetch attachment immediately for the benefit
                // of live listeners
                fetchAttachments([change], stores, opts).then(function () {
                  opts.onChange(change);
                });
              } else {
                opts.onChange(change);
              }
  
              if (returnDocs) {
                results.push(change);
              }
            }
            next();
          }
  
          if (metadata.seq !== seq) {
            // some other seq is later
            return next();
          }
  
          lastSeq = seq;
  
          if (winningRev === doc._rev) {
            return onGetWinningDoc(doc);
          }
  
          // fetch the winner
  
          var winningSeq = metadata.rev_map[winningRev];
  
          stores.bySeqStore.get(formatSeq(winningSeq), function (err, doc) {
            onGetWinningDoc(doc);
          });
        }
  
        metadata = docIdsToMetadata.get(doc._id);
        if (metadata) { // cached
          return onGetMetadata(metadata);
        }
        // metadata not cached, have to go fetch it
        stores.docStore.get(doc._id, function (err, metadata) {
          /* istanbul ignore if */
          if (opts.cancelled || opts.done || db.isClosed() ||
            isLocalId(metadata.id)) {
            return next();
          }
          docIdsToMetadata.set(doc._id, metadata);
          onGetMetadata(metadata);
        });
      }, function (next) {
        if (opts.cancelled) {
          return next();
        }
        if (returnDocs && opts.limit) {
          /* istanbul ignore if */
          if (opts.limit < results.length) {
            results.length = opts.limit;
          }
        }
  
        next();
      }).on('unpipe', function () {
        throughStream.end();
        complete();
      });
      changeStream.pipe(throughStream);
      return {
        cancel: function () {
          opts.cancelled = true;
          complete();
        }
      };
    };
  
    api._close = function (callback) {
      /* istanbul ignore if */
      if (db.isClosed()) {
        return callback(createError(NOT_OPEN));
      }
      db.close(function (err) {
        /* istanbul ignore if */
        if (err) {
          callback(err);
        } else {
          dbStore.delete(name);
          callback();
        }
      });
    };
  
    api._getRevisionTree = function (docId, callback) {
      stores.docStore.get(docId, function (err, metadata) {
        if (err) {
          callback(createError(MISSING_DOC));
        } else {
          callback(null, metadata.rev_tree);
        }
      });
    };
  
    api._doCompaction = writeLock(function (docId, revs, opts, callback) {
      api._doCompactionNoLock(docId, revs, opts, callback);
    });
  
    // the NoLock version is for use by bulkDocs
    api._doCompactionNoLock = function (docId, revs, opts, callback) {
      if (typeof opts === 'function') {
        callback = opts;
        opts = {};
      }
  
      if (!revs.length) {
        return callback();
      }
      var txn = opts.ctx || new LevelTransaction();
  
      txn.get(stores.docStore, docId, function (err, metadata) {
        /* istanbul ignore if */
        if (err) {
          return callback(err);
        }
        var seqs = revs.map(function (rev) {
          var seq = metadata.rev_map[rev];
          delete metadata.rev_map[rev];
          return seq;
        });
        traverseRevTree(metadata.rev_tree, function (isLeaf, pos,
                                                           revHash, ctx, opts) {
          var rev = pos + '-' + revHash;
          if (revs.indexOf(rev) !== -1) {
            opts.status = 'missing';
          }
        });
  
        var batch = [];
        batch.push({
          key: metadata.id,
          value: metadata,
          type: 'put',
          prefix: stores.docStore
        });
  
        var digestMap = {};
        var numDone = 0;
        var overallErr;
        function checkDone(err) {
          /* istanbul ignore if */
          if (err) {
            overallErr = err;
          }
          if (++numDone === revs.length) { // done
            /* istanbul ignore if */
            if (overallErr) {
              return callback(overallErr);
            }
            deleteOrphanedAttachments();
          }
        }
  
        function finish(err) {
          /* istanbul ignore if */
          if (err) {
            return callback(err);
          }
          txn.batch(batch);
          if (opts.ctx) {
            // don't execute immediately
            return callback();
          }
          txn.execute(db, callback);
        }
  
        function deleteOrphanedAttachments() {
          var possiblyOrphanedAttachments = Object.keys(digestMap);
          if (!possiblyOrphanedAttachments.length) {
            return finish();
          }
          var numDone = 0;
          var overallErr;
          function checkDone(err) {
            /* istanbul ignore if */
            if (err) {
              overallErr = err;
            }
            if (++numDone === possiblyOrphanedAttachments.length) {
              finish(overallErr);
            }
          }
          var refsToDelete = new pouchdbCollections.Map();
          revs.forEach(function (rev) {
            refsToDelete.set(docId + '@' + rev, true);
          });
          possiblyOrphanedAttachments.forEach(function (digest) {
            txn.get(stores.attachmentStore, digest, function (err, attData) {
              /* istanbul ignore if */
              if (err) {
                if (err.name === 'NotFoundError') {
                  return checkDone();
                } else {
                  return checkDone(err);
                }
              }
              var refs = Object.keys(attData.refs || {}).filter(function (ref) {
                return !refsToDelete.has(ref);
              });
              var newRefs = {};
              refs.forEach(function (ref) {
                newRefs[ref] = true;
              });
              if (refs.length) { // not orphaned
                batch.push({
                  key: digest,
                  type: 'put',
                  value: {refs: newRefs},
                  prefix: stores.attachmentStore
                });
              } else { // orphaned, can safely delete
                batch = batch.concat([{
                  key: digest,
                  type: 'del',
                  prefix: stores.attachmentStore
                }, {
                  key: digest,
                  type: 'del',
                  prefix: stores.binaryStore
                }]);
              }
              checkDone();
            });
          });
        }
  
        seqs.forEach(function (seq) {
          batch.push({
            key: formatSeq(seq),
            type: 'del',
            prefix: stores.bySeqStore
          });
          txn.get(stores.bySeqStore, formatSeq(seq), function (err, doc) {
            /* istanbul ignore if */
            if (err) {
              if (err.name === 'NotFoundError') {
                return checkDone();
              } else {
                return checkDone(err);
              }
            }
            var atts = Object.keys(doc._attachments || {});
            atts.forEach(function (attName) {
              var digest = doc._attachments[attName].digest;
              digestMap[digest] = true;
            });
            checkDone();
          });
        });
      });
    };
  
    api._getLocal = function (id, callback) {
      stores.localStore.get(id, function (err, doc) {
        if (err) {
          callback(createError(MISSING_DOC));
        } else {
          callback(null, doc);
        }
      });
    };
  
    api._putLocal = function (doc, opts, callback) {
      if (typeof opts === 'function') {
        callback = opts;
        opts = {};
      }
      if (opts.ctx) {
        api._putLocalNoLock(doc, opts, callback);
      } else {
        api._putLocalWithLock(doc, opts, callback);
      }
    };
  
    api._putLocalWithLock = writeLock(function (doc, opts, callback) {
      api._putLocalNoLock(doc, opts, callback);
    });
  
    // the NoLock version is for use by bulkDocs
    api._putLocalNoLock = function (doc, opts, callback) {
      delete doc._revisions; // ignore this, trust the rev
      var oldRev = doc._rev;
      var id = doc._id;
  
      var txn = opts.ctx || new LevelTransaction();
  
      txn.get(stores.localStore, id, function (err, resp) {
        if (err && oldRev) {
          return callback(createError(REV_CONFLICT));
        }
        if (resp && resp._rev !== oldRev) {
          return callback(createError(REV_CONFLICT));
        }
        doc._rev =
            oldRev ? '0-' + (parseInt(oldRev.split('-')[1], 10) + 1) : '0-1';
        var batch = [
          {
            type: 'put',
            prefix: stores.localStore,
            key: id,
            value: doc
          }
        ];
  
        txn.batch(batch);
        var ret = {ok: true, id: doc._id, rev: doc._rev};
  
        if (opts.ctx) {
          // don't execute immediately
          return callback(null, ret);
        }
        txn.execute(db, function (err) {
          /* istanbul ignore if */
          if (err) {
            return callback(err);
          }
          callback(null, ret);
        });
      });
    };
  
    api._removeLocal = function (doc, opts, callback) {
      if (typeof opts === 'function') {
        callback = opts;
        opts = {};
      }
      if (opts.ctx) {
        api._removeLocalNoLock(doc, opts, callback);
      } else {
        api._removeLocalWithLock(doc, opts, callback);
      }
    };
  
    api._removeLocalWithLock = writeLock(function (doc, opts, callback) {
      api._removeLocalNoLock(doc, opts, callback);
    });
  
    // the NoLock version is for use by bulkDocs
    api._removeLocalNoLock = function (doc, opts, callback) {
      var txn = opts.ctx || new LevelTransaction();
      txn.get(stores.localStore, doc._id, function (err, resp) {
        if (err) {
          /* istanbul ignore if */
          if (err.name !== 'NotFoundError') {
            return callback(err);
          } else {
            return callback(createError(MISSING_DOC));
          }
        }
        if (resp._rev !== doc._rev) {
          return callback(createError(REV_CONFLICT));
        }
        txn.batch([{
          prefix: stores.localStore,
          type: 'del',
          key: doc._id
        }]);
        var ret = {ok: true, id: doc._id, rev: '0-0'};
        if (opts.ctx) {
          // don't execute immediately
          return callback(null, ret);
        }
        txn.execute(db, function (err) {
          /* istanbul ignore if */
          if (err) {
            return callback(err);
          }
          callback(null, ret);
        });
      });
    };
  
    // close and delete open leveldb stores
    api._destroy = function (opts, callback) {
      var dbStore;
      var leveldownName = functionName(leveldown);
      /* istanbul ignore else */
      if (dbStores.has(leveldownName)) {
        dbStore = dbStores.get(leveldownName);
      } else {
        return callDestroy(name, callback);
      }
  
      /* istanbul ignore else */
      if (dbStore.has(name)) {
        levelChanges.removeAllListeners(name);
  
        dbStore.get(name).close(function () {
          dbStore.delete(name);
          callDestroy(name, callback);
        });
      } else {
        callDestroy(name, callback);
      }
    };
    function callDestroy(name, cb) {
      leveldown.destroy(name, cb);
    }
  }
  
  function LocalStoragePouch(opts, callback) {
    var _opts = jsExtend.extend({
      db: localstoragedown
    }, opts);
  
    LevelPouch.call(this, _opts, callback);
  }
  
  // overrides for normal LevelDB behavior on Node
  LocalStoragePouch.valid = function () {
    return typeof localStorage !== 'undefined';
  };
  LocalStoragePouch.use_prefix = true;
  
  function LocalStoragePouchPlugin (PouchDB) {
    PouchDB.adapter('localstorage', LocalStoragePouch, true);
  }
  
  var PDB = (typeof PouchDB !== 'undefined') ? PouchDB : require('pouchdb');
  if (!PDB) {
    guardedConsole('error', 'localstorage adapter plugin error: ' +
      'Cannot find global "PouchDB" object! ' +
      'Did you remember to include pouchdb.js?');
  } else {
    LocalStoragePouchPlugin(PDB);
  }