| 'use strict'; |
| |
| var errors = require('../../deps/errors'); |
| var utils = require('../../utils'); |
| var base64 = require('../../deps/binary/base64'); |
| var btoa = base64.btoa; |
| var constants = require('./constants'); |
| var readAsBinaryString = require('../../deps/binary/readAsBinaryString'); |
| var b64StringToBlob = require('../../deps/binary/base64StringToBlobOrBuffer'); |
| var createBlob = require('../../deps/binary/blob'); |
| |
| function tryCode(fun, that, args) { |
| try { |
| fun.apply(that, args); |
| } catch (err) { // shouldn't happen |
| if (typeof PouchDB !== 'undefined') { |
| PouchDB.emit('error', err); |
| } |
| } |
| } |
| |
| exports.taskQueue = { |
| running: false, |
| queue: [] |
| }; |
| |
| exports.applyNext = function () { |
| if (exports.taskQueue.running || !exports.taskQueue.queue.length) { |
| return; |
| } |
| exports.taskQueue.running = true; |
| var item = exports.taskQueue.queue.shift(); |
| item.action(function (err, res) { |
| tryCode(item.callback, this, [err, res]); |
| exports.taskQueue.running = false; |
| process.nextTick(exports.applyNext); |
| }); |
| }; |
| |
| exports.idbError = function (callback) { |
| return function (event) { |
| var message = (event.target && event.target.error && |
| event.target.error.name) || event.target; |
| callback(errors.error(errors.IDB_ERROR, message, event.type)); |
| }; |
| }; |
| |
| // Unfortunately, the metadata has to be stringified |
| // when it is put into the database, because otherwise |
| // IndexedDB can throw errors for deeply-nested objects. |
| // Originally we just used JSON.parse/JSON.stringify; now |
| // we use this custom vuvuzela library that avoids recursion. |
| // If we could do it all over again, we'd probably use a |
| // format for the revision trees other than JSON. |
| exports.encodeMetadata = function (metadata, winningRev, deleted) { |
| return { |
| data: utils.safeJsonStringify(metadata), |
| winningRev: winningRev, |
| deletedOrLocal: deleted ? '1' : '0', |
| seq: metadata.seq, // highest seq for this doc |
| id: metadata.id |
| }; |
| }; |
| |
| exports.decodeMetadata = function (storedObject) { |
| if (!storedObject) { |
| return null; |
| } |
| var metadata = utils.safeJsonParse(storedObject.data); |
| metadata.winningRev = storedObject.winningRev; |
| metadata.deleted = storedObject.deletedOrLocal === '1'; |
| metadata.seq = storedObject.seq; |
| return metadata; |
| }; |
| |
| // read the doc back out from the database. we don't store the |
| // _id or _rev because we already have _doc_id_rev. |
| exports.decodeDoc = function (doc) { |
| if (!doc) { |
| return doc; |
| } |
| var idx = doc._doc_id_rev.lastIndexOf(':'); |
| doc._id = doc._doc_id_rev.substring(0, idx - 1); |
| doc._rev = doc._doc_id_rev.substring(idx + 1); |
| delete doc._doc_id_rev; |
| return doc; |
| }; |
| |
| // Read a blob from the database, encoding as necessary |
| // and translating from base64 if the IDB doesn't support |
| // native Blobs |
| exports.readBlobData = function (body, type, asBlob, callback) { |
| if (asBlob) { |
| if (!body) { |
| callback(createBlob([''], {type: type})); |
| } else if (typeof body !== 'string') { // we have blob support |
| callback(body); |
| } else { // no blob support |
| callback(b64StringToBlob(body, type)); |
| } |
| } else { // as base64 string |
| if (!body) { |
| callback(''); |
| } else if (typeof body !== 'string') { // we have blob support |
| readAsBinaryString(body, function (binary) { |
| callback(btoa(binary)); |
| }); |
| } else { // no blob support |
| callback(body); |
| } |
| } |
| }; |
| |
| exports.fetchAttachmentsIfNecessary = function (doc, opts, 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 digest = attObj.digest; |
| var req = txn.objectStore(constants.ATTACH_STORE).get(digest); |
| req.onsuccess = function (e) { |
| attObj.body = e.target.result.body; |
| checkDone(); |
| }; |
| } |
| |
| attachments.forEach(function (att) { |
| if (opts.attachments && opts.include_docs) { |
| fetchAttachment(doc, att); |
| } else { |
| doc._attachments[att].stub = true; |
| checkDone(); |
| } |
| }); |
| }; |
| |
| // IDB-specific postprocessing necessary because |
| // we don't know whether we stored a true Blob or |
| // a base64-encoded string, and if it's a Blob it |
| // needs to be read outside of the transaction context |
| exports.postProcessAttachments = function (results, asBlob) { |
| return utils.Promise.all(results.map(function (row) { |
| if (row.doc && row.doc._attachments) { |
| var attNames = Object.keys(row.doc._attachments); |
| return utils.Promise.all(attNames.map(function (att) { |
| var attObj = row.doc._attachments[att]; |
| if (!('body' in attObj)) { // already processed |
| return; |
| } |
| var body = attObj.body; |
| var type = attObj.content_type; |
| return new utils.Promise(function (resolve) { |
| exports.readBlobData(body, type, asBlob, function (data) { |
| row.doc._attachments[att] = utils.extend( |
| utils.pick(attObj, ['digest', 'content_type']), |
| {data: data} |
| ); |
| resolve(); |
| }); |
| }); |
| })); |
| } |
| })); |
| }; |
| |
| exports.compactRevs = function (revs, docId, txn) { |
| |
| var possiblyOrphanedDigests = []; |
| var seqStore = txn.objectStore(constants.BY_SEQ_STORE); |
| var attStore = txn.objectStore(constants.ATTACH_STORE); |
| var attAndSeqStore = txn.objectStore(constants.ATTACH_AND_SEQ_STORE); |
| var count = revs.length; |
| |
| function checkDone() { |
| count--; |
| if (!count) { // done processing all revs |
| deleteOrphanedAttachments(); |
| } |
| } |
| |
| function deleteOrphanedAttachments() { |
| if (!possiblyOrphanedDigests.length) { |
| return; |
| } |
| possiblyOrphanedDigests.forEach(function (digest) { |
| var countReq = attAndSeqStore.index('digestSeq').count( |
| IDBKeyRange.bound( |
| digest + '::', digest + '::\uffff', false, false)); |
| countReq.onsuccess = function (e) { |
| var count = e.target.result; |
| if (!count) { |
| // orphaned |
| attStore.delete(digest); |
| } |
| }; |
| }); |
| } |
| |
| revs.forEach(function (rev) { |
| var index = seqStore.index('_doc_id_rev'); |
| var key = docId + "::" + rev; |
| index.getKey(key).onsuccess = function (e) { |
| var seq = e.target.result; |
| if (typeof seq !== 'number') { |
| return checkDone(); |
| } |
| seqStore.delete(seq); |
| |
| var cursor = attAndSeqStore.index('seq') |
| .openCursor(IDBKeyRange.only(seq)); |
| |
| cursor.onsuccess = function (event) { |
| var cursor = event.target.result; |
| if (cursor) { |
| var digest = cursor.value.digestSeq.split('::')[0]; |
| possiblyOrphanedDigests.push(digest); |
| attAndSeqStore.delete(cursor.primaryKey); |
| cursor.continue(); |
| } else { // done |
| checkDone(); |
| } |
| }; |
| }; |
| }); |
| }; |
| |
| exports.openTransactionSafely = function (idb, stores, mode) { |
| try { |
| return { |
| txn: idb.transaction(stores, mode) |
| }; |
| } catch (err) { |
| return { |
| error: err |
| }; |
| } |
| }; |