Rewrite migration 17 without idb

We ran into issues when doing async operations inside of an IndexedDB
`onupgradeneeded` handler. The errors were ‘The transaction is not active’ or
‘Transaction has finished’. The following documentation confirmed that
transactions are committed/terminated when control returns to the event loop:

Spec
- https://www.w3.org/TR/IndexedDB/#transaction-lifetime-concept
- https://www.w3.org/TR/IndexedDB/#upgrade-transaction-construct

Stack Overflow
- https://stackoverflow.com/a/11059085
- https://stackoverflow.com/a/27338944

Since the initial database migration is so critical, I decided to avoid `idb`
with promise support for IndexedDB for now, but will reconsider using it for
other tasks in the future to improve readability of IndexedDB code.
This commit is contained in:
Daniel Gasienica 2018-03-16 11:09:59 -04:00
parent db2941cbb0
commit 2642844c27

View file

@ -1,39 +1,63 @@
const idb = require('idb');
const Message = require('../../types/message');
exports.run = async (transaction) => {
const db = idb.upgradeDBFromTransaction(transaction);
const tx = db.transaction;
const messagesStore = tx.objectStore('messages');
const messagesStore = transaction.objectStore('messages');
console.log('Initialize messages schema version');
await exports._initializeMessageSchemaVersion(messagesStore);
const numUpgradedMessages = await _initializeMessageSchemaVersion(messagesStore);
console.log('Complete messages schema version initialization', { numUpgradedMessages });
console.log('Create index from attachment schema version to attachment');
messagesStore.createIndex('schemaVersion', 'schemaVersion', { unique: false });
await db.transaction.complete;
};
// NOTE: We disable `no-await-in-loop` because we want this migration to happen
// in sequence and not in parallel:
// https://eslint.org/docs/rules/no-await-in-loop#when-not-to-use-it
exports._initializeMessageSchemaVersion = async (messagesStore) => {
let cursor = await messagesStore.openCursor();
while (cursor) {
const message = cursor.value;
console.log('Initialize schema version for message:', message.id);
const _initializeMessageSchemaVersion = messagesStore =>
new Promise((resolve, reject) => {
const messagePutOperations = [];
const messageWithSchemaVersion = Message.initializeSchemaVersion(message);
const cursorRequest = messagesStore.openCursor();
cursorRequest.onsuccess = (event) => {
const cursor = event.target.result;
const hasMoreData = Boolean(cursor);
if (!hasMoreData) {
// eslint-disable-next-line more/no-then
return Promise.all(messagePutOperations)
.then(() => resolve(messagePutOperations.length));
}
const message = cursor.value;
const messageWithSchemaVersion = Message.initializeSchemaVersion(message);
messagePutOperations.push(new Promise((resolvePut) => {
console.log(
'Initialize schema version for message:',
messageWithSchemaVersion.id
);
resolvePut(putItem(
messagesStore,
messageWithSchemaVersion,
messageWithSchemaVersion.id
));
}));
return cursor.continue();
};
cursorRequest.onerror = event =>
reject(event.target.error);
});
// putItem :: IDBObjectStore -> Item -> Key -> Promise Item
const putItem = (store, item, key) =>
new Promise((resolve, reject) => {
try {
// eslint-disable-next-line no-await-in-loop
await messagesStore.put(messageWithSchemaVersion, message.id);
const request = store.put(item, key);
request.onsuccess = event =>
resolve(event.target.result);
request.onerror = event =>
reject(event.target.error);
} catch (error) {
console.log('Failed to put message with initialized schema version:', message.id);
reject(error);
}
// eslint-disable-next-line no-await-in-loop
cursor = await cursor.continue();
}
};
});