UNPKG

pouchdb-utils

Version:

Unassorted utilities used by PouchDB.

719 lines (624 loc) 18.9 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } var pouchdbErrors = require('pouchdb-errors'); var EventEmitter = _interopDefault(require('events')); var uuid = require('uuid'); var pouchdbMd5 = require('pouchdb-md5'); function isBinaryObject(object) { return object instanceof Buffer; } var cloneBinaryObject = (buffer) => Buffer.from(buffer); // 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 && isFinite(object)) { return object.toISOString(); } if (isBinaryObject(object)) { return cloneBinaryObject(object); } if (!isPlainObject(object)) { return object; // don't clone objects like Workers } newObject = {}; for (i in object) { /* istanbul ignore else */ if (Object.prototype.hasOwnProperty.call(object, i)) { var value = clone(object[i]); if (typeof value !== 'undefined') { newObject[i] = value; } } } return newObject; } function once(fun) { var called = false; return function (...args) { /* istanbul ignore if */ if (called) { // this is a smoke test and should never actually happen throw new Error('once called more than once'); } else { called = true; fun.apply(this, args); } }; } function toPromise(func) { //create the function we will be returning return function (...args) { // Clone arguments args = clone(args); var self = this; // if the last argument is a function, assume its a callback var usedCB = (typeof args[args.length - 1] === 'function') ? args.pop() : false; var promise = new Promise(function (fulfill, reject) { var resp; try { var callback = once(function (err, mesg) { if (err) { reject(err); } else { fulfill(mesg); } }); // create a callback for this invocation // apply the function in the orig context args.push(callback); resp = func.apply(self, args); if (resp && typeof resp.then === 'function') { fulfill(resp); } } catch (e) { reject(e); } }); // if there is a callback, call it back if (usedCB) { promise.then(function (result) { usedCB(null, result); }, usedCB); } return promise; }; } function logApiCall(self, name, args) { /* istanbul ignore if */ if (self.constructor.listeners('debug').length) { var logArgs = ['api', self.name, name]; for (var i = 0; i < args.length - 1; i++) { logArgs.push(args[i]); } self.constructor.emit('debug', logArgs); // override the callback itself to log the response var origCallback = args[args.length - 1]; args[args.length - 1] = function (err, res) { var responseArgs = ['api', self.name, name]; responseArgs = responseArgs.concat( err ? ['error', err] : ['success', res] ); self.constructor.emit('debug', responseArgs); origCallback(err, res); }; } } function adapterFun(name, callback) { return toPromise(function (...args) { if (this._closed) { return Promise.reject(new Error('database is closed')); } if (this._destroyed) { return Promise.reject(new Error('database is destroyed')); } var self = this; logApiCall(self, name, args); if (!this.taskqueue.isReady) { return new 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); }); } // like underscore/lodash _.pick() function pick(obj, arr) { var res = {}; for (var i = 0, len = arr.length; i < len; i++) { var prop = arr[i]; if (prop in obj) { res[prop] = obj[prop]; } } return res; } // Most browsers throttle concurrent requests at 6, so it's silly // to shim _bulk_get by trying to launch potentially hundreds of requests // and then letting the majority time out. We can handle this ourselves. var MAX_NUM_CONCURRENT_REQUESTS = 6; function identityFunction(x) { return x; } function formatResultForOpenRevsGet(result) { return [{ ok: result }]; } // shim for P/CouchDB adapters that don't directly implement _bulk_get function bulkGet(db, opts, callback) { var requests = opts.docs; // consolidate into one request per doc if possible var requestsById = new Map(); requests.forEach(function (request) { if (requestsById.has(request.id)) { requestsById.get(request.id).push(request); } else { requestsById.set(request.id, [request]); } }); var numDocs = requestsById.size; var numDone = 0; var perDocResults = new Array(numDocs); function collapseResultsAndFinish() { var results = []; perDocResults.forEach(function (res) { res.docs.forEach(function (info) { results.push({ id: res.id, docs: [info] }); }); }); callback(null, {results}); } function checkDone() { if (++numDone === numDocs) { collapseResultsAndFinish(); } } function gotResult(docIndex, id, docs) { perDocResults[docIndex] = {id, docs}; checkDone(); } var allRequests = []; requestsById.forEach(function (value, key) { allRequests.push(key); }); var i = 0; function nextBatch() { if (i >= allRequests.length) { return; } var upTo = Math.min(i + MAX_NUM_CONCURRENT_REQUESTS, allRequests.length); var batch = allRequests.slice(i, upTo); processBatch(batch, i); i += batch.length; } function processBatch(batch, offset) { batch.forEach(function (docId, j) { var docIdx = offset + j; var docRequests = requestsById.get(docId); // just use the first request as the "template" // TODO: The _bulk_get API allows for more subtle use cases than this, // but for now it is unlikely that there will be a mix of different // "atts_since" or "attachments" in the same request, since it's just // replicate.js that is using this for the moment. // Also, atts_since is aspirational, since we don't support it yet. var docOpts = pick(docRequests[0], ['atts_since', 'attachments']); docOpts.open_revs = docRequests.map(function (request) { // rev is optional, open_revs disallowed return request.rev; }); // remove falsey / undefined revisions docOpts.open_revs = docOpts.open_revs.filter(identityFunction); var formatResult = identityFunction; if (docOpts.open_revs.length === 0) { delete docOpts.open_revs; // when fetching only the "winning" leaf, // transform the result so it looks like an open_revs // request formatResult = formatResultForOpenRevsGet; } // globally-supplied options ['revs', 'attachments', 'binary', 'ajax', 'latest'].forEach(function (param) { if (param in opts) { docOpts[param] = opts[param]; } }); db.get(docId, docOpts, function (err, res) { var result; /* istanbul ignore if */ if (err) { result = [{error: err}]; } else { result = formatResult(res); } gotResult(docIdx, docId, result); nextBatch(); }); }); } nextBatch(); } // in Node of course this is false function hasLocalStorage() { return false; } const nextTick = typeof queueMicrotask === "function" ? queueMicrotask : function nextTick(fn) { Promise.resolve().then(fn); }; class Changes extends EventEmitter { constructor() { super(); this._listeners = {}; if (hasLocalStorage()) { addEventListener("storage", (e) => { this.emit(e.key); }); } } addListener(dbName, id, db, opts) { if (this._listeners[id]) { return; } var inprogress = false; var self = this; function eventFunction() { 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', 'return_docs' ]); 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') { nextTick(eventFunction); } inprogress = false; }).on('error', onError); } this._listeners[id] = eventFunction; this.on(dbName, eventFunction); } removeListener(dbName, id) { if (!(id in this._listeners)) { return; } super.removeListener(dbName, this._listeners[id]); delete this._listeners[id]; } notifyLocalWindows(dbName) { //do a useless change on a storage thing //in order to get other windows's listeners to activate if (hasLocalStorage()) { localStorage[dbName] = (localStorage[dbName] === "a") ? "b" : "a"; } } notify(dbName) { this.emit(dbName); this.notifyLocalWindows(dbName); } } function guardedConsole(method) { /* istanbul ignore else */ if (typeof console !== 'undefined' && typeof console[method] === 'function') { var args = Array.prototype.slice.call(arguments, 1); console[method].apply(console, args); } } function randomNumber(min, max) { var maxTimeout = 600000; // Hard-coded default of 10 minutes min = parseInt(min, 10) || 0; max = parseInt(max, 10); if (max !== max || max <= min) { max = (min || 1) << 1; //doubling } else { max = max + 1; } // In order to not exceed maxTimeout, pick a random value between half of maxTimeout and maxTimeout if (max > maxTimeout) { min = maxTimeout >> 1; // divide by two max = maxTimeout; } var ratio = Math.random(); var range = max - min; return ~~(range * ratio + min); // ~~ coerces to an int, but fast. } function defaultBackOff(min) { var max = 0; if (!min) { max = 2000; } return randomNumber(min, max); } // We assume Node users don't need to see this warning var res = function () {}; function tryFilter(filter, doc, req) { try { return !filter(doc, req); } catch (err) { var msg = 'Filter function threw: ' + err.toString(); return pouchdbErrors.createError(pouchdbErrors.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 (Object.prototype.hasOwnProperty.call(change.doc._attachments, 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$1; // We don't run coverage in IE /* istanbul ignore else */ if (hasName) { res$1 = function (fun) { return fun.name; }; } else { res$1 = function (fun) { var match = fun.toString().match(/^\s*function\s*(?:(\S+)\s*)?\(/); if (match && match[1]) { return match[1]; } else { return ''; } }; } var res$2 = res$1; // 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 = pouchdbErrors.createError(pouchdbErrors.MISSING_ID); } else if (typeof id !== 'string') { err = pouchdbErrors.createError(pouchdbErrors.INVALID_ID); } else if (/^_/.test(id) && !(/^_(design|local)/).test(id)) { err = pouchdbErrors.createError(pouchdbErrors.RESERVED_ID); } if (err) { throw err; } } // Checks if a PouchDB object is "remote" or not. This is function isRemote(db) { if (typeof db._remote === 'boolean') { return db._remote; } /* istanbul ignore next */ if (typeof db.type === 'function') { guardedConsole('warn', 'db.type() is deprecated and will be removed in ' + 'a future version of PouchDB'); return db.type() === 'http'; } /* istanbul ignore next */ return false; } function listenerCount(ee, type) { return 'listenerCount' in ee ? ee.listenerCount(type) : EventEmitter.listenerCount(ee, type); } function parseDesignDocFunctionName(s) { if (!s) { return null; } var parts = s.split('/'); if (parts.length === 2) { return parts; } if (parts.length === 1) { return [s, s]; } return null; } function normalizeDesignDocFunctionName(s) { var normalized = parseDesignDocFunctionName(s); return normalized ? normalized.join('/') : null; } // originally parseUri 1.2.2, now patched by us // (c) Steven Levithan <stevenlevithan.com> // MIT License var keys = ["source", "protocol", "authority", "userInfo", "user", "password", "host", "port", "relative", "path", "directory", "file", "query", "anchor"]; var qName ="queryKey"; var qParser = /(?:^|&)([^&=]*)=?([^&]*)/g; // use the "loose" parser /* eslint no-useless-escape: 0 */ var parser = /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/; function parseUri(str) { var m = parser.exec(str); var uri = {}; var i = 14; while (i--) { var key = keys[i]; var value = m[i] || ""; var encoded = ['user', 'password'].indexOf(key) !== -1; uri[key] = encoded ? decodeURIComponent(value) : value; } uri[qName] = {}; uri[keys[12]].replace(qParser, function ($0, $1, $2) { if ($1) { uri[qName][$1] = $2; } }); return uri; } // Based on https://github.com/alexdavid/scope-eval v0.0.3 // (source: https://unpkg.com/scope-eval@0.0.3/scope_eval.js) // This is basically just a wrapper around new Function() function scopeEval(source, scope) { var keys = []; var values = []; for (var key in scope) { if (Object.prototype.hasOwnProperty.call(scope, key)) { keys.push(key); values.push(scope[key]); } } keys.push(source); return Function.apply(null, keys).apply(null, values); } // this is essentially the "update sugar" function from daleharvey/pouchdb#1388 // the diffFun tells us what delta to apply to the doc. it either returns // the doc, or false if it doesn't need to do an update after all function upsert(db, docId, diffFun) { return db.get(docId) .catch(function (err) { /* istanbul ignore next */ if (err.status !== 404) { throw err; } return {}; }) .then(function (doc) { // the user might change the _rev, so save it for posterity var docRev = doc._rev; var newDoc = diffFun(doc); if (!newDoc) { // if the diffFun returns falsy, we short-circuit as // an optimization return {updated: false, rev: docRev}; } // users aren't allowed to modify these values, // so reset them here newDoc._id = docId; newDoc._rev = docRev; return tryAndPut(db, newDoc, diffFun); }); } function tryAndPut(db, doc, diffFun) { return db.put(doc).then(function (res) { return { updated: true, rev: res.rev }; }, function (err) { /* istanbul ignore next */ if (err.status !== 409) { throw err; } return upsert(db, doc._id, diffFun); }); } /** * Creates a new revision string that does NOT include the revision height * For example '56649f1b0506c6ca9fda0746eb0cacdf' */ function rev(doc, deterministic_revs) { if (!deterministic_revs) { return uuid.v4().replace(/-/g, '').toLowerCase(); } var mutateableDoc = Object.assign({}, doc); delete mutateableDoc._rev_tree; return pouchdbMd5.stringMd5(JSON.stringify(mutateableDoc)); } var uuid$1 = uuid.v4; // mimic old import, only v4 is ever used elsewhere exports.adapterFun = adapterFun; exports.bulkGetShim = bulkGet; exports.changesHandler = Changes; exports.clone = clone; exports.defaultBackOff = defaultBackOff; exports.explainError = res; exports.filterChange = filterChange; exports.functionName = res$2; exports.guardedConsole = guardedConsole; exports.hasLocalStorage = hasLocalStorage; exports.invalidIdError = invalidIdError; exports.isRemote = isRemote; exports.listenerCount = listenerCount; exports.nextTick = nextTick; exports.normalizeDdocFunctionName = normalizeDesignDocFunctionName; exports.once = once; exports.parseDdocFunctionName = parseDesignDocFunctionName; exports.parseUri = parseUri; exports.pick = pick; exports.rev = rev; exports.scopeEval = scopeEval; exports.toPromise = toPromise; exports.upsert = upsert; exports.uuid = uuid$1;