UNPKG

cronapp-framework-mobile-js

Version:
1,556 lines (1,353 loc) 179 kB
/* * https://unpkg.com/pouchdb-adapter-cordova-sqlite@2.0.7/dist/pouchdb.cordova-sqlite.js */ (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.PouchAdapterCordovaSqlite = f()}})(function(){var define,module,exports;return (function(){function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s}return e})()({1:[function(require,module,exports){ var assign if (typeof Object.assign === 'function') { assign = Object.assign } else { // lite Object.assign polyfill based on // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign assign = function (target) { var to = Object(target) for (var index = 1; index < arguments.length; index++) { var nextSource = arguments[index] if (nextSource != null) { // Skip over if undefined or null for (var nextKey in nextSource) { // Avoid bugs when hasOwnProperty is shadowed if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { to[nextKey] = nextSource[nextKey] } } } } return to } } module.exports = assign },{}],2:[function(require,module,exports){ 'use strict'; module.exports = argsArray; function argsArray(fun) { return function () { var len = arguments.length; if (len) { var args = []; var i = -1; while (++i < len) { args[i] = arguments[i]; } return fun.call(this, args); } else { return fun.call(this, []); } }; } },{}],3:[function(require,module,exports){ // Copyright Joyent, Inc. and other Node contributors. // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit // persons to whom the Software is furnished to do so, subject to the // following conditions: // // The above copyright notice and this permission notice shall be included // in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. function EventEmitter() { this._events = this._events || {}; this._maxListeners = this._maxListeners || undefined; } module.exports = EventEmitter; // Backwards-compat with node 0.10.x EventEmitter.EventEmitter = EventEmitter; EventEmitter.prototype._events = undefined; EventEmitter.prototype._maxListeners = undefined; // By default EventEmitters will print a warning if more than 10 listeners are // added to it. This is a useful default which helps finding memory leaks. EventEmitter.defaultMaxListeners = 10; // Obviously not all Emitters should be limited to 10. This function allows // that to be increased. Set to zero for unlimited. EventEmitter.prototype.setMaxListeners = function(n) { if (!isNumber(n) || n < 0 || isNaN(n)) throw TypeError('n must be a positive number'); this._maxListeners = n; return this; }; EventEmitter.prototype.emit = function(type) { var er, handler, len, args, i, listeners; if (!this._events) this._events = {}; // If there is no 'error' event listener then throw. if (type === 'error') { if (!this._events.error || (isObject(this._events.error) && !this._events.error.length)) { er = arguments[1]; if (er instanceof Error) { throw er; // Unhandled 'error' event } else { // At least give some kind of context to the user var err = new Error('Uncaught, unspecified "error" event. (' + er + ')'); err.context = er; throw err; } } } handler = this._events[type]; if (isUndefined(handler)) return false; if (isFunction(handler)) { switch (arguments.length) { // fast cases case 1: handler.call(this); break; case 2: handler.call(this, arguments[1]); break; case 3: handler.call(this, arguments[1], arguments[2]); break; // slower default: args = Array.prototype.slice.call(arguments, 1); handler.apply(this, args); } } else if (isObject(handler)) { args = Array.prototype.slice.call(arguments, 1); listeners = handler.slice(); len = listeners.length; for (i = 0; i < len; i++) listeners[i].apply(this, args); } return true; }; EventEmitter.prototype.addListener = function(type, listener) { var m; if (!isFunction(listener)) throw TypeError('listener must be a function'); if (!this._events) this._events = {}; // To avoid recursion in the case that type === "newListener"! Before // adding it to the listeners, first emit "newListener". if (this._events.newListener) this.emit('newListener', type, isFunction(listener.listener) ? listener.listener : listener); if (!this._events[type]) // Optimize the case of one listener. Don't need the extra array object. this._events[type] = listener; else if (isObject(this._events[type])) // If we've already got an array, just append. this._events[type].push(listener); else // Adding the second element, need to change to array. this._events[type] = [this._events[type], listener]; // Check for listener leak if (isObject(this._events[type]) && !this._events[type].warned) { if (!isUndefined(this._maxListeners)) { m = this._maxListeners; } else { m = EventEmitter.defaultMaxListeners; } if (m && m > 0 && this._events[type].length > m) { this._events[type].warned = true; console.error('(node) warning: possible EventEmitter memory ' + 'leak detected. %d listeners added. ' + 'Use emitter.setMaxListeners() to increase limit.', this._events[type].length); if (typeof console.trace === 'function') { // not supported in IE 10 console.trace(); } } } return this; }; EventEmitter.prototype.on = EventEmitter.prototype.addListener; EventEmitter.prototype.once = function(type, listener) { if (!isFunction(listener)) throw TypeError('listener must be a function'); var fired = false; function g() { this.removeListener(type, g); if (!fired) { fired = true; listener.apply(this, arguments); } } g.listener = listener; this.on(type, g); return this; }; // emits a 'removeListener' event iff the listener was removed EventEmitter.prototype.removeListener = function(type, listener) { var list, position, length, i; if (!isFunction(listener)) throw TypeError('listener must be a function'); if (!this._events || !this._events[type]) return this; list = this._events[type]; length = list.length; position = -1; if (list === listener || (isFunction(list.listener) && list.listener === listener)) { delete this._events[type]; if (this._events.removeListener) this.emit('removeListener', type, listener); } else if (isObject(list)) { for (i = length; i-- > 0;) { if (list[i] === listener || (list[i].listener && list[i].listener === listener)) { position = i; break; } } if (position < 0) return this; if (list.length === 1) { list.length = 0; delete this._events[type]; } else { list.splice(position, 1); } if (this._events.removeListener) this.emit('removeListener', type, listener); } return this; }; EventEmitter.prototype.removeAllListeners = function(type) { var key, listeners; if (!this._events) return this; // not listening for removeListener, no need to emit if (!this._events.removeListener) { if (arguments.length === 0) this._events = {}; else if (this._events[type]) delete this._events[type]; return this; } // emit removeListener for all listeners on all events if (arguments.length === 0) { for (key in this._events) { if (key === 'removeListener') continue; this.removeAllListeners(key); } this.removeAllListeners('removeListener'); this._events = {}; return this; } listeners = this._events[type]; if (isFunction(listeners)) { this.removeListener(type, listeners); } else if (listeners) { // LIFO order while (listeners.length) this.removeListener(type, listeners[listeners.length - 1]); } delete this._events[type]; return this; }; EventEmitter.prototype.listeners = function(type) { var ret; if (!this._events || !this._events[type]) ret = []; else if (isFunction(this._events[type])) ret = [this._events[type]]; else ret = this._events[type].slice(); return ret; }; EventEmitter.prototype.listenerCount = function(type) { if (this._events) { var evlistener = this._events[type]; if (isFunction(evlistener)) return 1; else if (evlistener) return evlistener.length; } return 0; }; EventEmitter.listenerCount = function(emitter, type) { return emitter.listenerCount(type); }; function isFunction(arg) { return typeof arg === 'function'; } function isNumber(arg) { return typeof arg === 'number'; } function isObject(arg) { return typeof arg === 'object' && arg !== null; } function isUndefined(arg) { return arg === void 0; } },{}],4:[function(require,module,exports){ (function (global){ 'use strict'; var Mutation = global.MutationObserver || global.WebKitMutationObserver; var scheduleDrain; { if (Mutation) { var called = 0; var observer = new Mutation(nextTick); var element = global.document.createTextNode(''); observer.observe(element, { characterData: true }); scheduleDrain = function () { element.data = (called = ++called % 2); }; } else if (!global.setImmediate && typeof global.MessageChannel !== 'undefined') { var channel = new global.MessageChannel(); channel.port1.onmessage = nextTick; scheduleDrain = function () { channel.port2.postMessage(0); }; } else if ('document' in global && 'onreadystatechange' in global.document.createElement('script')) { scheduleDrain = function () { // Create a <script> element; its readystatechange event will be fired asynchronously once it is inserted // into the document. Do so, thus queuing up the task. Remember to clean up once it's been called. var scriptEl = global.document.createElement('script'); scriptEl.onreadystatechange = function () { nextTick(); scriptEl.onreadystatechange = null; scriptEl.parentNode.removeChild(scriptEl); scriptEl = null; }; global.document.documentElement.appendChild(scriptEl); }; } else { scheduleDrain = function () { setTimeout(nextTick, 0); }; } } var draining; var queue = []; //named nextTick for less confusing stack traces function nextTick() { draining = true; var i, oldQueue; var len = queue.length; while (len) { oldQueue = queue; queue = []; i = -1; while (++i < len) { oldQueue[i](); } len = queue.length; } draining = false; } module.exports = immediate; function immediate(task) { if (queue.push(task) === 1 && !draining) { scheduleDrain(); } } }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) },{}],5:[function(require,module,exports){ if (typeof Object.create === 'function') { // implementation from standard node.js 'util' module module.exports = function inherits(ctor, superCtor) { ctor.super_ = superCtor ctor.prototype = Object.create(superCtor.prototype, { constructor: { value: ctor, enumerable: false, writable: true, configurable: true } }); }; } else { // old school shim for old browsers module.exports = function inherits(ctor, superCtor) { ctor.super_ = superCtor var TempCtor = function () {} TempCtor.prototype = superCtor.prototype ctor.prototype = new TempCtor() ctor.prototype.constructor = ctor } } },{}],6:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var pouchdbUtils = require(14); var pouchdbErrors = require(10); var pouchdbBinaryUtils = require(8); var pouchdbMd5 = require(12); var pouchdbMerge = require(13); var pouchdbCollections = require(9); function allDocsKeysQuery(api, opts) { var keys = opts.keys; var finalResults = { offset: opts.skip }; return Promise.all(keys.map(function (key) { var subOpts = pouchdbUtils.assign({key: key, deleted: 'ok'}, opts); ['limit', 'skip', 'keys'].forEach(function (optKey) { delete subOpts[optKey]; }); return new Promise(function (resolve, reject) { api._allDocs(subOpts, function (err, res) { /* istanbul ignore if */ if (err) { return reject(err); } /* istanbul ignore if */ if (opts.update_seq && res.update_seq !== undefined) { finalResults.update_seq = res.update_seq; } finalResults.total_rows = res.total_rows; resolve(res.rows[0] || {key: key, error: 'not_found'}); }); }); })).then(function (results) { finalResults.rows = results; return finalResults; }); } 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 pouchdbErrors.createError(pouchdbErrors.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, dbOpts) { if (!dbOpts) { dbOpts = { deterministic_revs: true }; } var nRevNum; var newRevId; var revInfo; var opts = {status: 'available'}; if (doc._deleted) { opts.deleted = true; } if (newEdits) { if (!doc._id) { doc._id = pouchdbUtils.uuid(); } newRevId = pouchdbUtils.rev(doc, dbOpts.deterministic_revs); 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, []] }]; } } pouchdbUtils.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 = pouchdbErrors.createError(pouchdbErrors.DOC_VALIDATION, key); error.message = pouchdbErrors.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; } function parseBase64(data) { try { return pouchdbBinaryUtils.atob(data); } catch (e) { var err = pouchdbErrors.createError(pouchdbErrors.BAD_ARG, 'Attachment is not a valid base64 string'); return {error: err}; } } function preprocessString(att, blobType, callback) { var asBinary = parseBase64(att.data); if (asBinary.error) { return callback(asBinary.error); } att.length = asBinary.length; if (blobType === 'blob') { att.data = pouchdbBinaryUtils.binaryStringToBlobOrBuffer(asBinary, att.content_type); } else if (blobType === 'base64') { att.data = pouchdbBinaryUtils.btoa(asBinary); } else { // binary att.data = asBinary; } pouchdbMd5.binaryMd5(asBinary, function (result) { att.digest = 'md5-' + result; callback(); }); } function preprocessBlob(att, blobType, callback) { pouchdbMd5.binaryMd5(att.data, function (md5) { att.digest = 'md5-' + md5; // size is for blobs (browser), length is for buffers (node) att.length = att.data.size || att.data.length || 0; if (blobType === 'binary') { pouchdbBinaryUtils.blobOrBufferToBinaryString(att.data, function (binString) { att.data = binString; callback(); }); } else if (blobType === 'base64') { pouchdbBinaryUtils.blobOrBufferToBase64(att.data, function (b64) { att.data = b64; callback(); }); } else { callback(); } }); } function preprocessAttachment(att, blobType, callback) { if (att.stub) { return callback(); } if (typeof att.data === 'string') { // input is a base64 string preprocessString(att, blobType, callback); } else { // input is a blob preprocessBlob(att, blobType, callback); } } function preprocessAttachments(docInfos, blobType, callback) { if (!docInfos.length) { return callback(); } var docv = 0; 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], blobType, processedAttachment); } } }); function done() { docv++; if (docInfos.length === docv) { if (overallErr) { callback(overallErr); } else { callback(); } } } } function updateDoc(revLimit, prev, docInfo, results, i, cb, writeDoc, newEdits) { if (pouchdbMerge.revExists(prev.rev_tree, docInfo.metadata.rev) && !newEdits) { results[i] = docInfo; return cb(); } // sometimes this is pre-calculated. historically not always var previousWinningRev = prev.winningRev || pouchdbMerge.winningRev(prev); var previouslyDeleted = 'deleted' in prev ? prev.deleted : pouchdbMerge.isDeleted(prev, previousWinningRev); var deleted = 'deleted' in docInfo.metadata ? docInfo.metadata.deleted : pouchdbMerge.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 = pouchdbMerge.merge(prev.rev_tree, docInfo.metadata.rev_tree[0], revLimit); var inConflict = newEdits && (( (previouslyDeleted && deleted && merged.conflicts !== 'new_leaf') || (!previouslyDeleted && merged.conflicts !== 'new_leaf') || (previouslyDeleted && !deleted && merged.conflicts === 'new_branch'))); if (inConflict) { var err = pouchdbErrors.createError(pouchdbErrors.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 = pouchdbMerge.winningRev(docInfo.metadata); var winningRevIsDeleted = pouchdbMerge.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 = pouchdbMerge.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 = pouchdbMerge.winningRev(docInfo.metadata); var deleted = pouchdbMerge.isDeleted(docInfo.metadata, winningRev); if ('was_delete' in opts && deleted) { results[resultsIdx] = pouchdbErrors.createError(pouchdbErrors.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 = pouchdbErrors.createError(pouchdbErrors.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 && pouchdbMerge.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 = pouchdbMerge.merge([], currentDoc.metadata.rev_tree[0], revLimit); currentDoc.metadata.rev_tree = merged.tree; currentDoc.stemmedRevs = merged.stemmedRevs || []; insertDoc(currentDoc, resultsIdx, docWritten); } } nextDoc(); }); } exports.invalidIdError = pouchdbUtils.invalidIdError; exports.normalizeDdocFunctionName = pouchdbUtils.normalizeDdocFunctionName; exports.parseDdocFunctionName = pouchdbUtils.parseDdocFunctionName; exports.isDeleted = pouchdbMerge.isDeleted; exports.isLocalId = pouchdbMerge.isLocalId; exports.allDocsKeysQuery = allDocsKeysQuery; exports.parseDoc = parseDoc; exports.preprocessAttachments = preprocessAttachments; exports.processDocs = processDocs; exports.updateDoc = updateDoc; },{"10":10,"12":12,"13":13,"14":14,"8":8,"9":9}],7:[function(require,module,exports){ 'use strict'; var pouchdbCollections = require(9); var pouchdbErrors = require(10); var pouchdbUtils = require(14); var pouchdbAdapterUtils = require(6); var pouchdbMerge = require(13); var pouchdbJson = require(11); var pouchdbBinaryUtils = require(8); // // Parsing hex strings. Yeah. // // So basically we need this because of a bug in WebSQL: // https://code.google.com/p/chromium/issues/detail?id=422690 // https://bugs.webkit.org/show_bug.cgi?id=137637 // // UTF-8 and UTF-16 are provided as separate functions // for meager performance improvements // function decodeUtf8(str) { return decodeURIComponent(escape(str)); } function hexToInt(charCode) { // '0'-'9' is 48-57 // 'A'-'F' is 65-70 // SQLite will only give us uppercase hex return charCode < 65 ? (charCode - 48) : (charCode - 55); } // Example: // pragma encoding=utf8; // select hex('A'); // returns '41' function parseHexUtf8(str, start, end) { var result = ''; while (start < end) { result += String.fromCharCode( (hexToInt(str.charCodeAt(start++)) << 4) | hexToInt(str.charCodeAt(start++))); } return result; } // Example: // pragma encoding=utf16; // select hex('A'); // returns '4100' // notice that the 00 comes after the 41 (i.e. it's swizzled) function parseHexUtf16(str, start, end) { var result = ''; while (start < end) { // UTF-16, so swizzle the bytes result += String.fromCharCode( (hexToInt(str.charCodeAt(start + 2)) << 12) | (hexToInt(str.charCodeAt(start + 3)) << 8) | (hexToInt(str.charCodeAt(start)) << 4) | hexToInt(str.charCodeAt(start + 1))); start += 4; } return result; } function parseHexString(str, encoding) { if (encoding === 'UTF-8') { return decodeUtf8(parseHexUtf8(str, 0, str.length)); } else { return parseHexUtf16(str, 0, str.length); } } function quote(str) { return "'" + str + "'"; } var ADAPTER_VERSION = 7; // used to manage migrations // The object stores created for each database // DOC_STORE stores the document meta data, its revision history and state var DOC_STORE = quote('document-store'); // BY_SEQ_STORE stores a particular version of a document, keyed by its // sequence id var BY_SEQ_STORE = quote('by-sequence'); // Where we store attachments var ATTACH_STORE = quote('attach-store'); var LOCAL_STORE = quote('local-store'); var META_STORE = quote('metadata-store'); // where we store many-to-many relations between attachment // digests and seqs var ATTACH_AND_SEQ_STORE = quote('attach-seq-store'); // escapeBlob and unescapeBlob are workarounds for a websql bug: // https://code.google.com/p/chromium/issues/detail?id=422690 // https://bugs.webkit.org/show_bug.cgi?id=137637 // The goal is to never actually insert the \u0000 character // in the database. function escapeBlob(str) { /* eslint-disable no-control-regex */ return str .replace(/\u0002/g, '\u0002\u0002') .replace(/\u0001/g, '\u0001\u0002') .replace(/\u0000/g, '\u0001\u0001'); /* eslint-enable no-control-regex */ } function unescapeBlob(str) { /* eslint-disable no-control-regex */ return str .replace(/\u0001\u0001/g, '\u0000') .replace(/\u0001\u0002/g, '\u0001') .replace(/\u0002\u0002/g, '\u0002'); /* eslint-enable no-control-regex */ } function stringifyDoc(doc) { // don't bother storing the id/rev. it uses lots of space, // in persistent map/reduce especially delete doc._id; delete doc._rev; return JSON.stringify(doc); } function unstringifyDoc(doc, id, rev) { doc = JSON.parse(doc); doc._id = id; doc._rev = rev; return doc; } // question mark groups IN queries, e.g. 3 -> '(?,?,?)' function qMarks(num) { var s = '('; while (num--) { s += '?'; if (num) { s += ','; } } return s + ')'; } function select(selector, table, joiner, where, orderBy) { return 'SELECT ' + selector + ' FROM ' + (typeof table === 'string' ? table : table.join(' JOIN ')) + (joiner ? (' ON ' + joiner) : '') + (where ? (' WHERE ' + (typeof where === 'string' ? where : where.join(' AND '))) : '') + (orderBy ? (' ORDER BY ' + orderBy) : ''); } function compactRevs(revs, docId, tx) { if (!revs.length) { return; } var numDone = 0; var seqs = []; function checkDone() { if (++numDone === revs.length) { // done deleteOrphans(); } } function deleteOrphans() { // find orphaned attachment digests if (!seqs.length) { return; } var sql = 'SELECT DISTINCT digest AS digest FROM ' + ATTACH_AND_SEQ_STORE + ' WHERE seq IN ' + qMarks(seqs.length); tx.executeSql(sql, seqs, function (tx, res) { var digestsToCheck = []; for (var i = 0; i < res.rows.length; i++) { digestsToCheck.push(res.rows.item(i).digest); } if (!digestsToCheck.length) { return; } var sql = 'DELETE FROM ' + ATTACH_AND_SEQ_STORE + ' WHERE seq IN (' + seqs.map(function () { return '?'; }).join(',') + ')'; tx.executeSql(sql, seqs, function (tx) { var sql = 'SELECT digest FROM ' + ATTACH_AND_SEQ_STORE + ' WHERE digest IN (' + digestsToCheck.map(function () { return '?'; }).join(',') + ')'; tx.executeSql(sql, digestsToCheck, function (tx, res) { var nonOrphanedDigests = new pouchdbCollections.Set(); for (var i = 0; i < res.rows.length; i++) { nonOrphanedDigests.add(res.rows.item(i).digest); } digestsToCheck.forEach(function (digest) { if (nonOrphanedDigests.has(digest)) { return; } tx.executeSql( 'DELETE FROM ' + ATTACH_AND_SEQ_STORE + ' WHERE digest=?', [digest]); tx.executeSql( 'DELETE FROM ' + ATTACH_STORE + ' WHERE digest=?', [digest]); }); }); }); }); } // update by-seq and attach stores in parallel revs.forEach(function (rev) { var sql = 'SELECT seq FROM ' + BY_SEQ_STORE + ' WHERE doc_id=? AND rev=?'; tx.executeSql(sql, [docId, rev], function (tx, res) { if (!res.rows.length) { // already deleted return checkDone(); } var seq = res.rows.item(0).seq; seqs.push(seq); tx.executeSql( 'DELETE FROM ' + BY_SEQ_STORE + ' WHERE seq=?', [seq], checkDone); }); }); } function websqlError(callback) { return function (event) { pouchdbUtils.guardedConsole('error', 'WebSQL threw an error', event); // event may actually be a SQLError object, so report is as such var errorNameMatch = event && event.constructor.toString() .match(/function ([^(]+)/); var errorName = (errorNameMatch && errorNameMatch[1]) || event.type; var errorReason = event.target || event.message; callback(pouchdbErrors.createError(pouchdbErrors.WSQ_ERROR, errorReason, errorName)); }; } function getSize(opts) { if ('size' in opts) { // triggers immediate popup in iOS, fixes #2347 // e.g. 5000001 asks for 5 MB, 10000001 asks for 10 MB, return opts.size * 1000000; } // In iOS, doesn't matter as long as it's <= 5000000. // Except that if you request too much, our tests fail // because of the native "do you accept?" popup. // In Android <=4.3, this value is actually used as an // honest-to-god ceiling for data, so we need to // set it to a decently high number. var isAndroid = typeof navigator !== 'undefined' && /Android/.test(navigator.userAgent); return isAndroid ? 5000000 : 1; // in PhantomJS, if you use 0 it will crash } function websqlBulkDocs(dbOpts, req, opts, api, db, websqlChanges, callback) { var newEdits = opts.new_edits; var userDocs = req.docs; // Parse the docs, give them a sequence number for the result var docInfos = userDocs.map(function (doc) { if (doc._id && pouchdbAdapterUtils.isLocalId(doc._id)) { return doc; } var newDoc = pouchdbAdapterUtils.parseDoc(doc, newEdits, dbOpts); return newDoc; }); var docInfoErrors = docInfos.filter(function (docInfo) { return docInfo.error; }); if (docInfoErrors.length) { return callback(docInfoErrors[0]); } var tx; var results = new Array(docInfos.length); var fetchedDocs = new pouchdbCollections.Map(); var preconditionErrored; function complete() { if (preconditionErrored) { return callback(preconditionErrored); } websqlChanges.notify(api._name); callback(null, results); } function verifyAttachment(digest, callback) { var sql = 'SELECT count(*) as cnt FROM ' + ATTACH_STORE + ' WHERE digest=?'; tx.executeSql(sql, [digest], function (tx, result) { if (result.rows.item(0).cnt === 0) { var err = pouchdbErrors.createError(pouchdbErrors.MISSING_STUB, 'unknown stub attachment with digest ' + digest); callback(err); } else { callback(); } }); } function verifyAttachments(finish) { var digests = []; docInfos.forEach(function (docInfo) { if (docInfo.data && docInfo.data._attachments) { Object.keys(docInfo.data._attachments).forEach(function (filename) { var att = docInfo.data._attachments[filename]; if (att.stub) { digests.push(att.digest); } }); } }); if (!digests.length) { return finish(); } var numDone = 0; var err; function checkDone() { if (++numDone === digests.length) { finish(err); } } digests.forEach(function (digest) { verifyAttachment(digest, function (attErr) { if (attErr && !err) { err = attErr; } checkDone(); }); }); } function writeDoc(docInfo, winningRev, winningRevIsDeleted, newRevIsDeleted, isUpdate, delta, resultsIdx, callback) { function finish() { var data = docInfo.data; var deletedInt = newRevIsDeleted ? 1 : 0; var id = data._id; var rev = data._rev; var json = stringifyDoc(data); var sql = 'INSERT INTO ' + BY_SEQ_STORE + ' (doc_id, rev, json, deleted) VALUES (?, ?, ?, ?);'; var sqlArgs = [id, rev, json, deletedInt]; // map seqs to attachment digests, which // we will need later during compaction function insertAttachmentMappings(seq, callback) { var attsAdded = 0; var attsToAdd = Object.keys(data._attachments || {}); if (!attsToAdd.length) { return callback(); } function checkDone() { if (++attsAdded === attsToAdd.length) { callback(); } return false; // ack handling a constraint error } function add(att) { var sql = 'INSERT INTO ' + ATTACH_AND_SEQ_STORE + ' (digest, seq) VALUES (?,?)'; var sqlArgs = [data._attachments[att].digest, seq]; tx.executeSql(sql, sqlArgs, checkDone, checkDone); // second callback is for a constaint error, which we ignore // because this docid/rev has already been associated with // the digest (e.g. when new_edits == false) } for (var i = 0; i < attsToAdd.length; i++) { add(attsToAdd[i]); // do in parallel } } tx.executeSql(sql, sqlArgs, function (tx, result) { var seq = result.insertId; insertAttachmentMappings(seq, function () { dataWritten(tx, seq); }); }, function () { // constraint error, recover by updating instead (see #1638) var fetchSql = select('seq', BY_SEQ_STORE, null, 'doc_id=? AND rev=?'); tx.executeSql(fetchSql, [id, rev], function (tx, res) { var seq = res.rows.item(0).seq; var sql = 'UPDATE ' + BY_SEQ_STORE + ' SET json=?, deleted=? WHERE doc_id=? AND rev=?;'; var sqlArgs = [json, deletedInt, id, rev]; tx.executeSql(sql, sqlArgs, function (tx) { insertAttachmentMappings(seq, function () { dataWritten(tx, seq); }); }); }); return false; // ack that we've handled the error }); } function collectResults(attachmentErr) { if (!err) { if (attachmentErr) { err = attachmentErr; callback(err); } else if (recv === attachments.length) { finish(); } } } var err = null; var recv = 0; docInfo.data._id = docInfo.metadata.id; docInfo.data._rev = docInfo.metadata.rev; var attachments = Object.keys(docInfo.data._attachments || {}); if (newRevIsDeleted) { docInfo.data._deleted = true; } function attachmentSaved(err) { recv++; collectResults(err); } attachments.forEach(function (key) { var att = docInfo.data._attachments[key]; if (!att.stub) { var data = att.data; delete att.data; att.revpos = parseInt(winningRev, 10); var digest = att.digest; saveAttachment(digest, data, attachmentSaved); } else { recv++; collectResults(); } }); if (!attachments.length) { finish(); } function dataWritten(tx, seq) { var id = docInfo.metadata.id; var revsToCompact = docInfo.stemmedRevs || []; if (isUpdate && api.auto_compaction) { revsToCompact = pouchdbMerge.compactTree(docInfo.metadata).concat(revsToCompact); } if (revsToCompact.length) { compactRevs(revsToCompact, id, tx); } docInfo.metadata.seq = seq; var rev = docInfo.metadata.rev; delete docInfo.metadata.rev; var sql = isUpdate ? 'UPDATE ' + DOC_STORE + ' SET json=?, max_seq=?, winningseq=' + '(SELECT seq FROM ' + BY_SEQ_STORE + ' WHERE doc_id=' + DOC_STORE + '.id AND rev=?) WHERE id=?' : 'INSERT INTO ' + DOC_STORE + ' (id, winningseq, max_seq, json) VALUES (?,?,?,?);'; var metadataStr = pouchdbJson.safeJsonStringify(docInfo.metadata); var params = isUpdate ? [metadataStr, seq, winningRev, id] : [id, seq, seq, metadataStr]; tx.executeSql(sql, params, function () { results[resultsIdx] = { ok: true, id: docInfo.metadata.id, rev: rev }; fetchedDocs.set(id, docInfo.metadata); callback(); }); } } function websqlProcessDocs() { pouchdbAdapterUtils.processDocs(dbOpts.revs_limit, docInfos, api, fetchedDocs, tx, results, writeDoc, opts); } function fetchExistingDocs(callback) { if (!docInfos.length) { return callback(); } var numFetched = 0; function checkDone() { if (++numFetched === docInfos.length) { callback(); } } docInfos.forEach(function (docInfo) { if (docInfo._id && pouchdbAdapterUtils.isLocalId(docInfo._id)) { return checkDone(); // skip local docs } var id = docInfo.metadata.id; tx.executeSql('SELECT json FROM ' + DOC_STORE + ' WHERE id = ?', [id], function (tx, result) { if (result.rows.length) { var metadata = pouchdbJson.safeJsonParse(result.rows.item(0).json); fetchedDocs.set(id, metadata); } checkDone(); }); }); } function saveAttachment(digest, data, callback) { var sql = 'SELECT digest FROM ' + ATTACH_STORE + ' WHERE digest=?'; tx.executeSql(sql, [digest], function (tx, result) { if (result.rows.length) { // attachment already exists return callback(); } // we could just insert before selecting and catch the error, // but my hunch is that it's cheaper not to serialize the blob // from JS to C if we don't have to (TODO: confirm this) sql = 'INSERT INTO ' + ATTACH_STORE + ' (digest, body, escaped) VALUES (?,?,1)'; tx.executeSql(sql, [digest, escapeBlob(data)], function () { callback(); }, function () { // ignore constaint errors, means it already exists callback(); return false; // ack we handled the error }); }); } pouchdbAdapterUtils.preprocessAttachments(docInfos, 'binary', function (err) { if (err) { return callback(err); } db.transaction(function (txn) { tx = txn; verifyAttachments(function (err) { if (err) { preconditionErrored = err; } else { fetchExistingDocs(websqlProcessDocs); } }); }, websqlError(callback), complete); }); } var cachedDatabases = new pouchdbCollections.Map(); // openDatabase passed in through opts (e.g. for node-websql) function openDatabaseWithOpts(opts) { return opts.websql(opts.name, opts.version, opts.description, opts.size); } function openDBSafely(opts) { try { return { db: openDatabaseWithOpts(opts) }; } catch (err) { return { error: err }; } } function openDB(opts) { var cachedResult = cachedDatabases.get(opts.name); if (!cachedResult) { cachedResult = openDBSafely(opts); cachedDatabases.set(opts.name, cachedResult); } return cachedResult; } var websqlChanges = new pouchdbUtils.changesHandler(); function fetchAttachmentsIfNecessary(doc, opts, api, txn, cb) { var attachments = Object.keys(doc._attachments || {}); if (!attachments.length) { return cb && cb(); } var numDone = 0; function checkDone() { if (++numDone === attachments.length && cb) { cb(); } } function fetchAttachment(doc, att) { var attObj = doc._attachments[att]; var attOpts = {binary: opts.binary, ctx: txn}; api._getAttachment(doc._id, att, attObj, attOpts, function (_, data) { doc._attachments[att] = pouchdbUtils.assign( pouchdbUtils.pick(attObj, ['digest', 'content_type']), { data: data } ); checkDone(); }); } attachments.forEach(function (att) { if (opts.attachments && opts.include_docs) {