blob: dd5e34810f7c55565fa510551439a59f59eff38a [file] [log] [blame]
'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
};
}
};