| |
| 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 |
| }; |