Extract database and settings modules
		
	This commit is contained in:
		
					parent
					
						
							
								5bea894abd
							
						
					
				
			
			
				commit
				
					
						016432826b
					
				
			
		
					 4 changed files with 117 additions and 102 deletions
				
			
		
							
								
								
									
										33
									
								
								js/modules/database.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								js/modules/database.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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());
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,23 +1,24 @@
 | 
				
			||||||
/* eslint-env browser */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Module to upgrade the schema of messages, e.g. migrate attachments to disk.
 | 
					// Module to upgrade the schema of messages, e.g. migrate attachments to disk.
 | 
				
			||||||
// `processAll` purposely doesn’t rely on our Backbone IndexedDB adapter to
 | 
					// `processAll` purposely doesn’t rely on our Backbone IndexedDB adapter to
 | 
				
			||||||
// prevent automatic migrations. Rather, it uses direct IndexedDB access.
 | 
					// prevent automatic migrations. Rather, it uses direct IndexedDB access.
 | 
				
			||||||
// This includes avoiding usage of `storage` module which uses Backbone under
 | 
					// This includes avoiding usage of `storage` module which uses Backbone under
 | 
				
			||||||
// the hood.
 | 
					// the hood.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* global IDBKeyRange */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const isFunction = require('lodash/isFunction');
 | 
					const isFunction = require('lodash/isFunction');
 | 
				
			||||||
const isNumber = require('lodash/isNumber');
 | 
					const isNumber = require('lodash/isNumber');
 | 
				
			||||||
const isObject = require('lodash/isObject');
 | 
					const isObject = require('lodash/isObject');
 | 
				
			||||||
const isString = require('lodash/isString');
 | 
					const isString = require('lodash/isString');
 | 
				
			||||||
const last = require('lodash/last');
 | 
					const last = require('lodash/last');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const database = require('./database');
 | 
				
			||||||
const Message = require('./types/message');
 | 
					const Message = require('./types/message');
 | 
				
			||||||
 | 
					const settings = require('./settings');
 | 
				
			||||||
const { deferredToPromise } = require('./deferred_to_promise');
 | 
					const { deferredToPromise } = require('./deferred_to_promise');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const MESSAGES_STORE_NAME = 'messages';
 | 
					const MESSAGES_STORE_NAME = 'messages';
 | 
				
			||||||
const ITEMS_STORE_NAME = 'items';
 | 
					 | 
				
			||||||
const NUM_MESSAGES_PER_BATCH = 50;
 | 
					const NUM_MESSAGES_PER_BATCH = 50;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
