227 lines
5.7 KiB
JavaScript
227 lines
5.7 KiB
JavaScript
/* global window, IDBKeyRange */
|
|
|
|
const { includes, isFunction, isString, last, map } = require('lodash');
|
|
const {
|
|
saveMessages,
|
|
_removeMessages,
|
|
saveUnprocesseds,
|
|
removeUnprocessed,
|
|
} = require('./data');
|
|
const {
|
|
getMessageExportLastIndex,
|
|
setMessageExportLastIndex,
|
|
getMessageExportCount,
|
|
setMessageExportCount,
|
|
getUnprocessedExportLastIndex,
|
|
setUnprocessedExportLastIndex,
|
|
} = require('./settings');
|
|
|
|
module.exports = {
|
|
migrateToSQL,
|
|
};
|
|
|
|
async function migrateToSQL({
|
|
db,
|
|
clearStores,
|
|
handleDOMException,
|
|
countCallback,
|
|
arrayBufferToString,
|
|
}) {
|
|
if (!db) {
|
|
throw new Error('Need db for IndexedDB connection!');
|
|
}
|
|
if (!isFunction(clearStores)) {
|
|
throw new Error('Need clearStores function!');
|
|
}
|
|
if (!isFunction(arrayBufferToString)) {
|
|
throw new Error('Need arrayBufferToString function!');
|
|
}
|
|
if (!isFunction(handleDOMException)) {
|
|
throw new Error('Need handleDOMException function!');
|
|
}
|
|
|
|
window.log.info('migrateToSQL: start');
|
|
|
|
let [lastIndex, doneSoFar] = await Promise.all([
|
|
getMessageExportLastIndex(db),
|
|
getMessageExportCount(db),
|
|
]);
|
|
let complete = false;
|
|
|
|
while (!complete) {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
const status = await migrateStoreToSQLite({
|
|
db,
|
|
save: saveMessages,
|
|
remove: _removeMessages,
|
|
storeName: 'messages',
|
|
handleDOMException,
|
|
lastIndex,
|
|
});
|
|
|
|
({ complete, lastIndex } = status);
|
|
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await Promise.all([
|
|
setMessageExportCount(db, doneSoFar),
|
|
setMessageExportLastIndex(db, lastIndex),
|
|
]);
|
|
|
|
const { count } = status;
|
|
doneSoFar += count;
|
|
if (countCallback) {
|
|
countCallback(doneSoFar);
|
|
}
|
|
}
|
|
window.log.info('migrateToSQL: migrate of messages complete');
|
|
|
|
lastIndex = await getUnprocessedExportLastIndex(db);
|
|
complete = false;
|
|
|
|
while (!complete) {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
const status = await migrateStoreToSQLite({
|
|
db,
|
|
save: async array => {
|
|
await Promise.all(
|
|
map(array, async item => {
|
|
// In the new database, we can't store ArrayBuffers, so we turn these two
|
|
// fields into strings like MessageReceiver now does before save.
|
|
|
|
// Need to set it to version two, since we're using Base64 strings now
|
|
// eslint-disable-next-line no-param-reassign
|
|
item.version = 2;
|
|
|
|
if (item.envelope) {
|
|
// eslint-disable-next-line no-param-reassign
|
|
item.envelope = await arrayBufferToString(item.envelope);
|
|
}
|
|
if (item.decrypted) {
|
|
// eslint-disable-next-line no-param-reassign
|
|
item.decrypted = await arrayBufferToString(item.decrypted);
|
|
}
|
|
})
|
|
);
|
|
await saveUnprocesseds(array);
|
|
},
|
|
remove: removeUnprocessed,
|
|
storeName: 'unprocessed',
|
|
handleDOMException,
|
|
lastIndex,
|
|
});
|
|
|
|
({ complete, lastIndex } = status);
|
|
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await setUnprocessedExportLastIndex(db, lastIndex);
|
|
}
|
|
window.log.info('migrateToSQL: migrate of unprocessed complete');
|
|
|
|
await clearStores(['messages', 'unprocessed']);
|
|
|
|
window.log.info('migrateToSQL: complete');
|
|
}
|
|
|
|
async function migrateStoreToSQLite({
|
|
db,
|
|
save,
|
|
remove,
|
|
storeName,
|
|
handleDOMException,
|
|
lastIndex = null,
|
|
batchSize = 50,
|
|
}) {
|
|
if (!db) {
|
|
throw new Error('Need db for IndexedDB connection!');
|
|
}
|
|
if (!isFunction(save)) {
|
|
throw new Error('Need save function!');
|
|
}
|
|
if (!isFunction(remove)) {
|
|
throw new Error('Need remove function!');
|
|
}
|
|
if (!isString(storeName)) {
|
|
throw new Error('Need storeName!');
|
|
}
|
|
if (!isFunction(handleDOMException)) {
|
|
throw new Error('Need handleDOMException for error handling!');
|
|
}
|
|
|
|
if (!includes(db.objectStoreNames, storeName)) {
|
|
return {
|
|
complete: true,
|
|
count: 0,
|
|
};
|
|
}
|
|
|
|
const queryPromise = new Promise((resolve, reject) => {
|
|
const items = [];
|
|
const transaction = db.transaction(storeName, 'readonly');
|
|
transaction.onerror = () => {
|
|
handleDOMException(
|
|
'migrateToSQLite transaction error',
|
|
transaction.error,
|
|
reject
|
|
);
|
|
};
|
|
transaction.oncomplete = () => {};
|
|
|
|
const store = transaction.objectStore(storeName);
|
|
const excludeLowerBound = true;
|
|
const range = lastIndex
|
|
? IDBKeyRange.lowerBound(lastIndex, excludeLowerBound)
|
|
: undefined;
|
|
const request = store.openCursor(range);
|
|
request.onerror = () => {
|
|
handleDOMException(
|
|
'migrateToSQLite: request error',
|
|
request.error,
|
|
reject
|
|
);
|
|
};
|
|
request.onsuccess = event => {
|
|
const cursor = event.target.result;
|
|
|
|
if (!cursor || !cursor.value) {
|
|
return resolve({
|
|
complete: true,
|
|
items,
|
|
});
|
|
}
|
|
|
|
const item = cursor.value;
|
|
items.push(item);
|
|
|
|
if (items.length >= batchSize) {
|
|
return resolve({
|
|
complete: false,
|
|
items,
|
|
});
|
|
}
|
|
|
|
return cursor.continue();
|
|
};
|
|
});
|
|
|
|
const { items, complete } = await queryPromise;
|
|
|
|
if (items.length) {
|
|
// Because of the force save and some failed imports, we're going to delete before
|
|
// we attempt to insert.
|
|
const ids = items.map(item => item.id);
|
|
await remove(ids);
|
|
|
|
// We need to pass forceSave parameter, because these items already have an
|
|
// id key. Normally, this call would be interpreted as an update request.
|
|
await save(items, { forceSave: true });
|
|
}
|
|
|
|
const lastItem = last(items);
|
|
const id = lastItem ? lastItem.id : null;
|
|
|
|
return {
|
|
complete,
|
|
count: items.length,
|
|
lastIndex: id,
|
|
};
|
|
}
|