blob: ec8439c0365d7872db5bd1411b99f29160cf497d [file] [log] [blame]
import { createError, IDB_ERROR } from 'pouchdb-errors';
import {
pick
} from 'pouchdb-utils';
import {
safeJsonParse,
safeJsonStringify
} from 'pouchdb-json';
import {
btoa,
readAsBinaryString,
base64StringToBlobOrBuffer as b64StringToBlob,
blob as createBlob
} from 'pouchdb-binary-utils';
import { ATTACH_AND_SEQ_STORE, ATTACH_STORE, BY_SEQ_STORE } from './constants';
function idbError(callback) {
return function (evt) {
var message = 'unknown_error';
if (evt.target && evt.target.error) {
message = evt.target.error.name || evt.target.error.message;
}
callback(createError(IDB_ERROR, message, evt.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.
function encodeMetadata(metadata, winningRev, deleted) {
return {
data: safeJsonStringify(metadata),
winningRev,
deletedOrLocal: deleted ? '1' : '0',
seq: metadata.seq, // highest seq for this doc
id: metadata.id
};
}
function decodeMetadata(storedObject) {
if (!storedObject) {
return null;
}
var metadata = 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.
function decodeDoc(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
function readBlobData(body, type, asBlob, callback) {
if (asBlob) {
if (!body) {
callback(createBlob([''], {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);
}
}
}
function fetchAttachmentsIfNecessary(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(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
function postProcessAttachments(results, asBlob) {
return Promise.all(results.map(function (row) {
if (row.doc && row.doc._attachments) {
var attNames = Object.keys(row.doc._attachments);
return 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 Promise(function (resolve) {
readBlobData(body, type, asBlob, function (data) {
row.doc._attachments[att] = Object.assign(
pick(attObj, ['digest', 'content_type']),
{data}
);
resolve();
});
});
}));
}
}));
}
function compactRevs(revs, docId, txn) {
var possiblyOrphanedDigests = [];
var seqStore = txn.objectStore(BY_SEQ_STORE);
var attStore = txn.objectStore(ATTACH_STORE);
var attAndSeqStore = txn.objectStore(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();
}
};
};
});
}
function openTransactionSafely(idb, stores, mode) {
try {
return {
txn: idb.transaction(stores, mode)
};
} catch (err) {
return {
error: err
};
}
}
export {
fetchAttachmentsIfNecessary,
openTransactionSafely,
compactRevs,
postProcessAttachments,
idbError,
encodeMetadata,
decodeMetadata,
decodeDoc,
readBlobData
};