| /*jshint strict: false */ |
| /*global chrome */ |
| var merge = require('./merge'); |
| exports.extend = require('pouchdb-extend'); |
| exports.ajax = require('./deps/ajax'); |
| exports.createBlob = require('./deps/blob'); |
| exports.uuid = require('./deps/uuid'); |
| exports.getArguments = require('argsarray'); |
| var buffer = require('./deps/buffer'); |
| var errors = require('./deps/errors'); |
| var EventEmitter = require('events').EventEmitter; |
| var collections = require('pouchdb-collections'); |
| exports.Map = collections.Map; |
| exports.Set = collections.Set; |
| |
| if (typeof global.Promise === 'function') { |
| exports.Promise = global.Promise; |
| } else { |
| exports.Promise = require('bluebird'); |
| } |
| var Promise = exports.Promise; |
| |
| 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' |
| ]); |
| |
| exports.lastIndexOf = function (str, char) { |
| for (var i = str.length - 1; i >= 0; i--) { |
| if (str.charAt(i) === char) { |
| return i; |
| } |
| } |
| return -1; |
| }; |
| |
| exports.clone = function (obj) { |
| return exports.extend(true, {}, obj); |
| }; |
| |
| // like underscore/lodash _.pick() |
| exports.pick = function (obj, arr) { |
| var res = {}; |
| for (var i = 0, len = arr.length; i < len; i++) { |
| var prop = arr[i]; |
| res[prop] = obj[prop]; |
| } |
| return res; |
| }; |
| |
| exports.inherits = require('inherits'); |
| |
| // 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 |
| exports.invalidIdError = function (id) { |
| var err; |
| if (!id) { |
| err = errors.error(errors.MISSING_ID); |
| } else if (typeof id !== 'string') { |
| err = errors.error(errors.INVALID_ID); |
| } else if (/^_/.test(id) && !(/^_(design|local)/).test(id)) { |
| err = errors.error(errors.RESERVED_ID); |
| } |
| if (err) { |
| throw err; |
| } |
| }; |
| |
| function isChromeApp() { |
| return (typeof chrome !== "undefined" && |
| typeof chrome.storage !== "undefined" && |
| typeof chrome.storage.local !== "undefined"); |
| } |
| |
| // Pretty dumb name for a function, just wraps callback calls so we dont |
| // to if (callback) callback() everywhere |
| exports.call = exports.getArguments(function (args) { |
| if (!args.length) { |
| return; |
| } |
| var fun = args.shift(); |
| if (typeof fun === 'function') { |
| fun.apply(this, args); |
| } |
| }); |
| |
| exports.isLocalId = function (id) { |
| return (/^_local/).test(id); |
| }; |
| |
| // 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 |
| exports.isDeleted = function (metadata, rev) { |
| if (!rev) { |
| rev = merge.winningRev(metadata); |
| } |
| var dashIndex = rev.indexOf('-'); |
| if (dashIndex !== -1) { |
| rev = rev.substring(dashIndex + 1); |
| } |
| var deleted = false; |
| merge.traverseRevTree(metadata.rev_tree, |
| function (isLeaf, pos, id, acc, opts) { |
| if (id === rev) { |
| deleted = !!opts.deleted; |
| } |
| }); |
| |
| return deleted; |
| }; |
| |
| exports.revExists = function (metadata, rev) { |
| var found = false; |
| merge.traverseRevTree(metadata.rev_tree, function (leaf, pos, id) { |
| if ((pos + '-' + id) === rev) { |
| found = true; |
| } |
| }); |
| return found; |
| }; |
| |
| exports.filterChange = function filterChange(opts) { |
| var req = {}; |
| var hasFilter = opts.filter && typeof opts.filter === 'function'; |
| req.query = opts.query_params; |
| |
| return function filter(change) { |
| if (opts.filter && hasFilter && !opts.filter.call(this, change.doc, req)) { |
| return false; |
| } |
| if (!opts.include_docs) { |
| delete change.doc; |
| } else if (!opts.attachments) { |
| for (var att in change.doc._attachments) { |
| if (change.doc._attachments.hasOwnProperty(att)) { |
| change.doc._attachments[att].stub = true; |
| } |
| } |
| } |
| return true; |
| }; |
| }; |
| |
| // Preprocess documents, parse their revisions, assign an id and a |
| // revision for new writes that are missing them, etc |
| exports.parseDoc = function (doc, newEdits) { |
| |
| var nRevNum; |
| var newRevId; |
| var revInfo; |
| var error; |
| var opts = {status: 'available'}; |
| if (doc._deleted) { |
| opts.deleted = true; |
| } |
| |
| function parseRevisionInfo() { |
| revInfo = /^(\d+)-(.+)$/.exec(doc._rev); |
| if (!revInfo) { |
| error = errors.error(errors.INVALID_REV); |
| return error; |
| } |
| return { |
| prefix: parseInt(revInfo[1], 10), |
| id: revInfo[2] |
| }; |
| } |
| |
| if (newEdits) { |
| if (!doc._id) { |
| doc._id = exports.uuid(); |
| } |
| newRevId = exports.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 = [{ |
| pos: doc._revisions.start - doc._revisions.ids.length + 1, |
| ids: doc._revisions.ids.reduce(function (acc, x) { |
| if (acc === null) { |
| return [x, opts, []]; |
| } else { |
| return [x, {status: 'missing'}, [acc]]; |
| } |
| }, null) |
| }]; |
| 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, []] |
| }]; |
| } |
| } |
| |
| exports.invalidIdError(doc._id); |
| |
| doc._rev = [nRevNum, newRevId].join('-'); |
| |
| var result = {metadata : {}, data : {}}; |
| for (var key in doc) { |
| if (doc.hasOwnProperty(key)) { |
| var specialKey = key[0] === '_'; |
| if (specialKey && !reservedWords[key]) { |
| error = errors.error(errors.DOC_VALIDATION, key); |
| error.message = errors.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; |
| }; |
| |
| exports.isCordova = function () { |
| return (typeof cordova !== "undefined" || |
| typeof PhoneGap !== "undefined" || |
| typeof phonegap !== "undefined"); |
| }; |
| |
| exports.hasLocalStorage = function () { |
| if (isChromeApp()) { |
| return false; |
| } |
| try { |
| return localStorage; |
| } catch (e) { |
| return false; |
| } |
| }; |
| exports.Changes = Changes; |
| exports.inherits(Changes, EventEmitter); |
| function Changes() { |
| if (!(this instanceof Changes)) { |
| return new Changes(); |
| } |
| var self = this; |
| EventEmitter.call(this); |
| this.isChrome = isChromeApp(); |
| this.listeners = {}; |
| this.hasLocal = false; |
| if (!this.isChrome) { |
| this.hasLocal = exports.hasLocalStorage(); |
| } |
| if (this.isChrome) { |
| 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 (this.hasLocal) { |
| if (typeof addEventListener !== 'undefined') { |
| addEventListener("storage", function (e) { |
| self.emit(e.key); |
| }); |
| } else { // old IE |
| window.attachEvent("storage", function (e) { |
| self.emit(e.key); |
| }); |
| } |
| } |
| |
| } |
| Changes.prototype.addListener = function (dbName, id, db, opts) { |
| if (this.listeners[id]) { |
| return; |
| } |
| var self = this; |
| var inprogress = false; |
| function eventFunction() { |
| if (!self.listeners[id]) { |
| return; |
| } |
| if (inprogress) { |
| inprogress = 'waiting'; |
| return; |
| } |
| inprogress = true; |
| db.changes({ |
| include_docs: opts.include_docs, |
| attachments: opts.attachments, |
| conflicts: opts.conflicts, |
| continuous: false, |
| descending: false, |
| filter: opts.filter, |
| doc_ids: opts.doc_ids, |
| view: opts.view, |
| since: opts.since, |
| query_params: opts.query_params |
| }).on('change', function (c) { |
| if (c.seq > opts.since && !opts.cancelled) { |
| opts.since = c.seq; |
| exports.call(opts.onChange, c); |
| } |
| }).on('complete', function () { |
| if (inprogress === 'waiting') { |
| process.nextTick(function () { |
| self.notify(dbName); |
| }); |
| } |
| inprogress = false; |
| }).on('error', function () { |
| inprogress = false; |
| }); |
| } |
| this.listeners[id] = eventFunction; |
| this.on(dbName, eventFunction); |
| }; |
| |
| Changes.prototype.removeListener = function (dbName, id) { |
| if (!(id in this.listeners)) { |
| return; |
| } |
| EventEmitter.prototype.removeListener.call(this, dbName, |
| this.listeners[id]); |
| }; |
| |
| |
| Changes.prototype.notifyLocalWindows = function (dbName) { |
| //do a useless change on a storage thing |
| //in order to get other windows's listeners to activate |
| if (this.isChrome) { |
| chrome.storage.local.set({dbName: dbName}); |
| } else if (this.hasLocal) { |
| localStorage[dbName] = (localStorage[dbName] === "a") ? "b" : "a"; |
| } |
| }; |
| |
| Changes.prototype.notify = function (dbName) { |
| this.emit(dbName); |
| this.notifyLocalWindows(dbName); |
| }; |
| |
| if (typeof window === 'undefined' || typeof window.atob !== 'function') { |
| exports.atob = function (str) { |
| var base64 = new buffer(str, 'base64'); |
| // Node.js will just skip the characters it can't encode instead of |
| // throwing and exception |
| if (base64.toString('base64') !== str) { |
| throw ("Cannot base64 encode full string"); |
| } |
| return base64.toString('binary'); |
| }; |
| } else { |
| exports.atob = function (str) { |
| return atob(str); |
| }; |
| } |
| |
| if (typeof window === 'undefined' || typeof window.btoa !== 'function') { |
| exports.btoa = function (str) { |
| return new buffer(str, 'binary').toString('base64'); |
| }; |
| } else { |
| exports.btoa = function (str) { |
| return btoa(str); |
| }; |
| } |
| |
| // From http://stackoverflow.com/questions/14967647/ (continues on next line) |
| // encode-decode-image-with-base64-breaks-image (2013-04-21) |
| exports.fixBinary = function (bin) { |
| if (!process.browser) { |
| // don't need to do this in Node |
| return 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; |
| }; |
| |
| // shim for browsers that don't support it |
| exports.readAsBinaryString = function (blob, callback) { |
| 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(exports.arrayBufferToBinaryString(result)); |
| }; |
| if (hasBinaryString) { |
| reader.readAsBinaryString(blob); |
| } else { |
| reader.readAsArrayBuffer(blob); |
| } |
| }; |
| |
| // simplified API. universal browser support is assumed |
| exports.readAsArrayBuffer = function (blob, callback) { |
| var reader = new FileReader(); |
| reader.onloadend = function (e) { |
| var result = e.target.result || new ArrayBuffer(0); |
| callback(result); |
| }; |
| reader.readAsArrayBuffer(blob); |
| }; |
| |
| exports.once = function (fun) { |
| var called = false; |
| return exports.getArguments(function (args) { |
| if (called) { |
| throw new Error('once called more than once'); |
| } else { |
| called = true; |
| fun.apply(this, args); |
| } |
| }); |
| }; |
| |
| exports.toPromise = function (func) { |
| //create the function we will be returning |
| return exports.getArguments(function (args) { |
| var self = this; |
| var tempCB = |
| (typeof args[args.length - 1] === 'function') ? args.pop() : false; |
| // if the last argument is a function, assume its a callback |
| var usedCB; |
| if (tempCB) { |
| // if it was a callback, create a new callback which calls it, |
| // but do so async so we don't trap any errors |
| usedCB = function (err, resp) { |
| process.nextTick(function () { |
| tempCB(err, resp); |
| }); |
| }; |
| } |
| var promise = new Promise(function (fulfill, reject) { |
| var resp; |
| try { |
| var callback = exports.once(function (err, mesg) { |
| if (err) { |
| reject(err); |
| } else { |
| fulfill(mesg); |
| } |
| }); |
| // create a callback for this invocation |
| // apply the function in the orig context |
| args.push(callback); |
| resp = func.apply(self, args); |
| if (resp && typeof resp.then === 'function') { |
| fulfill(resp); |
| } |
| } catch (e) { |
| reject(e); |
| } |
| }); |
| // if there is a callback, call it back |
| if (usedCB) { |
| promise.then(function (result) { |
| usedCB(null, result); |
| }, usedCB); |
| } |
| promise.cancel = function () { |
| return this; |
| }; |
| return promise; |
| }); |
| }; |
| |
| exports.adapterFun = function (name, callback) { |
| var log = require('debug')('pouchdb:api'); |
| |
| function logApiCall(self, name, args) { |
| if (!log.enabled) { |
| return; |
| } |
| var logArgs = [self._db_name, name]; |
| for (var i = 0; i < args.length - 1; i++) { |
| logArgs.push(args[i]); |
| } |
| log.apply(null, logArgs); |
| |
| // override the callback itself to log the response |
| var origCallback = args[args.length - 1]; |
| args[args.length - 1] = function (err, res) { |
| var responseArgs = [self._db_name, name]; |
| responseArgs = responseArgs.concat( |
| err ? ['error', err] : ['success', res] |
| ); |
| log.apply(null, responseArgs); |
| origCallback(err, res); |
| }; |
| } |
| |
| |
| return exports.toPromise(exports.getArguments(function (args) { |
| if (this._closed) { |
| return Promise.reject(new Error('database is closed')); |
| } |
| var self = this; |
| logApiCall(self, name, args); |
| if (!this.taskqueue.isReady) { |
| return new exports.Promise(function (fulfill, reject) { |
| self.taskqueue.addTask(function (failed) { |
| if (failed) { |
| reject(failed); |
| } else { |
| fulfill(self[name].apply(self, args)); |
| } |
| }); |
| }); |
| } |
| return callback.apply(this, args); |
| })); |
| }; |
| |
| //Can't find original post, but this is close |
| //http://stackoverflow.com/questions/6965107/ (continues on next line) |
| //converting-between-strings-and-arraybuffers |
| exports.arrayBufferToBinaryString = function (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; |
| }; |
| |
| exports.cancellableFun = function (fun, self, opts) { |
| |
| opts = opts ? exports.clone(true, {}, opts) : {}; |
| |
| var emitter = new EventEmitter(); |
| var oldComplete = opts.complete || function () { }; |
| var complete = opts.complete = exports.once(function (err, resp) { |
| if (err) { |
| oldComplete(err); |
| } else { |
| emitter.emit('end', resp); |
| oldComplete(null, resp); |
| } |
| emitter.removeAllListeners(); |
| }); |
| var oldOnChange = opts.onChange || function () {}; |
| var lastChange = 0; |
| self.on('destroyed', function () { |
| emitter.removeAllListeners(); |
| }); |
| opts.onChange = function (change) { |
| oldOnChange(change); |
| if (change.seq <= lastChange) { |
| return; |
| } |
| lastChange = change.seq; |
| emitter.emit('change', change); |
| if (change.deleted) { |
| emitter.emit('delete', change); |
| } else if (change.changes.length === 1 && |
| change.changes[0].rev.slice(0, 1) === '1-') { |
| emitter.emit('create', change); |
| } else { |
| emitter.emit('update', change); |
| } |
| }; |
| var promise = new Promise(function (fulfill, reject) { |
| opts.complete = function (err, res) { |
| if (err) { |
| reject(err); |
| } else { |
| fulfill(res); |
| } |
| }; |
| }); |
| |
| promise.then(function (result) { |
| complete(null, result); |
| }, complete); |
| |
| // this needs to be overwridden by caller, dont fire complete until |
| // the task is ready |
| promise.cancel = function () { |
| promise.isCancelled = true; |
| if (self.taskqueue.isReady) { |
| opts.complete(null, {status: 'cancelled'}); |
| } |
| }; |
| |
| if (!self.taskqueue.isReady) { |
| self.taskqueue.addTask(function () { |
| if (promise.isCancelled) { |
| opts.complete(null, {status: 'cancelled'}); |
| } else { |
| fun(self, opts, promise); |
| } |
| }); |
| } else { |
| fun(self, opts, promise); |
| } |
| promise.on = emitter.on.bind(emitter); |
| promise.once = emitter.once.bind(emitter); |
| promise.addListener = emitter.addListener.bind(emitter); |
| promise.removeListener = emitter.removeListener.bind(emitter); |
| promise.removeAllListeners = emitter.removeAllListeners.bind(emitter); |
| promise.setMaxListeners = emitter.setMaxListeners.bind(emitter); |
| promise.listeners = emitter.listeners.bind(emitter); |
| promise.emit = emitter.emit.bind(emitter); |
| return promise; |
| }; |
| |
| exports.MD5 = exports.toPromise(require('./deps/md5')); |
| |
| // designed to give info to browser users, who are disturbed |
| // when they see 404s in the console |
| exports.explain404 = function (str) { |
| if (process.browser && 'console' in global && 'info' in console) { |
| console.info('The above 404 is totally normal. ' + str); |
| } |
| }; |
| |
| exports.info = function (str) { |
| if (typeof console !== 'undefined' && 'info' in console) { |
| console.info(str); |
| } |
| }; |
| |
| exports.parseUri = require('./deps/parse-uri'); |
| |
| exports.compare = function (left, right) { |
| return left < right ? -1 : left > right ? 1 : 0; |
| }; |
| |
| exports.updateDoc = function updateDoc(prev, docInfo, results, |
| i, cb, writeDoc, newEdits) { |
| |
| if (exports.revExists(prev, docInfo.metadata.rev)) { |
| results[i] = docInfo; |
| return cb(); |
| } |
| |
| var previouslyDeleted = exports.isDeleted(prev); |
| var deleted = exports.isDeleted(docInfo.metadata); |
| var isRoot = /^1-/.test(docInfo.metadata.rev); |
| |
| if (previouslyDeleted && !deleted && newEdits && isRoot) { |
| var newDoc = docInfo.data; |
| newDoc._rev = merge.winningRev(prev); |
| newDoc._id = docInfo.metadata.id; |
| docInfo = exports.parseDoc(newDoc, newEdits); |
| } |
| |
| var merged = merge.merge(prev.rev_tree, docInfo.metadata.rev_tree[0], 1000); |
| |
| var inConflict = newEdits && (((previouslyDeleted && deleted) || |
| (!previouslyDeleted && merged.conflicts !== 'new_leaf') || |
| (previouslyDeleted && !deleted && merged.conflicts === 'new_branch'))); |
| |
| if (inConflict) { |
| var err = errors.error(errors.REV_CONFLICT); |
| results[i] = err; |
| return cb(); |
| } |
| |
| var newRev = docInfo.metadata.rev; |
| docInfo.metadata.rev_tree = merged.tree; |
| if (prev.rev_map) { |
| docInfo.metadata.rev_map = prev.rev_map; // used by leveldb |
| } |
| |
| // recalculate |
| var winningRev = merge.winningRev(docInfo.metadata); |
| deleted = exports.isDeleted(docInfo.metadata, winningRev); |
| |
| var delta = 0; |
| if (newEdits || winningRev === newRev) { |
| // if newEdits==false and we're pushing existing revisions, |
| // then the only thing that matters is whether this revision |
| // is the winning one, and thus replaces an old one |
| delta = (previouslyDeleted === deleted) ? 0 : |
| previouslyDeleted < deleted ? -1 : 1; |
| } |
| |
| writeDoc(docInfo, winningRev, deleted, cb, true, delta, i); |
| }; |
| |
| exports.processDocs = function processDocs(docInfos, api, fetchedDocs, |
| tx, results, writeDoc, opts, |
| overallCallback) { |
| |
| if (!docInfos.length) { |
| return; |
| } |
| |
| function insertDoc(docInfo, resultsIdx, callback) { |
| // Cant insert new deleted documents |
| var winningRev = merge.winningRev(docInfo.metadata); |
| var deleted = exports.isDeleted(docInfo.metadata, winningRev); |
| if ('was_delete' in opts && deleted) { |
| results[resultsIdx] = errors.error(errors.MISSING_DOC, 'deleted'); |
| return callback(); |
| } |
| |
| var delta = deleted ? 0 : 1; |
| |
| writeDoc(docInfo, winningRev, deleted, callback, false, delta, resultsIdx); |
| } |
| |
| var newEdits = opts.new_edits; |
| var idsToDocs = new exports.Map(); |
| |
| var docsDone = 0; |
| var docsToDo = docInfos.length; |
| |
| function checkAllDocsDone() { |
| if (++docsDone === docsToDo && overallCallback) { |
| overallCallback(); |
| } |
| } |
| |
| docInfos.forEach(function (currentDoc, resultsIdx) { |
| |
| if (currentDoc._id && exports.isLocalId(currentDoc._id)) { |
| api[currentDoc._deleted ? '_removeLocal' : '_putLocal']( |
| currentDoc, {ctx: tx}, function (err) { |
| if (err) { |
| results[resultsIdx] = err; |
| } else { |
| results[resultsIdx] = {ok: true}; |
| } |
| 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)) { |
| exports.updateDoc(fetchedDocs.get(id), currentDoc, results, |
| resultsIdx, docWritten, writeDoc, newEdits); |
| } else { |
| insertDoc(currentDoc, resultsIdx, docWritten); |
| } |
| } |
| nextDoc(); |
| }); |
| }; |
| |
| exports.preprocessAttachments = function preprocessAttachments( |
| docInfos, blobType, callback) { |
| |
| if (!docInfos.length) { |
| return callback(); |
| } |
| |
| var docv = 0; |
| |
| function parseBase64(data) { |
| try { |
| return exports.atob(data); |
| } catch (e) { |
| var err = errors.error(errors.BAD_ARG, |
| 'Attachments need to be base64 encoded'); |
| return {error: err}; |
| } |
| } |
| |
| function preprocessAttachment(att, callback) { |
| if (att.stub) { |
| return callback(); |
| } |
| if (typeof att.data === 'string') { |
| // input is a base64 string |
| |
| var asBinary = parseBase64(att.data); |
| if (asBinary.error) { |
| return callback(asBinary.error); |
| } |
| |
| att.length = asBinary.length; |
| if (blobType === 'blob') { |
| att.data = exports.createBlob([exports.fixBinary(asBinary)], |
| {type: att.content_type}); |
| } else if (blobType === 'base64') { |
| att.data = exports.btoa(asBinary); |
| } else { // binary |
| att.data = asBinary; |
| } |
| exports.MD5(asBinary).then(function (result) { |
| att.digest = 'md5-' + result; |
| callback(); |
| }); |
| } else { // input is a blob |
| exports.readAsArrayBuffer(att.data, function (buff) { |
| if (blobType === 'binary') { |
| att.data = exports.arrayBufferToBinaryString(buff); |
| } else if (blobType === 'base64') { |
| att.data = exports.btoa(exports.arrayBufferToBinaryString(buff)); |
| } |
| exports.MD5(buff).then(function (result) { |
| att.digest = 'md5-' + result; |
| att.length = buff.byteLength; |
| callback(); |
| }); |
| }); |
| } |
| } |
| |
| var overallErr; |
| |
| docInfos.forEach(function (docInfo) { |
| var attachments = docInfo.data && docInfo.data._attachments ? |
| Object.keys(docInfo.data._attachments) : []; |
| var recv = 0; |
| |
| if (!attachments.length) { |
| return done(); |
| } |
| |
| function processedAttachment(err) { |
| overallErr = err; |
| recv++; |
| if (recv === attachments.length) { |
| done(); |
| } |
| } |
| |
| for (var key in docInfo.data._attachments) { |
| if (docInfo.data._attachments.hasOwnProperty(key)) { |
| preprocessAttachment(docInfo.data._attachments[key], |
| processedAttachment); |
| } |
| } |
| }); |
| |
| function done() { |
| docv++; |
| if (docInfos.length === docv) { |
| if (overallErr) { |
| callback(overallErr); |
| } else { |
| callback(); |
| } |
| } |
| } |
| }; |
| |
| // compact a tree by marking its non-leafs as missing, |
| // and return a list of revs to delete |
| exports.compactTree = function compactTree(metadata) { |
| var revs = []; |
| merge.traverseRevTree(metadata.rev_tree, function (isLeaf, pos, |
| revHash, ctx, opts) { |
| if (opts.status === 'available' && !isLeaf) { |
| revs.push(pos + '-' + revHash); |
| opts.status = 'missing'; |
| } |
| }); |
| return revs; |
| }; |
| |
| var vuvuzela = require('vuvuzela'); |
| |
| exports.safeJsonParse = function safeJsonParse(str) { |
| try { |
| return JSON.parse(str); |
| } catch (e) { |
| return vuvuzela.parse(str); |
| } |
| }; |
| |
| exports.safeJsonStringify = function safeJsonStringify(json) { |
| try { |
| return JSON.stringify(json); |
| } catch (e) { |
| return vuvuzela.stringify(json); |
| } |
| }; |