exports.processNext = async ({
 | 
					exports.processNext = async ({
 | 
				
			||||||
| 
						 | 
					@ -74,11 +75,11 @@ exports.processNext = async ({
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
exports.processAll = async ({
 | 
					exports.processAll = async ({
 | 
				
			||||||
    Backbone,
 | 
					  Backbone,
 | 
				
			||||||
    databaseName,
 | 
					  databaseName,
 | 
				
			||||||
    databaseVersion,
 | 
					  databaseVersion,
 | 
				
			||||||
    upgradeMessageSchema,
 | 
					  upgradeMessageSchema,
 | 
				
			||||||
  } = {}) => {
 | 
					} = {}) => {
 | 
				
			||||||
  if (!isObject(Backbone)) {
 | 
					  if (!isObject(Backbone)) {
 | 
				
			||||||
    throw new TypeError('"Backbone" is required');
 | 
					    throw new TypeError('"Backbone" is required');
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -95,8 +96,8 @@ exports.processAll = async ({
 | 
				
			||||||
    throw new TypeError('"upgradeMessageSchema" is required');
 | 
					    throw new TypeError('"upgradeMessageSchema" is required');
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const connection = await openDatabase(databaseName, databaseVersion);
 | 
					  const connection = await database.open(databaseName, databaseVersion);
 | 
				
			||||||
  const isComplete = await isMigrationComplete(connection);
 | 
					  const isComplete = await settings.isAttachmentMigrationComplete(connection);
 | 
				
			||||||
  console.log('Attachment migration status:', isComplete ? 'complete' : 'incomplete');
 | 
					  console.log('Attachment migration status:', isComplete ? 'complete' : 'incomplete');
 | 
				
			||||||
  if (isComplete) {
 | 
					  if (isComplete) {
 | 
				
			||||||
    return;
 | 
					    return;
 | 
				
			||||||
| 
						 | 
					@ -106,8 +107,9 @@ exports.processAll = async ({
 | 
				
			||||||
  let unprocessedMessages = [];
 | 
					  let unprocessedMessages = [];
 | 
				
			||||||
  let totalMessagesProcessed = 0;
 | 
					  let totalMessagesProcessed = 0;
 | 
				
			||||||
  do {
 | 
					  do {
 | 
				
			||||||
    // eslint-disable-next-line no-await-in-loop
 | 
					    const lastProcessedIndex =
 | 
				
			||||||
    const lastProcessedIndex = await getLastProcessedIndex(connection);
 | 
					      // eslint-disable-next-line no-await-in-loop
 | 
				
			||||||
 | 
					      await settings.getAttachmentMigrationLastProcessedIndex(connection);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const fetchUnprocessedMessagesStartTime = Date.now();
 | 
					    const fetchUnprocessedMessagesStartTime = Date.now();
 | 
				
			||||||
    unprocessedMessages =
 | 
					    unprocessedMessages =
 | 
				
			||||||
| 
						 | 
					@ -132,7 +134,7 @@ exports.processAll = async ({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const saveMessagesStartTime = Date.now();
 | 
					    const saveMessagesStartTime = Date.now();
 | 
				
			||||||
    const transaction = connection.transaction(MESSAGES_STORE_NAME, 'readwrite');
 | 
					    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
 | 
					    // eslint-disable-next-line no-await-in-loop
 | 
				
			||||||
    await Promise.all(upgradedMessages.map(_saveMessage({ transaction })));
 | 
					    await Promise.all(upgradedMessages.map(_saveMessage({ transaction })));
 | 
				
			||||||
    // eslint-disable-next-line no-await-in-loop
 | 
					    // eslint-disable-next-line no-await-in-loop
 | 
				
			||||||
| 
						 | 
					@ -145,7 +147,10 @@ exports.processAll = async ({
 | 
				
			||||||
    const newLastProcessedIndex = lastMessage ? lastMessage.id : null;
 | 
					    const newLastProcessedIndex = lastMessage ? lastMessage.id : null;
 | 
				
			||||||
    if (newLastProcessedIndex) {
 | 
					    if (newLastProcessedIndex) {
 | 
				
			||||||
      // eslint-disable-next-line no-await-in-loop
 | 
					      // eslint-disable-next-line no-await-in-loop
 | 
				
			||||||
      await setLastProcessedIndex(connection, newLastProcessedIndex);
 | 
					      await settings.setAttachmentMigrationLastProcessedIndex(
 | 
				
			||||||
 | 
					        connection,
 | 
				
			||||||
 | 
					        newLastProcessedIndex
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    totalMessagesProcessed += numUnprocessedMessages;
 | 
					    totalMessagesProcessed += numUnprocessedMessages;
 | 
				
			||||||
| 
						 | 
					@ -161,7 +166,7 @@ exports.processAll = async ({
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  } while (unprocessedMessages.length > 0);
 | 
					  } while (unprocessedMessages.length > 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await markMigrationComplete(connection);
 | 
					  await settings.markAttachmentMigrationComplete(connection);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  console.log('Close database connection');
 | 
					  console.log('Close database connection');
 | 
				
			||||||
  connection.close();
 | 
					  connection.close();
 | 
				
			||||||
| 
						 | 
					@ -251,89 +256,3 @@ const _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex =
 | 
				
			||||||
        reject(event.target.error);
 | 
					        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());
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,7 +8,7 @@ const { deferredToPromise } = require('../deferred_to_promise');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const closeDatabase = ({ Backbone } = {}) =>
 | 
					const closeDatabase = ({ Backbone } = {}) =>
 | 
				
			||||||
  deferredToPromise(Backbone.sync('closeall'))
 | 
					  deferredToPromise(Backbone.sync('closeall'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
exports.runMigrations = async ({ Backbone, database } = {}) => {
 | 
					exports.runMigrations = async ({ Backbone, database } = {}) => {
 | 
				
			||||||
  if (!isObject(Backbone) || !isObject(Backbone.Collection) ||
 | 
					  if (!isObject(Backbone) || !isObject(Backbone.Collection) ||
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										63
									
								
								js/modules/settings.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								js/modules/settings.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue