From 016432826bec416da6cea886ff53f6eddf94918c Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Wed, 28 Mar 2018 10:54:01 -0400 Subject: [PATCH] Extract `database` and `settings` modules --- js/modules/database.js | 33 +++++++ js/modules/messages_data_migrator.js | 121 ++++-------------------- js/modules/migrations/run_migrations.js | 2 +- js/modules/settings.js | 63 ++++++++++++ 4 files changed, 117 insertions(+), 102 deletions(-) create mode 100644 js/modules/database.js create mode 100644 js/modules/settings.js diff --git a/js/modules/database.js b/js/modules/database.js new file mode 100644 index 0000000000..c287d5109a --- /dev/null +++ b/js/modules/database.js @@ -0,0 +1,33 @@ +/* global indexedDB */ + +// Module for interacting with IndexedDB without Backbone IndexedDB adapter +// and using promises. Revisit use of `idb` dependency as it might cover +// this functionality. + +exports.open = (name, version) => { + const request = indexedDB.open(name, version); + return new Promise((resolve, reject) => { + request.onblocked = () => + reject(new Error('Database blocked')); + + request.onupgradeneeded = event => + reject(new Error('Unexpected database upgrade required:' + + `oldVersion: ${event.oldVersion}, newVersion: ${event.newVersion}`)); + + request.onerror = event => + reject(event.target.error); + + request.onsuccess = (event) => { + const connection = event.target.result; + resolve(connection); + }; + }); +}; + +exports.completeTransaction = transaction => + new Promise((resolve, reject) => { + transaction.addEventListener('abort', event => reject(event.target.error)); + transaction.addEventListener('error', event => reject(event.target.error)); + transaction.addEventListener('complete', () => resolve()); + }); + diff --git a/js/modules/messages_data_migrator.js b/js/modules/messages_data_migrator.js index f5d3301dfe..e554d72087 100644 --- a/js/modules/messages_data_migrator.js +++ b/js/modules/messages_data_migrator.js @@ -1,23 +1,24 @@ -/* eslint-env browser */ - // Module to upgrade the schema of messages, e.g. migrate attachments to disk. // `processAll` purposely doesn’t rely on our Backbone IndexedDB adapter to // prevent automatic migrations. Rather, it uses direct IndexedDB access. // This includes avoiding usage of `storage` module which uses Backbone under // the hood. +/* global IDBKeyRange */ + const isFunction = require('lodash/isFunction'); const isNumber = require('lodash/isNumber'); const isObject = require('lodash/isObject'); const isString = require('lodash/isString'); const last = require('lodash/last'); +const database = require('./database'); const Message = require('./types/message'); +const settings = require('./settings'); const { deferredToPromise } = require('./deferred_to_promise'); const MESSAGES_STORE_NAME = 'messages'; -const ITEMS_STORE_NAME = 'items'; const NUM_MESSAGES_PER_BATCH = 50; exports.processNext = async ({ @@ -74,11 +75,11 @@ exports.processNext = async ({ }; exports.processAll = async ({ - Backbone, - databaseName, - databaseVersion, - upgradeMessageSchema, - } = {}) => { + Backbone, + databaseName, + databaseVersion, + upgradeMessageSchema, +} = {}) => { if (!isObject(Backbone)) { throw new TypeError('"Backbone" is required'); } @@ -95,8 +96,8 @@ exports.processAll = async ({ throw new TypeError('"upgradeMessageSchema" is required'); } - const connection = await openDatabase(databaseName, databaseVersion); - const isComplete = await isMigrationComplete(connection); + const connection = await database.open(databaseName, databaseVersion); + const isComplete = await settings.isAttachmentMigrationComplete(connection); console.log('Attachment migration status:', isComplete ? 'complete' : 'incomplete'); if (isComplete) { return; @@ -106,8 +107,9 @@ exports.processAll = async ({ let unprocessedMessages = []; let totalMessagesProcessed = 0; do { - // eslint-disable-next-line no-await-in-loop - const lastProcessedIndex = await getLastProcessedIndex(connection); + const lastProcessedIndex = + // eslint-disable-next-line no-await-in-loop + await settings.getAttachmentMigrationLastProcessedIndex(connection); const fetchUnprocessedMessagesStartTime = Date.now(); unprocessedMessages = @@ -132,7 +134,7 @@ exports.processAll = async ({ const saveMessagesStartTime = Date.now(); const transaction = connection.transaction(MESSAGES_STORE_NAME, 'readwrite'); - const transactionCompletion = completeTransaction(transaction); + const transactionCompletion = database.completeTransaction(transaction); // eslint-disable-next-line no-await-in-loop await Promise.all(upgradedMessages.map(_saveMessage({ transaction }))); // eslint-disable-next-line no-await-in-loop @@ -145,7 +147,10 @@ exports.processAll = async ({ const newLastProcessedIndex = lastMessage ? lastMessage.id : null; if (newLastProcessedIndex) { // eslint-disable-next-line no-await-in-loop - await setLastProcessedIndex(connection, newLastProcessedIndex); + await settings.setAttachmentMigrationLastProcessedIndex( + connection, + newLastProcessedIndex + ); } totalMessagesProcessed += numUnprocessedMessages; @@ -161,7 +166,7 @@ exports.processAll = async ({ }); } while (unprocessedMessages.length > 0); - await markMigrationComplete(connection); + await settings.markAttachmentMigrationComplete(connection); console.log('Close database connection'); connection.close(); @@ -251,89 +256,3 @@ const _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex = reject(event.target.error); }); }; - -const openDatabase = (name, version) => { - const request = window.indexedDB.open(name, version); - return new Promise((resolve, reject) => { - request.onblocked = () => - reject(new Error('Database blocked')); - - request.onupgradeneeded = event => - reject(new Error('Unexpected database upgrade required:' + - `oldVersion: ${event.oldVersion}, newVersion: ${event.newVersion}`)); - - request.onerror = event => - reject(event.target.error); - - request.onsuccess = (event) => { - const connection = event.target.result; - resolve(connection); - }; - }); -}; - -const LAST_PROCESSED_INDEX_KEY = 'attachmentMigration_lastProcessedIndex'; -const IS_MIGRATION_COMPLETE_KEY = 'attachmentMigration_isComplete'; - -const getLastProcessedIndex = connection => - getItem(connection, LAST_PROCESSED_INDEX_KEY); - -const setLastProcessedIndex = (connection, value) => - setItem(connection, LAST_PROCESSED_INDEX_KEY, value); - -const isMigrationComplete = async (connection) => { - const value = await getItem(connection, IS_MIGRATION_COMPLETE_KEY); - return Boolean(value); -}; - -const markMigrationComplete = connection => - setItem(connection, IS_MIGRATION_COMPLETE_KEY, true); - -const getItem = (connection, key) => { - if (!isObject(connection)) { - throw new TypeError('"connection" is required'); - } - - if (!isString(key)) { - throw new TypeError('"key" must be a string'); - } - - const transaction = connection.transaction(ITEMS_STORE_NAME, 'readonly'); - const itemsStore = transaction.objectStore(ITEMS_STORE_NAME); - const request = itemsStore.get(key); - return new Promise((resolve, reject) => { - request.onerror = event => - reject(event.target.error); - - request.onsuccess = event => - resolve(event.target.result ? event.target.result.value : null); - }); -}; - -const setItem = (connection, key, value) => { - if (!isObject(connection)) { - throw new TypeError('"connection" is required'); - } - - if (!isString(key)) { - throw new TypeError('"key" must be a string'); - } - - const transaction = connection.transaction(ITEMS_STORE_NAME, 'readwrite'); - const itemsStore = transaction.objectStore(ITEMS_STORE_NAME); - const request = itemsStore.put({id: key, value}, key); - return new Promise((resolve, reject) => { - request.onerror = event => - reject(event.target.error); - - request.onsuccess = () => - resolve(); - }); -}; - -const completeTransaction = transaction => - new Promise((resolve, reject) => { - transaction.addEventListener('abort', event => reject(event.target.error)); - transaction.addEventListener('error', event => reject(event.target.error)); - transaction.addEventListener('complete', () => resolve()); - }); diff --git a/js/modules/migrations/run_migrations.js b/js/modules/migrations/run_migrations.js index af1c70af8d..948d1802d9 100644 --- a/js/modules/migrations/run_migrations.js +++ b/js/modules/migrations/run_migrations.js @@ -8,7 +8,7 @@ const { deferredToPromise } = require('../deferred_to_promise'); const closeDatabase = ({ Backbone } = {}) => - deferredToPromise(Backbone.sync('closeall')) + deferredToPromise(Backbone.sync('closeall')); exports.runMigrations = async ({ Backbone, database } = {}) => { if (!isObject(Backbone) || !isObject(Backbone.Collection) || diff --git a/js/modules/settings.js b/js/modules/settings.js new file mode 100644 index 0000000000..9d1cc018eb --- /dev/null +++ b/js/modules/settings.js @@ -0,0 +1,63 @@ +const isObject = require('lodash/isObject'); +const isString = require('lodash/isString'); + + +const ITEMS_STORE_NAME = 'items'; +const LAST_PROCESSED_INDEX_KEY = 'attachmentMigration_lastProcessedIndex'; +const IS_MIGRATION_COMPLETE_KEY = 'attachmentMigration_isComplete'; + +// Public API +exports.getAttachmentMigrationLastProcessedIndex = connection => + getItem(connection, LAST_PROCESSED_INDEX_KEY); + +exports.setAttachmentMigrationLastProcessedIndex = (connection, value) => + setItem(connection, LAST_PROCESSED_INDEX_KEY, value); + +exports.isAttachmentMigrationComplete = async connection => + Boolean(await getItem(connection, IS_MIGRATION_COMPLETE_KEY)); + +exports.markAttachmentMigrationComplete = connection => + setItem(connection, IS_MIGRATION_COMPLETE_KEY, true); + +// Private API +const getItem = (connection, key) => { + if (!isObject(connection)) { + throw new TypeError('"connection" is required'); + } + + if (!isString(key)) { + throw new TypeError('"key" must be a string'); + } + + const transaction = connection.transaction(ITEMS_STORE_NAME, 'readonly'); + const itemsStore = transaction.objectStore(ITEMS_STORE_NAME); + const request = itemsStore.get(key); + return new Promise((resolve, reject) => { + request.onerror = event => + reject(event.target.error); + + request.onsuccess = event => + resolve(event.target.result ? event.target.result.value : null); + }); +}; + +const setItem = (connection, key, value) => { + if (!isObject(connection)) { + throw new TypeError('"connection" is required'); + } + + if (!isString(key)) { + throw new TypeError('"key" must be a string'); + } + + const transaction = connection.transaction(ITEMS_STORE_NAME, 'readwrite'); + const itemsStore = transaction.objectStore(ITEMS_STORE_NAME); + const request = itemsStore.put({ id: key, value }, key); + return new Promise((resolve, reject) => { + request.onerror = event => + reject(event.target.error); + + request.onsuccess = () => + resolve(); + }); +};