blob: 26e2bfc5f291261bf5a5aae7a2c9e03f2f8175e2 [file] [log] [blame]
import { createError, WSQ_ERROR } from 'pouchdb-errors';
import { guardedConsole } from 'pouchdb-utils';
import {
BY_SEQ_STORE,
ATTACH_STORE,
ATTACH_AND_SEQ_STORE
} from './constants';
// 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) {
return str
.replace(/\u0002/g, '\u0002\u0002')
.replace(/\u0001/g, '\u0001\u0002')
.replace(/\u0000/g, '\u0001\u0001');
}
function unescapeBlob(str) {
return str
.replace(/\u0001\u0001/g, '\u0000')
.replace(/\u0001\u0002/g, '\u0001')
.replace(/\u0002\u0002/g, '\u0002');
}
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 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) {
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(createError(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
}
export {
escapeBlob,
unescapeBlob,
stringifyDoc,
unstringifyDoc,
qMarks,
select,
compactRevs,
getSize,
websqlError
};