From d16178638e6aa284a74307994da1a3f7e1c8f0a9 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Mon, 26 Mar 2018 14:57:10 -0400 Subject: [PATCH 01/56] Split database migrations into pre- and post-attachment migration - Run light-weight migrations before attachment migration. - Run regular migrations after attachments have been moved to disk. --- js/background.js | 17 +- js/database.js | 133 +------------- js/modules/migrations/17/index.js | 55 ------ ...rations_0_database_with_attachment_data.js | 166 ++++++++++++++++++ ...ions_1_database_without_attachment_data.js | 9 + preload.js | 6 +- 6 files changed, 193 insertions(+), 193 deletions(-) delete mode 100644 js/modules/migrations/17/index.js create mode 100644 js/modules/migrations/migrations_0_database_with_attachment_data.js create mode 100644 js/modules/migrations/migrations_1_database_without_attachment_data.js diff --git a/js/background.js b/js/background.js index d72edc427..7fc323e8e 100644 --- a/js/background.js +++ b/js/background.js @@ -11,12 +11,13 @@ /* global Whisper: false */ /* global wrapDeferred: false */ -;(function() { +;(async function() { 'use strict'; const { IdleDetector, MessageDataMigrator } = Signal.Workflow; const { Errors, Message } = window.Signal.Types; const { upgradeMessageSchema } = window.Signal.Migrations; + const { Migrations0DatabaseWithAttachmentData } = window.Signal.Database; const { Views } = window.Signal; // Implicitly used in `indexeddb-backbonejs-adapter`: @@ -75,13 +76,17 @@ return accountManager; }; - const cancelInitializationMessage = Views.Initialization.setMessage(); - console.log('Start IndexedDB migrations'); - storage.fetch(); - - /* eslint-enable */ /* jshint ignore:start */ + const cancelInitializationMessage = Views.Initialization.setMessage(); + console.log('Start IndexedDB migrations'); + + console.log('Migrate database with attachments'); + await Migrations0DatabaseWithAttachmentData.run({ Backbone }); + + console.log('Migrate database without attachments'); + storage.fetch(); + const NUM_MESSAGE_UPGRADES_PER_IDLE = 2; const idleDetector = new IdleDetector(); idleDetector.on('idle', async () => { diff --git a/js/database.js b/js/database.js index a7335abd3..db0688d35 100644 --- a/js/database.js +++ b/js/database.js @@ -2,13 +2,11 @@ /* global Backbone: false */ /* global _: false */ -/* eslint-disable more/no-then */ - // eslint-disable-next-line func-names (function () { 'use strict'; - const { Migrations } = window.Signal; + const { Migrations1DatabaseWithoutAttachmentData } = window.Signal.Database; window.Whisper = window.Whisper || {}; window.Whisper.Database = window.Whisper.Database || {}; @@ -125,132 +123,5 @@ request.onsuccess = resolve; })); - Whisper.Database.migrations = [ - { - version: '12.0', - migrate(transaction, next) { - console.log('migration 12.0'); - console.log('creating object stores'); - const messages = transaction.db.createObjectStore('messages'); - messages.createIndex('conversation', ['conversationId', 'received_at'], { - unique: false, - }); - messages.createIndex('receipt', 'sent_at', { unique: false }); - messages.createIndex('unread', ['conversationId', 'unread'], { unique: false }); - messages.createIndex('expires_at', 'expires_at', { unique: false }); - - const conversations = transaction.db.createObjectStore('conversations'); - conversations.createIndex('inbox', 'active_at', { unique: false }); - conversations.createIndex('group', 'members', { - unique: false, - multiEntry: true, - }); - conversations.createIndex('type', 'type', { - unique: false, - }); - conversations.createIndex('search', 'tokens', { - unique: false, - multiEntry: true, - }); - - transaction.db.createObjectStore('groups'); - - transaction.db.createObjectStore('sessions'); - transaction.db.createObjectStore('identityKeys'); - transaction.db.createObjectStore('preKeys'); - transaction.db.createObjectStore('signedPreKeys'); - transaction.db.createObjectStore('items'); - - console.log('creating debug log'); - transaction.db.createObjectStore('debug'); - - next(); - }, - }, - { - version: '13.0', - migrate(transaction, next) { - console.log('migration 13.0'); - console.log('Adding fields to identity keys'); - const identityKeys = transaction.objectStore('identityKeys'); - const request = identityKeys.openCursor(); - const promises = []; - request.onsuccess = (event) => { - const cursor = event.target.result; - if (cursor) { - const attributes = cursor.value; - attributes.timestamp = 0; - attributes.firstUse = false; - attributes.nonblockingApproval = false; - attributes.verified = 0; - promises.push(new Promise(((resolve, reject) => { - const putRequest = identityKeys.put(attributes, attributes.id); - putRequest.onsuccess = resolve; - putRequest.onerror = (e) => { - console.log(e); - reject(e); - }; - }))); - cursor.continue(); - } else { - // no more results - Promise.all(promises).then(() => { - next(); - }); - } - }; - request.onerror = (event) => { - console.log(event); - }; - }, - }, - { - version: '14.0', - migrate(transaction, next) { - console.log('migration 14.0'); - console.log('Adding unprocessed message store'); - const unprocessed = transaction.db.createObjectStore('unprocessed'); - unprocessed.createIndex('received', 'timestamp', { unique: false }); - next(); - }, - }, - { - version: '15.0', - migrate(transaction, next) { - console.log('migration 15.0'); - console.log('Adding messages index for de-duplication'); - const messages = transaction.objectStore('messages'); - messages.createIndex('unique', ['source', 'sourceDevice', 'sent_at'], { - unique: true, - }); - next(); - }, - }, - { - version: '16.0', - migrate(transaction, next) { - console.log('migration 16.0'); - console.log('Dropping log table, since we now log to disk'); - transaction.db.deleteObjectStore('debug'); - next(); - }, - }, - { - version: 17, - async migrate(transaction, next) { - console.log('migration 17'); - console.log('Start migration to database version 17'); - - const start = Date.now(); - await Migrations.V17.run(transaction); - const duration = Date.now() - start; - - console.log( - 'Complete migration to database version 17.', - `Duration: ${duration}ms` - ); - next(); - }, - }, - ]; + Whisper.Database.migrations = Migrations1DatabaseWithoutAttachmentData.migrations; }()); diff --git a/js/modules/migrations/17/index.js b/js/modules/migrations/17/index.js deleted file mode 100644 index feffbb058..000000000 --- a/js/modules/migrations/17/index.js +++ /dev/null @@ -1,55 +0,0 @@ -const Message = require('../../types/message'); - - -exports.run = async (transaction) => { - const messagesStore = transaction.objectStore('messages'); - - console.log('Initialize messages schema version'); - 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 }); -}; - -const _initializeMessageSchemaVersion = messagesStore => - new Promise((resolve, reject) => { - const messagePutOperations = []; - - const cursorRequest = messagesStore.openCursor(); - cursorRequest.onsuccess = async (event) => { - const cursor = event.target.result; - const hasMoreData = Boolean(cursor); - if (!hasMoreData) { - await Promise.all(messagePutOperations); - return resolve(messagePutOperations.length); - } - - const message = cursor.value; - const messageWithSchemaVersion = Message.initializeSchemaVersion(message); - messagePutOperations.push(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 { - const request = store.put(item, key); - request.onsuccess = event => - resolve(event.target.result); - request.onerror = event => - reject(event.target.error); - } catch (error) { - reject(error); - } - }); diff --git a/js/modules/migrations/migrations_0_database_with_attachment_data.js b/js/modules/migrations/migrations_0_database_with_attachment_data.js new file mode 100644 index 000000000..45fc11505 --- /dev/null +++ b/js/modules/migrations/migrations_0_database_with_attachment_data.js @@ -0,0 +1,166 @@ +const isFunction = require('lodash/isFunction'); +const isObject = require('lodash/isObject'); + + +// IMPORTANT: The migrations below are run on a database that may be very large +// due to attachments being directly stored inside the database. Please avoid +// any expensive operations, e.g. modifying all messages / attachments, etc., as +// it may cause out-of-memory errors for users with long histories: +// https://github.com/signalapp/Signal-Desktop/issues/2163 +const migrations = [ + { + version: '12.0', + migrate(transaction, next) { + console.log('Migration 12'); + console.log('creating object stores'); + const messages = transaction.db.createObjectStore('messages'); + messages.createIndex('conversation', ['conversationId', 'received_at'], { + unique: false, + }); + messages.createIndex('receipt', 'sent_at', { unique: false }); + messages.createIndex('unread', ['conversationId', 'unread'], { unique: false }); + messages.createIndex('expires_at', 'expires_at', { unique: false }); + + const conversations = transaction.db.createObjectStore('conversations'); + conversations.createIndex('inbox', 'active_at', { unique: false }); + conversations.createIndex('group', 'members', { + unique: false, + multiEntry: true, + }); + conversations.createIndex('type', 'type', { + unique: false, + }); + conversations.createIndex('search', 'tokens', { + unique: false, + multiEntry: true, + }); + + transaction.db.createObjectStore('groups'); + + transaction.db.createObjectStore('sessions'); + transaction.db.createObjectStore('identityKeys'); + transaction.db.createObjectStore('preKeys'); + transaction.db.createObjectStore('signedPreKeys'); + transaction.db.createObjectStore('items'); + + console.log('creating debug log'); + transaction.db.createObjectStore('debug'); + + next(); + }, + }, + { + version: '13.0', + migrate(transaction, next) { + console.log('Migration 13'); + console.log('Adding fields to identity keys'); + const identityKeys = transaction.objectStore('identityKeys'); + const request = identityKeys.openCursor(); + const promises = []; + request.onsuccess = (event) => { + const cursor = event.target.result; + if (cursor) { + const attributes = cursor.value; + attributes.timestamp = 0; + attributes.firstUse = false; + attributes.nonblockingApproval = false; + attributes.verified = 0; + promises.push(new Promise(((resolve, reject) => { + const putRequest = identityKeys.put(attributes, attributes.id); + putRequest.onsuccess = resolve; + putRequest.onerror = (e) => { + console.log(e); + reject(e); + }; + }))); + cursor.continue(); + } else { + // no more results + // eslint-disable-next-line more/no-then + Promise.all(promises).then(() => { + next(); + }); + } + }; + request.onerror = (event) => { + console.log(event); + }; + }, + }, + { + version: '14.0', + migrate(transaction, next) { + console.log('Migration 14'); + console.log('Adding unprocessed message store'); + const unprocessed = transaction.db.createObjectStore('unprocessed'); + unprocessed.createIndex('received', 'timestamp', { unique: false }); + next(); + }, + }, + { + version: '15.0', + migrate(transaction, next) { + console.log('Migration 15'); + console.log('Adding messages index for de-duplication'); + const messages = transaction.objectStore('messages'); + messages.createIndex('unique', ['source', 'sourceDevice', 'sent_at'], { + unique: true, + }); + next(); + }, + }, + { + version: '16.0', + migrate(transaction, next) { + console.log('Migration 16'); + console.log('Dropping log table, since we now log to disk'); + transaction.db.deleteObjectStore('debug'); + next(); + }, + }, + { + version: 17, + async migrate(transaction, next) { + console.log('Migration 17'); + console.log('Start migration to database version 17'); + + const start = Date.now(); + + const messagesStore = transaction.objectStore('messages'); + console.log('Create index from attachment schema version to attachment'); + messagesStore.createIndex('schemaVersion', 'schemaVersion', { unique: false }); + + const duration = Date.now() - start; + + console.log( + 'Complete migration to database version 17.', + `Duration: ${duration}ms` + ); + next(); + }, + }, +]; + +const database = { + id: 'signal', + nolog: true, + migrations, +}; + +exports.run = ({ Backbone } = {}) => { + if (!isObject(Backbone) || !isObject(Backbone.Collection) || + !isFunction(Backbone.Collection.extend)) { + throw new TypeError('"Backbone" is required'); + } + + const migrationCollection = new (Backbone.Collection.extend({ + database, + storeName: 'conversations', + }))(); + + return new Promise((resolve) => { + // NOTE: This `then` refers to a jQuery `Deferred`: + // eslint-disable-next-line more/no-then + migrationCollection.fetch().then(() => resolve()); + }); +}; diff --git a/js/modules/migrations/migrations_1_database_without_attachment_data.js b/js/modules/migrations/migrations_1_database_without_attachment_data.js new file mode 100644 index 000000000..5cb5446c8 --- /dev/null +++ b/js/modules/migrations/migrations_1_database_without_attachment_data.js @@ -0,0 +1,9 @@ +exports.migrations = [ + { + version: 18, + async migrate(transaction, next) { + console.log('Migration 18'); + next(); + }, + }, +]; diff --git a/preload.js b/preload.js index c10ed563a..a2feb6736 100644 --- a/preload.js +++ b/preload.js @@ -127,12 +127,16 @@ window.Signal = {}; window.Signal.Backup = require('./js/modules/backup'); window.Signal.Crypto = require('./js/modules/crypto'); + window.Signal.Database = {}; + window.Signal.Database.Migrations0DatabaseWithAttachmentData = + require('./js/modules/migrations/migrations_0_database_with_attachment_data'); + window.Signal.Database.Migrations1DatabaseWithoutAttachmentData = + require('./js/modules/migrations/migrations_1_database_without_attachment_data'); window.Signal.Logs = require('./js/modules/logs'); window.Signal.Migrations = {}; window.Signal.Migrations.loadAttachmentData = Attachment.loadData(readAttachmentData); window.Signal.Migrations.deleteAttachmentData = Attachment.deleteData(deleteAttachmentData); window.Signal.Migrations.upgradeMessageSchema = upgradeMessageSchema; - window.Signal.Migrations.V17 = require('./js/modules/migrations/17'); window.Signal.OS = require('./js/modules/os'); window.Signal.Types = {}; window.Signal.Types.Attachment = Attachment; From d7c8d33edb5d90e5acb4501b51f7f91fa5a33649 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Mon, 26 Mar 2018 15:17:19 -0400 Subject: [PATCH 02/56] Extract `runMigrations` --- ...rations_0_database_with_attachment_data.js | 22 +++------------ js/modules/migrations/run_migrations.js | 27 +++++++++++++++++++ 2 files changed, 30 insertions(+), 19 deletions(-) create mode 100644 js/modules/migrations/run_migrations.js diff --git a/js/modules/migrations/migrations_0_database_with_attachment_data.js b/js/modules/migrations/migrations_0_database_with_attachment_data.js index 45fc11505..6f8f99079 100644 --- a/js/modules/migrations/migrations_0_database_with_attachment_data.js +++ b/js/modules/migrations/migrations_0_database_with_attachment_data.js @@ -1,5 +1,4 @@ -const isFunction = require('lodash/isFunction'); -const isObject = require('lodash/isObject'); +const { runMigrations } = require('./run_migrations'); // IMPORTANT: The migrations below are run on a database that may be very large @@ -147,20 +146,5 @@ const database = { migrations, }; -exports.run = ({ Backbone } = {}) => { - if (!isObject(Backbone) || !isObject(Backbone.Collection) || - !isFunction(Backbone.Collection.extend)) { - throw new TypeError('"Backbone" is required'); - } - - const migrationCollection = new (Backbone.Collection.extend({ - database, - storeName: 'conversations', - }))(); - - return new Promise((resolve) => { - // NOTE: This `then` refers to a jQuery `Deferred`: - // eslint-disable-next-line more/no-then - migrationCollection.fetch().then(() => resolve()); - }); -}; +exports.run = ({ Backbone } = {}) => + runMigrations({ Backbone, database }); diff --git a/js/modules/migrations/run_migrations.js b/js/modules/migrations/run_migrations.js new file mode 100644 index 000000000..cdd26dd97 --- /dev/null +++ b/js/modules/migrations/run_migrations.js @@ -0,0 +1,27 @@ +const isFunction = require('lodash/isFunction'); +const isObject = require('lodash/isObject'); +const isString = require('lodash/isString'); + + +exports.runMigrations = ({ Backbone, database } = {}) => { + if (!isObject(Backbone) || !isObject(Backbone.Collection) || + !isFunction(Backbone.Collection.extend)) { + throw new TypeError('"Backbone" is required'); + } + + if (!isObject(database) || !isString(database.id) || + !Array.isArray(database.migrations)) { + throw new TypeError('"database" is required'); + } + + const migrationCollection = new (Backbone.Collection.extend({ + database, + storeName: 'items', + }))(); + + return new Promise((resolve) => { + // NOTE: This `then` refers to a jQuery `Deferred`: + // eslint-disable-next-line more/no-then + migrationCollection.fetch().then(() => resolve()); + }); +}; From e2f1339ab97fa4d6e475dcc298390adac8549eb4 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Mon, 26 Mar 2018 15:17:35 -0400 Subject: [PATCH 03/56] Explicitly run post-attachment migrations --- js/background.js | 11 ++++++++++- .../migrations_1_database_without_attachment_data.js | 6 ++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/js/background.js b/js/background.js index 7fc323e8e..e21c45e28 100644 --- a/js/background.js +++ b/js/background.js @@ -17,7 +17,10 @@ const { IdleDetector, MessageDataMigrator } = Signal.Workflow; const { Errors, Message } = window.Signal.Types; const { upgradeMessageSchema } = window.Signal.Migrations; - const { Migrations0DatabaseWithAttachmentData } = window.Signal.Database; + const { + Migrations0DatabaseWithAttachmentData, + Migrations1DatabaseWithoutAttachmentData, + } = window.Signal.Database; const { Views } = window.Signal; // Implicitly used in `indexeddb-backbonejs-adapter`: @@ -85,6 +88,12 @@ await Migrations0DatabaseWithAttachmentData.run({ Backbone }); console.log('Migrate database without attachments'); + await Migrations1DatabaseWithoutAttachmentData.run({ + Backbone, + Database: Whisper.Database, + }); + + console.log('Storage fetch'); storage.fetch(); const NUM_MESSAGE_UPGRADES_PER_IDLE = 2; diff --git a/js/modules/migrations/migrations_1_database_without_attachment_data.js b/js/modules/migrations/migrations_1_database_without_attachment_data.js index 5cb5446c8..04bacb870 100644 --- a/js/modules/migrations/migrations_1_database_without_attachment_data.js +++ b/js/modules/migrations/migrations_1_database_without_attachment_data.js @@ -1,3 +1,6 @@ +const { runMigrations } = require('./run_migrations'); + + exports.migrations = [ { version: 18, @@ -7,3 +10,6 @@ exports.migrations = [ }, }, ]; + +exports.run = ({ Backbone, Database } = {}) => + runMigrations({ Backbone, database: Database }); From c765422fa14888bebb274e383efe08d56d5c948f Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Mon, 26 Mar 2018 15:30:51 -0400 Subject: [PATCH 04/56] Extract `deferredToPromise` --- js/modules/deferred_to_promise.js | 3 +++ preload.js | 7 ++----- 2 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 js/modules/deferred_to_promise.js diff --git a/js/modules/deferred_to_promise.js b/js/modules/deferred_to_promise.js new file mode 100644 index 000000000..d6409b09f --- /dev/null +++ b/js/modules/deferred_to_promise.js @@ -0,0 +1,3 @@ +exports.deferredToPromise = deferred => + // eslint-disable-next-line more/no-then + new Promise((resolve, reject) => deferred.then(resolve, reject)); diff --git a/preload.js b/preload.js index a2feb6736..9509f30fe 100644 --- a/preload.js +++ b/preload.js @@ -7,17 +7,14 @@ const Attachment = require('./js/modules/types/attachment'); const Attachments = require('./app/attachments'); const Message = require('./js/modules/types/message'); + const { deferredToPromise } = require('./js/modules/deferred_to_promise'); const { app } = electron.remote; window.PROTO_ROOT = 'protos'; window.config = require('url').parse(window.location.toString(), true).query; - window.wrapDeferred = function(deferred) { - return new Promise(function(resolve, reject) { - deferred.then(resolve, reject); - }); - }; + window.wrapDeferred = deferredToPromise; const ipc = electron.ipcRenderer; window.config.localeMessages = ipc.sendSync('locale-data'); From fcd30cd91943036d94473e14731d3d1ee09d9bed Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Mon, 26 Mar 2018 15:34:36 -0400 Subject: [PATCH 05/56] Close database after migration This is not 100% reliable as database connections are closed in a separate thread according to the documentation: - https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase/close - https://stackoverflow.com/a/18639298 - https://github.com/superfeedr/indexeddb-backbonejs-adapter/blob/80c7a06d5c0a75296106e9a62663230459cef857/backbone-indexeddb.js#L558-L565 --- js/background.js | 5 ++++- ...migrations_0_database_with_attachment_data.js | 4 ++-- ...rations_1_database_without_attachment_data.js | 4 ++-- js/modules/migrations/run_migrations.js | 16 ++++++++++------ 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/js/background.js b/js/background.js index e21c45e28..7eea361d9 100644 --- a/js/background.js +++ b/js/background.js @@ -85,11 +85,14 @@ console.log('Start IndexedDB migrations'); console.log('Migrate database with attachments'); - await Migrations0DatabaseWithAttachmentData.run({ Backbone }); + const closeDatabase = () => + Whisper.Database.close(); + await Migrations0DatabaseWithAttachmentData.run({ Backbone, closeDatabase }); console.log('Migrate database without attachments'); await Migrations1DatabaseWithoutAttachmentData.run({ Backbone, + closeDatabase, Database: Whisper.Database, }); diff --git a/js/modules/migrations/migrations_0_database_with_attachment_data.js b/js/modules/migrations/migrations_0_database_with_attachment_data.js index 6f8f99079..0fa12cc97 100644 --- a/js/modules/migrations/migrations_0_database_with_attachment_data.js +++ b/js/modules/migrations/migrations_0_database_with_attachment_data.js @@ -146,5 +146,5 @@ const database = { migrations, }; -exports.run = ({ Backbone } = {}) => - runMigrations({ Backbone, database }); +exports.run = ({ Backbone, closeDatabase } = {}) => + runMigrations({ Backbone, closeDatabase, database }); diff --git a/js/modules/migrations/migrations_1_database_without_attachment_data.js b/js/modules/migrations/migrations_1_database_without_attachment_data.js index 04bacb870..46ca5aedd 100644 --- a/js/modules/migrations/migrations_1_database_without_attachment_data.js +++ b/js/modules/migrations/migrations_1_database_without_attachment_data.js @@ -11,5 +11,5 @@ exports.migrations = [ }, ]; -exports.run = ({ Backbone, Database } = {}) => - runMigrations({ Backbone, database: Database }); +exports.run = ({ Backbone, closeDatabase, Database } = {}) => + runMigrations({ Backbone, closeDatabase, database: Database }); diff --git a/js/modules/migrations/run_migrations.js b/js/modules/migrations/run_migrations.js index cdd26dd97..878ef3bad 100644 --- a/js/modules/migrations/run_migrations.js +++ b/js/modules/migrations/run_migrations.js @@ -2,13 +2,19 @@ const isFunction = require('lodash/isFunction'); const isObject = require('lodash/isObject'); const isString = require('lodash/isString'); +const { deferredToPromise } = require('../deferred_to_promise'); -exports.runMigrations = ({ Backbone, database } = {}) => { + +exports.runMigrations = async ({ Backbone, closeDatabase, database } = {}) => { if (!isObject(Backbone) || !isObject(Backbone.Collection) || !isFunction(Backbone.Collection.extend)) { throw new TypeError('"Backbone" is required'); } + if (!isFunction(closeDatabase)) { + throw new TypeError('"closeDatabase" is required'); + } + if (!isObject(database) || !isString(database.id) || !Array.isArray(database.migrations)) { throw new TypeError('"database" is required'); @@ -19,9 +25,7 @@ exports.runMigrations = ({ Backbone, database } = {}) => { storeName: 'items', }))(); - return new Promise((resolve) => { - // NOTE: This `then` refers to a jQuery `Deferred`: - // eslint-disable-next-line more/no-then - migrationCollection.fetch().then(() => resolve()); - }); + await deferredToPromise(migrationCollection.fetch()); + await closeDatabase(); + return; }; From 106ce21c49da96960150a675715209a3da42a36a Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Mon, 26 Mar 2018 16:18:56 -0400 Subject: [PATCH 06/56] Remove redundant log message --- .../migrations/migrations_0_database_with_attachment_data.js | 1 - 1 file changed, 1 deletion(-) diff --git a/js/modules/migrations/migrations_0_database_with_attachment_data.js b/js/modules/migrations/migrations_0_database_with_attachment_data.js index 0fa12cc97..d26697b34 100644 --- a/js/modules/migrations/migrations_0_database_with_attachment_data.js +++ b/js/modules/migrations/migrations_0_database_with_attachment_data.js @@ -121,7 +121,6 @@ const migrations = [ version: 17, async migrate(transaction, next) { console.log('Migration 17'); - console.log('Start migration to database version 17'); const start = Date.now(); From da144edc56613e7b2669a718645f50795a792c48 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Mon, 26 Mar 2018 16:23:34 -0400 Subject: [PATCH 07/56] Manually close database connection after migration --- js/background.js | 8 ++--- ...rations_0_database_with_attachment_data.js | 4 +-- ...ions_1_database_without_attachment_data.js | 4 +-- js/modules/migrations/run_migrations.js | 31 ++++++++++++++----- 4 files changed, 31 insertions(+), 16 deletions(-) diff --git a/js/background.js b/js/background.js index 7eea361d9..c03ea2562 100644 --- a/js/background.js +++ b/js/background.js @@ -11,7 +11,8 @@ /* global Whisper: false */ /* global wrapDeferred: false */ -;(async function() { + +;(/* jshint ignore:start */ async /* jshint ignore:end */function() { 'use strict'; const { IdleDetector, MessageDataMigrator } = Signal.Workflow; @@ -85,14 +86,11 @@ console.log('Start IndexedDB migrations'); console.log('Migrate database with attachments'); - const closeDatabase = () => - Whisper.Database.close(); - await Migrations0DatabaseWithAttachmentData.run({ Backbone, closeDatabase }); + await Migrations0DatabaseWithAttachmentData.run({ Backbone }); console.log('Migrate database without attachments'); await Migrations1DatabaseWithoutAttachmentData.run({ Backbone, - closeDatabase, Database: Whisper.Database, }); diff --git a/js/modules/migrations/migrations_0_database_with_attachment_data.js b/js/modules/migrations/migrations_0_database_with_attachment_data.js index d26697b34..19b9d7063 100644 --- a/js/modules/migrations/migrations_0_database_with_attachment_data.js +++ b/js/modules/migrations/migrations_0_database_with_attachment_data.js @@ -145,5 +145,5 @@ const database = { migrations, }; -exports.run = ({ Backbone, closeDatabase } = {}) => - runMigrations({ Backbone, closeDatabase, database }); +exports.run = ({ Backbone } = {}) => + runMigrations({ Backbone, database }); diff --git a/js/modules/migrations/migrations_1_database_without_attachment_data.js b/js/modules/migrations/migrations_1_database_without_attachment_data.js index 46ca5aedd..04bacb870 100644 --- a/js/modules/migrations/migrations_1_database_without_attachment_data.js +++ b/js/modules/migrations/migrations_1_database_without_attachment_data.js @@ -11,5 +11,5 @@ exports.migrations = [ }, ]; -exports.run = ({ Backbone, closeDatabase, Database } = {}) => - runMigrations({ Backbone, closeDatabase, database: Database }); +exports.run = ({ Backbone, Database } = {}) => + runMigrations({ Backbone, database: Database }); diff --git a/js/modules/migrations/run_migrations.js b/js/modules/migrations/run_migrations.js index 878ef3bad..7296debb6 100644 --- a/js/modules/migrations/run_migrations.js +++ b/js/modules/migrations/run_migrations.js @@ -1,3 +1,5 @@ +/* eslint-env browser */ + const isFunction = require('lodash/isFunction'); const isObject = require('lodash/isObject'); const isString = require('lodash/isString'); @@ -5,16 +7,32 @@ const isString = require('lodash/isString'); const { deferredToPromise } = require('../deferred_to_promise'); -exports.runMigrations = async ({ Backbone, closeDatabase, database } = {}) => { +const closeDatabase = name => + new Promise((resolve, reject) => { + const request = window.indexedDB.open(name); + request.onblocked = () => { + reject(new Error(`Database '${name}' blocked`)); + }; + request.onupgradeneeded = (event) => { + reject(new Error('Unexpected database upgraded needed:' + + ` oldVersion: ${event.oldVersion}, newVersion: ${event.newVersion}`)); + }; + request.onerror = (event) => { + reject(event.target.error); + }; + request.onsuccess = (event) => { + const connection = event.target.result; + connection.close(); + resolve(); + }; + }); + +exports.runMigrations = async ({ Backbone, database } = {}) => { if (!isObject(Backbone) || !isObject(Backbone.Collection) || !isFunction(Backbone.Collection.extend)) { throw new TypeError('"Backbone" is required'); } - if (!isFunction(closeDatabase)) { - throw new TypeError('"closeDatabase" is required'); - } - if (!isObject(database) || !isString(database.id) || !Array.isArray(database.migrations)) { throw new TypeError('"database" is required'); @@ -26,6 +44,5 @@ exports.runMigrations = async ({ Backbone, closeDatabase, database } = {}) => { }))(); await deferredToPromise(migrationCollection.fetch()); - await closeDatabase(); - return; + await closeDatabase(database.id); }; From 40c40c800a9247ddab10d8a41641a916d4d5a36b Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Mon, 26 Mar 2018 16:32:22 -0400 Subject: [PATCH 08/56] Prefer `exports` --- js/modules/messages_data_migrator.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/js/modules/messages_data_migrator.js b/js/modules/messages_data_migrator.js index c1b4f3e0d..9ccb8c98a 100644 --- a/js/modules/messages_data_migrator.js +++ b/js/modules/messages_data_migrator.js @@ -3,7 +3,7 @@ const isFunction = require('lodash/isFunction'); const Message = require('./types/message'); -const processNext = async ({ +exports.processNext = async ({ BackboneMessage, BackboneMessageCollection, count, @@ -92,8 +92,3 @@ const _fetchMessagesRequiringSchemaUpgrade = resolve(messages); })); }; - - -module.exports = { - processNext, -}; From 579b01283e73422b8223b47032999156a82fa01f Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Mon, 26 Mar 2018 16:33:23 -0400 Subject: [PATCH 09/56] Replace `wrapDeferred` with `deferredToPromise` --- js/background.js | 1 - js/modules/messages_data_migrator.js | 13 +++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/js/background.js b/js/background.js index c03ea2562..18a4c6d4e 100644 --- a/js/background.js +++ b/js/background.js @@ -105,7 +105,6 @@ BackboneMessageCollection: Whisper.MessageCollection, count: NUM_MESSAGE_UPGRADES_PER_IDLE, upgradeMessageSchema, - wrapDeferred, }); console.log('Upgrade message schema:', results); diff --git a/js/modules/messages_data_migrator.js b/js/modules/messages_data_migrator.js index 9ccb8c98a..2df16daff 100644 --- a/js/modules/messages_data_migrator.js +++ b/js/modules/messages_data_migrator.js @@ -1,6 +1,8 @@ const isNumber = require('lodash/isNumber'); const isFunction = require('lodash/isFunction'); + const Message = require('./types/message'); +const { deferredToPromise } = require('./deferred_to_promise'); exports.processNext = async ({ @@ -8,7 +10,6 @@ exports.processNext = async ({ BackboneMessageCollection, count, upgradeMessageSchema, - wrapDeferred, } = {}) => { if (!isFunction(BackboneMessage)) { throw new TypeError('`BackboneMessage` (Whisper.Message) constructor is required'); @@ -27,10 +28,6 @@ exports.processNext = async ({ throw new TypeError('`upgradeMessageSchema` is required'); } - if (!isFunction(wrapDeferred)) { - throw new TypeError('`wrapDeferred` is required'); - } - const startTime = Date.now(); const startFetchTime = Date.now(); @@ -44,7 +41,7 @@ exports.processNext = async ({ const upgradeDuration = Date.now() - startUpgradeTime; const startSaveTime = Date.now(); - const saveMessage = _saveMessage({ BackboneMessage, wrapDeferred }); + const saveMessage = _saveMessage({ BackboneMessage }); await Promise.all(upgradedMessages.map(saveMessage)); const saveDuration = Date.now() - startSaveTime; @@ -61,9 +58,9 @@ exports.processNext = async ({ }; }; -const _saveMessage = ({ BackboneMessage, wrapDeferred } = {}) => (message) => { +const _saveMessage = ({ BackboneMessage } = {}) => (message) => { const backboneMessage = new BackboneMessage(message); - return wrapDeferred(backboneMessage.save()); + return deferredToPromise(backboneMessage.save()); }; const _fetchMessagesRequiringSchemaUpgrade = From 172616ca4fbd12d13f224dd7ad5ddc4feb77ab4d Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Mon, 26 Mar 2018 17:13:25 -0400 Subject: [PATCH 10/56] Add log message for dummy migration 18 --- .../migrations/migrations_1_database_without_attachment_data.js | 1 + 1 file changed, 1 insertion(+) diff --git a/js/modules/migrations/migrations_1_database_without_attachment_data.js b/js/modules/migrations/migrations_1_database_without_attachment_data.js index 04bacb870..2b7c010c4 100644 --- a/js/modules/migrations/migrations_1_database_without_attachment_data.js +++ b/js/modules/migrations/migrations_1_database_without_attachment_data.js @@ -6,6 +6,7 @@ exports.migrations = [ version: 18, async migrate(transaction, next) { console.log('Migration 18'); + console.log('Attachments stored on disk'); next(); }, }, From 8ea257ad4dbfd2f51ff8d28f3743d5bb15ff8867 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Mon, 26 Mar 2018 19:04:38 -0400 Subject: [PATCH 11/56] Use double quotes for identifiers in error messages --- js/modules/messages_data_migrator.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/js/modules/messages_data_migrator.js b/js/modules/messages_data_migrator.js index 2df16daff..dc4bbd377 100644 --- a/js/modules/messages_data_migrator.js +++ b/js/modules/messages_data_migrator.js @@ -12,20 +12,20 @@ exports.processNext = async ({ upgradeMessageSchema, } = {}) => { if (!isFunction(BackboneMessage)) { - throw new TypeError('`BackboneMessage` (Whisper.Message) constructor is required'); + throw new TypeError('"BackboneMessage" (Whisper.Message) constructor is required'); } if (!isFunction(BackboneMessageCollection)) { - throw new TypeError('`BackboneMessageCollection` (Whisper.MessageCollection)' + + throw new TypeError('"BackboneMessageCollection" (Whisper.MessageCollection)' + ' constructor is required'); } if (!isNumber(count)) { - throw new TypeError('`count` is required'); + throw new TypeError('"count" is required'); } if (!isFunction(upgradeMessageSchema)) { - throw new TypeError('`upgradeMessageSchema` is required'); + throw new TypeError('"upgradeMessageSchema" is required'); } const startTime = Date.now(); @@ -66,12 +66,12 @@ const _saveMessage = ({ BackboneMessage } = {}) => (message) => { const _fetchMessagesRequiringSchemaUpgrade = async ({ BackboneMessageCollection, count } = {}) => { if (!isFunction(BackboneMessageCollection)) { - throw new TypeError('`BackboneMessageCollection` (Whisper.MessageCollection)' + + throw new TypeError('"BackboneMessageCollection" (Whisper.MessageCollection)' + ' constructor is required'); } if (!isNumber(count)) { - throw new TypeError('`count` is required'); + throw new TypeError('"count" is required'); } const collection = new BackboneMessageCollection(); From 457bf7ab9ddb33a3b9110799e2ab0a45dd213d35 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Mon, 26 Mar 2018 19:07:50 -0400 Subject: [PATCH 12/56] Add `createCollection` function --- ...rations_0_database_with_attachment_data.js | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/js/modules/migrations/migrations_0_database_with_attachment_data.js b/js/modules/migrations/migrations_0_database_with_attachment_data.js index 19b9d7063..e00f71870 100644 --- a/js/modules/migrations/migrations_0_database_with_attachment_data.js +++ b/js/modules/migrations/migrations_0_database_with_attachment_data.js @@ -1,3 +1,7 @@ +const isFunction = require('lodash/isFunction'); +const isObject = require('lodash/isObject'); +const isString = require('lodash/isString'); + const { runMigrations } = require('./run_migrations'); @@ -147,3 +151,21 @@ const database = { exports.run = ({ Backbone } = {}) => runMigrations({ Backbone, database }); + +exports.createCollection = ({ Backbone, storeName }) => { + if (!isObject(Backbone) || !isObject(Backbone.Collection) || + !isFunction(Backbone.Collection.extend)) { + throw new TypeError('"Backbone" is required'); + } + + if (!isString(storeName)) { + throw new TypeError('"database" is required'); + } + + const collection = new (Backbone.Collection.extend({ + database, + storeName, + }))(); + + return collection; +}; From b6e978f74c75eddbeef4291afc72635b439f7ddf Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Mon, 26 Mar 2018 19:09:06 -0400 Subject: [PATCH 13/56] Implement `MessagesDataMigrator.processAll` --- js/modules/messages_data_migrator.js | 63 +++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/js/modules/messages_data_migrator.js b/js/modules/messages_data_migrator.js index dc4bbd377..e3ce66c89 100644 --- a/js/modules/messages_data_migrator.js +++ b/js/modules/messages_data_migrator.js @@ -1,9 +1,12 @@ -const isNumber = require('lodash/isNumber'); const isFunction = require('lodash/isFunction'); +const isNumber = require('lodash/isNumber'); +const isObject = require('lodash/isObject'); +const isString = require('lodash/isString'); const Message = require('./types/message'); const { deferredToPromise } = require('./deferred_to_promise'); - +const Migrations0DatabaseWithAttachmentData = + require('./migrations/migrations_0_database_with_attachment_data'); exports.processNext = async ({ BackboneMessage, @@ -58,6 +61,32 @@ exports.processNext = async ({ }; }; +exports.processAll = async ({ + Backbone, + storage, + upgradeMessageSchema, +} = {}) => { + if (!isObject(Backbone)) { + throw new TypeError('"Backbone" is required'); + } + + if (!isObject(storage)) { + throw new TypeError('"storage" is required'); + } + + if (!isFunction(upgradeMessageSchema)) { + throw new TypeError('"upgradeMessageSchema" is required'); + } + + const lastIndex = null; + const unprocessedMessages = + await _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex({ + Backbone, + count: 10, + lastIndex, + }); +}; + const _saveMessage = ({ BackboneMessage } = {}) => (message) => { const backboneMessage = new BackboneMessage(message); return deferredToPromise(backboneMessage.save()); @@ -89,3 +118,33 @@ const _fetchMessagesRequiringSchemaUpgrade = resolve(messages); })); }; + +const MAX_MESSAGE_KEY = 'ffffffff-ffff-ffff-ffff-ffffffffffff'; +const _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex = + async ({ Backbone, count, lastIndex } = {}) => { + if (!isObject(Backbone)) { + throw new TypeError('"Backbone" is required'); + } + + if (!isNumber(count)) { + throw new TypeError('"count" is required'); + } + + if (lastIndex && !isString(lastIndex)) { + throw new TypeError('"lastIndex" must be a string'); + } + + const storeName = 'messages'; + const collection = + Migrations0DatabaseWithAttachmentData.createCollection({ Backbone, storeName }); + + const range = lastIndex ? [lastIndex, MAX_MESSAGE_KEY] : null; + await deferredToPromise(collection.fetch({ + limit: count, + range, + })); + + const models = collection.models || []; + const messages = models.map(model => model.toJSON()); + return messages; + }; From 178a3cc2624b3f5fd45bb3d448f500a7fb3a9ece Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Mon, 26 Mar 2018 19:11:21 -0400 Subject: [PATCH 14/56] Reduce work for verifying transaction completion --- js/modules/migrations/run_migrations.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/modules/migrations/run_migrations.js b/js/modules/migrations/run_migrations.js index 7296debb6..992895373 100644 --- a/js/modules/migrations/run_migrations.js +++ b/js/modules/migrations/run_migrations.js @@ -43,6 +43,6 @@ exports.runMigrations = async ({ Backbone, database } = {}) => { storeName: 'items', }))(); - await deferredToPromise(migrationCollection.fetch()); + await deferredToPromise(migrationCollection.fetch({ limit: 1 })); await closeDatabase(database.id); }; From b8a0bc34235e44ac14a5f0f0db8074fece5ccb6a Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Mon, 26 Mar 2018 19:12:38 -0400 Subject: [PATCH 15/56] Run attachment to disk migration on startup --- js/background.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/js/background.js b/js/background.js index 18a4c6d4e..dce1de980 100644 --- a/js/background.js +++ b/js/background.js @@ -88,6 +88,9 @@ console.log('Migrate database with attachments'); await Migrations0DatabaseWithAttachmentData.run({ Backbone }); + console.log('Migrate attachments to disk'); + await MessageDataMigrator.processAll({ Backbone, storage, upgradeMessageSchema }); + console.log('Migrate database without attachments'); await Migrations1DatabaseWithoutAttachmentData.run({ Backbone, From 070235b59b738a1fb86fda2600fc32352060de3b Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Tue, 27 Mar 2018 11:51:21 -0400 Subject: [PATCH 16/56] Implement `MessageDataMigrator.processAll` Upgrades schema of all messags upon startup. --- js/modules/messages_data_migrator.js | 223 ++++++++++++++++++++++++--- 1 file changed, 198 insertions(+), 25 deletions(-) diff --git a/js/modules/messages_data_migrator.js b/js/modules/messages_data_migrator.js index e3ce66c89..1ba211e0f 100644 --- a/js/modules/messages_data_migrator.js +++ b/js/modules/messages_data_migrator.js @@ -1,12 +1,20 @@ +/* eslint-env browser */ + 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 Message = require('./types/message'); const { deferredToPromise } = require('./deferred_to_promise'); -const Migrations0DatabaseWithAttachmentData = - require('./migrations/migrations_0_database_with_attachment_data'); + + +const DATABASE_NAME = 'signal'; +// Last version with attachment data stored in database: +const EXPECTED_DATABASE_VERSION = 17; +const MESSAGES_STORE_NAME = 'messages'; +const ITEMS_STORE_NAME = 'items'; exports.processNext = async ({ BackboneMessage, @@ -44,7 +52,7 @@ exports.processNext = async ({ const upgradeDuration = Date.now() - startUpgradeTime; const startSaveTime = Date.now(); - const saveMessage = _saveMessage({ BackboneMessage }); + const saveMessage = _saveMessageBackbone({ BackboneMessage }); await Promise.all(upgradedMessages.map(saveMessage)); const saveDuration = Date.now() - startSaveTime; @@ -78,20 +86,91 @@ exports.processAll = async ({ throw new TypeError('"upgradeMessageSchema" is required'); } - const lastIndex = null; - const unprocessedMessages = - await _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex({ - Backbone, - count: 10, - lastIndex, + const connection = await openDatabase(DATABASE_NAME, EXPECTED_DATABASE_VERSION); + const isComplete = await isMigrationComplete(connection); + console.log('Is attachment migration complete?', isComplete); + if (isComplete) { + return; + } + + const migrationStartTime = Date.now(); + let unprocessedMessages = []; + do { + // eslint-disable-next-line no-await-in-loop + const lastProcessedIndex = (await getLastProcessedIndex(connection)) || null; + + const fetchUnprocessedMessagesStartTime = Date.now(); + unprocessedMessages = + // eslint-disable-next-line no-await-in-loop + await _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex({ + connection, + count: 10, + lastIndex: lastProcessedIndex, + }); + const fetchDuration = Date.now() - fetchUnprocessedMessagesStartTime; + const numUnprocessedMessages = unprocessedMessages.length; + + const upgradeStartTime = Date.now(); + const upgradedMessages = + // eslint-disable-next-line no-await-in-loop + await Promise.all(unprocessedMessages.map(upgradeMessageSchema)); + const upgradeDuration = Date.now() - upgradeStartTime; + + const saveMessagesStartTime = Date.now(); + const transaction = connection.transaction(MESSAGES_STORE_NAME, 'readwrite'); + const transactionCompletion = 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 + await transactionCompletion; + const saveDuration = Date.now() - saveMessagesStartTime; + + // TODO: Confirm transaction is complete + + const lastMessage = last(upgradedMessages); + const newLastProcessedIndex = lastMessage ? lastMessage.id : null; + if (newLastProcessedIndex) { + // eslint-disable-next-line no-await-in-loop + await setLastProcessedIndex(connection, newLastProcessedIndex); + } + + console.log('Upgrade message schema on startup:', { + lastProcessedIndex, + numUnprocessedMessages, + fetchDuration, + saveDuration, + upgradeDuration, + newLastProcessedIndex, }); + } while (unprocessedMessages.length > 0); + + await markMigrationComplete(connection); + connection.close(); + + const totalDuration = Date.now() - migrationStartTime; + console.log('Attachment migration complete:', { totalDuration }); }; -const _saveMessage = ({ BackboneMessage } = {}) => (message) => { +const _saveMessageBackbone = ({ BackboneMessage } = {}) => (message) => { const backboneMessage = new BackboneMessage(message); return deferredToPromise(backboneMessage.save()); }; +const _saveMessage = ({ transaction } = {}) => (message) => { + if (!isObject(transaction)) { + throw new TypeError('"transaction" is required'); + } + + const messagesStore = transaction.objectStore(MESSAGES_STORE_NAME); + const request = messagesStore.put(message, message.id); + return new Promise((resolve, reject) => { + request.onsuccess = () => + resolve(); + request.onerror = event => + reject(event.target.error); + }); +}; + const _fetchMessagesRequiringSchemaUpgrade = async ({ BackboneMessageCollection, count } = {}) => { if (!isFunction(BackboneMessageCollection)) { @@ -119,11 +198,10 @@ const _fetchMessagesRequiringSchemaUpgrade = })); }; -const MAX_MESSAGE_KEY = 'ffffffff-ffff-ffff-ffff-ffffffffffff'; const _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex = - async ({ Backbone, count, lastIndex } = {}) => { - if (!isObject(Backbone)) { - throw new TypeError('"Backbone" is required'); + ({ connection, count, lastIndex } = {}) => { + if (!isObject(connection)) { + throw new TypeError('"connection" is required'); } if (!isNumber(count)) { @@ -134,17 +212,112 @@ const _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex = throw new TypeError('"lastIndex" must be a string'); } - const storeName = 'messages'; - const collection = - Migrations0DatabaseWithAttachmentData.createCollection({ Backbone, storeName }); + const hasLastIndex = Boolean(lastIndex); - const range = lastIndex ? [lastIndex, MAX_MESSAGE_KEY] : null; - await deferredToPromise(collection.fetch({ - limit: count, - range, - })); + const transaction = connection.transaction(MESSAGES_STORE_NAME, 'readonly'); + const messagesStore = transaction.objectStore(MESSAGES_STORE_NAME); - const models = collection.models || []; - const messages = models.map(model => model.toJSON()); - return messages; + const excludeLowerBound = true; + const query = hasLastIndex + ? IDBKeyRange.lowerBound(lastIndex, excludeLowerBound) + : undefined; + const request = messagesStore.getAll(query, count); + return new Promise((resolve, reject) => { + request.onsuccess = event => + resolve(event.target.result); + request.onerror = event => + 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); + }); +}; + +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(value, key); + return new Promise((resolve, reject) => { + request.onerror = event => + reject(event.target.error); + + request.onsuccess = () => + resolve(); + }); +}; + +const completeTransaction = transaction => + new Promise((resolve, reject) => { + // eslint-disable-next-line no-param-reassign + transaction.onabort = event => + reject(event.target.error); + // eslint-disable-next-line no-param-reassign + transaction.onerror = event => + reject(event.target.error); + // eslint-disable-next-line no-param-reassign + transaction.oncomplete = () => + resolve(); + }); From 7de7fcf56147a2a125b009984e2b399b8c3cacde Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Tue, 27 Mar 2018 12:13:25 -0400 Subject: [PATCH 17/56] Avoid `no-param-reassign` violation --- js/modules/messages_data_migrator.js | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/js/modules/messages_data_migrator.js b/js/modules/messages_data_migrator.js index 1ba211e0f..7b07ad03c 100644 --- a/js/modules/messages_data_migrator.js +++ b/js/modules/messages_data_migrator.js @@ -311,13 +311,7 @@ const setItem = (connection, key, value) => { const completeTransaction = transaction => new Promise((resolve, reject) => { - // eslint-disable-next-line no-param-reassign - transaction.onabort = event => - reject(event.target.error); - // eslint-disable-next-line no-param-reassign - transaction.onerror = event => - reject(event.target.error); - // eslint-disable-next-line no-param-reassign - transaction.oncomplete = () => - resolve(); + transaction.addEventListener('abort', event => reject(event.target.error)); + transaction.addEventListener('error', event => reject(event.target.error)); + transaction.addEventListener('complete', () => resolve()); }); From 85788d3c4a1de1c866314ede9952258c891e9700 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Tue, 27 Mar 2018 12:13:46 -0400 Subject: [PATCH 18/56] Match `items` storage format to Backbone adapter --- js/modules/messages_data_migrator.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/js/modules/messages_data_migrator.js b/js/modules/messages_data_migrator.js index 7b07ad03c..047243b71 100644 --- a/js/modules/messages_data_migrator.js +++ b/js/modules/messages_data_migrator.js @@ -97,7 +97,7 @@ exports.processAll = async ({ let unprocessedMessages = []; do { // eslint-disable-next-line no-await-in-loop - const lastProcessedIndex = (await getLastProcessedIndex(connection)) || null; + const lastProcessedIndex = await getLastProcessedIndex(connection); const fetchUnprocessedMessagesStartTime = Date.now(); unprocessedMessages = @@ -284,7 +284,7 @@ const getItem = (connection, key) => { reject(event.target.error); request.onsuccess = event => - resolve(event.target.result); + resolve(event.target.result ? event.target.result.value : null); }); }; @@ -299,7 +299,7 @@ const setItem = (connection, key, value) => { const transaction = connection.transaction(ITEMS_STORE_NAME, 'readwrite'); const itemsStore = transaction.objectStore(ITEMS_STORE_NAME); - const request = itemsStore.put(value, key); + const request = itemsStore.put({id: key, value}, key); return new Promise((resolve, reject) => { request.onerror = event => reject(event.target.error); From 3c57dbfb567ba084f3faf8312a04b7257aa2aa70 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Tue, 27 Mar 2018 12:15:14 -0400 Subject: [PATCH 19/56] Extract `NUM_MESSAGES_PER_BATCH` --- js/modules/messages_data_migrator.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/js/modules/messages_data_migrator.js b/js/modules/messages_data_migrator.js index 047243b71..50508c3d3 100644 --- a/js/modules/messages_data_migrator.js +++ b/js/modules/messages_data_migrator.js @@ -15,6 +15,7 @@ const DATABASE_NAME = 'signal'; const EXPECTED_DATABASE_VERSION = 17; const MESSAGES_STORE_NAME = 'messages'; const ITEMS_STORE_NAME = 'items'; +const NUM_MESSAGES_PER_BATCH = 50; exports.processNext = async ({ BackboneMessage, @@ -104,7 +105,7 @@ exports.processAll = async ({ // eslint-disable-next-line no-await-in-loop await _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex({ connection, - count: 10, + count: NUM_MESSAGES_PER_BATCH, lastIndex: lastProcessedIndex, }); const fetchDuration = Date.now() - fetchUnprocessedMessagesStartTime; From 3e2d575506af019773adf67546a2a1c8fcabcad0 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Tue, 27 Mar 2018 12:19:40 -0400 Subject: [PATCH 20/56] Document `MessageDataMigrator` module design --- js/modules/messages_data_migrator.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/js/modules/messages_data_migrator.js b/js/modules/messages_data_migrator.js index 50508c3d3..8142922be 100644 --- a/js/modules/messages_data_migrator.js +++ b/js/modules/messages_data_migrator.js @@ -1,5 +1,12 @@ /* eslint-env browser */ +// Module to upgrade the schema of messages, e.g. migrate attachments to disk. +// `processAll` is meant to be run after the initial database migrations +// (12 – 17) and purposely doesn’t rely on our Backbone IndexedDB adapter to +// prevent subsequent migrations to run (18+) but rather uses direct IndexedDB +// access. This includes avoiding usage of `storage` module which uses Backbone +// under the hood. + const isFunction = require('lodash/isFunction'); const isNumber = require('lodash/isNumber'); const isObject = require('lodash/isObject'); From 8966e802848cd1149361b0829946c8ea1c052e18 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Tue, 27 Mar 2018 12:19:55 -0400 Subject: [PATCH 21/56] Improve identifier names --- js/modules/messages_data_migrator.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/js/modules/messages_data_migrator.js b/js/modules/messages_data_migrator.js index 8142922be..9587d0300 100644 --- a/js/modules/messages_data_migrator.js +++ b/js/modules/messages_data_migrator.js @@ -49,20 +49,20 @@ exports.processNext = async ({ const startTime = Date.now(); - const startFetchTime = Date.now(); + const fetchStartTime = Date.now(); const messagesRequiringSchemaUpgrade = await _fetchMessagesRequiringSchemaUpgrade({ BackboneMessageCollection, count }); - const fetchDuration = Date.now() - startFetchTime; + const fetchDuration = Date.now() - fetchStartTime; - const startUpgradeTime = Date.now(); + const upgradeStartTime = Date.now(); const upgradedMessages = await Promise.all(messagesRequiringSchemaUpgrade.map(upgradeMessageSchema)); - const upgradeDuration = Date.now() - startUpgradeTime; + const upgradeDuration = Date.now() - upgradeStartTime; - const startSaveTime = Date.now(); + const saveStartTime = Date.now(); const saveMessage = _saveMessageBackbone({ BackboneMessage }); await Promise.all(upgradedMessages.map(saveMessage)); - const saveDuration = Date.now() - startSaveTime; + const saveDuration = Date.now() - saveStartTime; const totalDuration = Date.now() - startTime; const numProcessed = messagesRequiringSchemaUpgrade.length; From d5d0eabdfda2f1945f643606c9467fbbca87f259 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Tue, 27 Mar 2018 12:20:07 -0400 Subject: [PATCH 22/56] Remove usage of `storage` module --- js/background.js | 2 +- js/modules/messages_data_migrator.js | 10 +--------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/js/background.js b/js/background.js index dce1de980..a81311790 100644 --- a/js/background.js +++ b/js/background.js @@ -89,7 +89,7 @@ await Migrations0DatabaseWithAttachmentData.run({ Backbone }); console.log('Migrate attachments to disk'); - await MessageDataMigrator.processAll({ Backbone, storage, upgradeMessageSchema }); + await MessageDataMigrator.processAll({ Backbone, upgradeMessageSchema }); console.log('Migrate database without attachments'); await Migrations1DatabaseWithoutAttachmentData.run({ diff --git a/js/modules/messages_data_migrator.js b/js/modules/messages_data_migrator.js index 9587d0300..165905bc2 100644 --- a/js/modules/messages_data_migrator.js +++ b/js/modules/messages_data_migrator.js @@ -77,19 +77,11 @@ exports.processNext = async ({ }; }; -exports.processAll = async ({ - Backbone, - storage, - upgradeMessageSchema, -} = {}) => { +exports.processAll = async ({ Backbone, upgradeMessageSchema } = {}) => { if (!isObject(Backbone)) { throw new TypeError('"Backbone" is required'); } - if (!isObject(storage)) { - throw new TypeError('"storage" is required'); - } - if (!isFunction(upgradeMessageSchema)) { throw new TypeError('"upgradeMessageSchema" is required'); } From 85490fbc98c88fbb477b3809f63f88e689c6f1ac Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Wed, 28 Mar 2018 10:10:09 -0400 Subject: [PATCH 23/56] Disable JSHint for `background.js` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It doesn’t recognize `async` and I couldn’t figure out how to ignore a top-level `async` without cascading errors. --- Gruntfile.js | 1 + js/background.js | 7 +------ 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 78cba0c41..ed99860fe 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -97,6 +97,7 @@ module.exports = function(grunt) { files: [ 'Gruntfile.js', 'js/**/*.js', + '!js/background.js', '!js/jquery.js', '!js/libtextsecure.js', '!js/WebAudioRecorderMp3.js', diff --git a/js/background.js b/js/background.js index a81311790..1e09efa44 100644 --- a/js/background.js +++ b/js/background.js @@ -11,8 +11,7 @@ /* global Whisper: false */ /* global wrapDeferred: false */ - -;(/* jshint ignore:start */ async /* jshint ignore:end */function() { +;(async function() { 'use strict'; const { IdleDetector, MessageDataMigrator } = Signal.Workflow; @@ -81,7 +80,6 @@ }; /* eslint-enable */ - /* jshint ignore:start */ const cancelInitializationMessage = Views.Initialization.setMessage(); console.log('Start IndexedDB migrations'); @@ -115,7 +113,6 @@ idleDetector.stop(); } }); - /* jshint ignore:end */ /* eslint-disable */ // We need this 'first' check because we don't want to start the app up any other time @@ -575,7 +572,6 @@ } /* eslint-enable */ - /* jshint ignore:start */ // Descriptors const getGroupDescriptor = group => ({ @@ -684,7 +680,6 @@ getMessageDescriptor: getDescriptorForSent, createMessage: createSentMessage, }); - /* jshint ignore:end */ /* eslint-disable */ function isMessageDuplicate(message) { From 1df6dc8378a26fd85a03653595cf91eca493a45b Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Wed, 28 Mar 2018 10:10:45 -0400 Subject: [PATCH 24/56] Abort processing if there are no more messages --- js/modules/messages_data_migrator.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/js/modules/messages_data_migrator.js b/js/modules/messages_data_migrator.js index 165905bc2..c00c27b6c 100644 --- a/js/modules/messages_data_migrator.js +++ b/js/modules/messages_data_migrator.js @@ -110,6 +110,10 @@ exports.processAll = async ({ Backbone, upgradeMessageSchema } = {}) => { const fetchDuration = Date.now() - fetchUnprocessedMessagesStartTime; const numUnprocessedMessages = unprocessedMessages.length; + if (numUnprocessedMessages === 0) { + break; + } + const upgradeStartTime = Date.now(); const upgradedMessages = // eslint-disable-next-line no-await-in-loop From eca930770c0287b3094a318dcb4943f339cfd7fa Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Wed, 28 Mar 2018 10:23:36 -0400 Subject: [PATCH 25/56] Remove hard-coded database connection settings --- js/background.js | 8 ++++- js/modules/messages_data_migrator.js | 29 ++++++++++++------- ...rations_0_database_with_attachment_data.js | 6 ++++ 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/js/background.js b/js/background.js index 1e09efa44..e287db1c0 100644 --- a/js/background.js +++ b/js/background.js @@ -87,7 +87,13 @@ await Migrations0DatabaseWithAttachmentData.run({ Backbone }); console.log('Migrate attachments to disk'); - await MessageDataMigrator.processAll({ Backbone, upgradeMessageSchema }); + const database = Migrations0DatabaseWithAttachmentData.getDatabase(); + await MessageDataMigrator.processAll({ + Backbone, + databaseName: database.name, + databaseVersion: database.version, + upgradeMessageSchema, + }); console.log('Migrate database without attachments'); await Migrations1DatabaseWithoutAttachmentData.run({ diff --git a/js/modules/messages_data_migrator.js b/js/modules/messages_data_migrator.js index c00c27b6c..c240e7f2d 100644 --- a/js/modules/messages_data_migrator.js +++ b/js/modules/messages_data_migrator.js @@ -1,11 +1,10 @@ /* eslint-env browser */ // Module to upgrade the schema of messages, e.g. migrate attachments to disk. -// `processAll` is meant to be run after the initial database migrations -// (12 – 17) and purposely doesn’t rely on our Backbone IndexedDB adapter to -// prevent subsequent migrations to run (18+) but rather uses direct IndexedDB -// access. This includes avoiding usage of `storage` module which uses Backbone -// under the hood. +// `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. const isFunction = require('lodash/isFunction'); const isNumber = require('lodash/isNumber'); @@ -17,9 +16,6 @@ const Message = require('./types/message'); const { deferredToPromise } = require('./deferred_to_promise'); -const DATABASE_NAME = 'signal'; -// Last version with attachment data stored in database: -const EXPECTED_DATABASE_VERSION = 17; const MESSAGES_STORE_NAME = 'messages'; const ITEMS_STORE_NAME = 'items'; const NUM_MESSAGES_PER_BATCH = 50; @@ -77,16 +73,29 @@ exports.processNext = async ({ }; }; -exports.processAll = async ({ Backbone, upgradeMessageSchema } = {}) => { +exports.processAll = async ({ + Backbone, + databaseName, + databaseVersion, + upgradeMessageSchema, + } = {}) => { if (!isObject(Backbone)) { throw new TypeError('"Backbone" is required'); } + if (!isString(databaseName)) { + throw new TypeError('"databaseName" must be a string'); + } + + if (!isNumber(databaseVersion)) { + throw new TypeError('"databaseVersion" must be a number'); + } + if (!isFunction(upgradeMessageSchema)) { throw new TypeError('"upgradeMessageSchema" is required'); } - const connection = await openDatabase(DATABASE_NAME, EXPECTED_DATABASE_VERSION); + const connection = await openDatabase(databaseName, databaseVersion); const isComplete = await isMigrationComplete(connection); console.log('Is attachment migration complete?', isComplete); if (isComplete) { diff --git a/js/modules/migrations/migrations_0_database_with_attachment_data.js b/js/modules/migrations/migrations_0_database_with_attachment_data.js index e00f71870..02cd097ff 100644 --- a/js/modules/migrations/migrations_0_database_with_attachment_data.js +++ b/js/modules/migrations/migrations_0_database_with_attachment_data.js @@ -1,6 +1,7 @@ const isFunction = require('lodash/isFunction'); const isObject = require('lodash/isObject'); const isString = require('lodash/isString'); +const last = require('lodash/last'); const { runMigrations } = require('./run_migrations'); @@ -169,3 +170,8 @@ exports.createCollection = ({ Backbone, storeName }) => { return collection; }; + +exports.getDatabase = () => ({ + name: database.id, + version: last(migrations).version, +}); From f7f24b5822ab039faba6c57edee9833496a0b261 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Wed, 28 Mar 2018 10:24:07 -0400 Subject: [PATCH 26/56] Log total number of processed messages --- js/modules/messages_data_migrator.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/js/modules/messages_data_migrator.js b/js/modules/messages_data_migrator.js index c240e7f2d..2fa04573f 100644 --- a/js/modules/messages_data_migrator.js +++ b/js/modules/messages_data_migrator.js @@ -104,6 +104,7 @@ exports.processAll = async ({ const migrationStartTime = Date.now(); let unprocessedMessages = []; + let totalMessagesProcessed = 0; do { // eslint-disable-next-line no-await-in-loop const lastProcessedIndex = await getLastProcessedIndex(connection); @@ -147,9 +148,11 @@ exports.processAll = async ({ await setLastProcessedIndex(connection, newLastProcessedIndex); } - console.log('Upgrade message schema on startup:', { + totalMessagesProcessed += numUnprocessedMessages; + console.log('Upgrade message schema:', { lastProcessedIndex, numUnprocessedMessages, + numCumulativeMessagesProcessed: totalMessagesProcessed, fetchDuration, saveDuration, upgradeDuration, @@ -161,7 +164,10 @@ exports.processAll = async ({ connection.close(); const totalDuration = Date.now() - migrationStartTime; - console.log('Attachment migration complete:', { totalDuration }); + console.log('Attachment migration complete:', { + totalDuration, + totalMessagesProcessed, + }); }; const _saveMessageBackbone = ({ BackboneMessage } = {}) => (message) => { From ce5b450fdbab4c0d6050099d33a6ac91755527bd Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Wed, 28 Mar 2018 10:24:49 -0400 Subject: [PATCH 27/56] Log `targetSchemaVersion` --- js/modules/messages_data_migrator.js | 1 + 1 file changed, 1 insertion(+) diff --git a/js/modules/messages_data_migrator.js b/js/modules/messages_data_migrator.js index 2fa04573f..70268ce06 100644 --- a/js/modules/messages_data_migrator.js +++ b/js/modules/messages_data_migrator.js @@ -157,6 +157,7 @@ exports.processAll = async ({ saveDuration, upgradeDuration, newLastProcessedIndex, + targetSchemaVersion: Message.CURRENT_SCHEMA_VERSION, }); } while (unprocessedMessages.length > 0); From 3720c3f3bbeeb55eec5f2885c598e74862c4c866 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Wed, 28 Mar 2018 10:25:22 -0400 Subject: [PATCH 28/56] Improve log message --- js/modules/messages_data_migrator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/modules/messages_data_migrator.js b/js/modules/messages_data_migrator.js index 70268ce06..86bebcf36 100644 --- a/js/modules/messages_data_migrator.js +++ b/js/modules/messages_data_migrator.js @@ -97,7 +97,7 @@ exports.processAll = async ({ const connection = await openDatabase(databaseName, databaseVersion); const isComplete = await isMigrationComplete(connection); - console.log('Is attachment migration complete?', isComplete); + console.log('Attachment migration status:', isComplete ? 'complete' : 'incomplete'); if (isComplete) { return; } From 4ff8bc3357254cc491fafe3e687d046750cb222c Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Wed, 28 Mar 2018 10:37:11 -0400 Subject: [PATCH 29/56] Use `camelCase` for non-constructors --- js/background.js | 2 +- .../migrations_1_database_without_attachment_data.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/js/background.js b/js/background.js index e287db1c0..991414f06 100644 --- a/js/background.js +++ b/js/background.js @@ -98,7 +98,7 @@ console.log('Migrate database without attachments'); await Migrations1DatabaseWithoutAttachmentData.run({ Backbone, - Database: Whisper.Database, + database: Whisper.Database, }); console.log('Storage fetch'); diff --git a/js/modules/migrations/migrations_1_database_without_attachment_data.js b/js/modules/migrations/migrations_1_database_without_attachment_data.js index 2b7c010c4..b10106ef2 100644 --- a/js/modules/migrations/migrations_1_database_without_attachment_data.js +++ b/js/modules/migrations/migrations_1_database_without_attachment_data.js @@ -12,5 +12,4 @@ exports.migrations = [ }, ]; -exports.run = ({ Backbone, Database } = {}) => - runMigrations({ Backbone, database: Database }); +exports.run = runMigrations; From f50e9ae364023481461869839c09498d71d55f17 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Wed, 28 Mar 2018 10:37:21 -0400 Subject: [PATCH 30/56] Log closing connection of database --- js/modules/messages_data_migrator.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/js/modules/messages_data_migrator.js b/js/modules/messages_data_migrator.js index 86bebcf36..f5d3301df 100644 --- a/js/modules/messages_data_migrator.js +++ b/js/modules/messages_data_migrator.js @@ -162,6 +162,8 @@ exports.processAll = async ({ } while (unprocessedMessages.length > 0); await markMigrationComplete(connection); + + console.log('Close database connection'); connection.close(); const totalDuration = Date.now() - migrationStartTime; From 5bea894abda85ffe4b3ca809a6c77ad0e82e5619 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Wed, 28 Mar 2018 10:38:05 -0400 Subject: [PATCH 31/56] Close database connection via Backbone IDB adapter --- js/modules/migrations/run_migrations.js | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/js/modules/migrations/run_migrations.js b/js/modules/migrations/run_migrations.js index 992895373..af1c70af8 100644 --- a/js/modules/migrations/run_migrations.js +++ b/js/modules/migrations/run_migrations.js @@ -7,25 +7,8 @@ const isString = require('lodash/isString'); const { deferredToPromise } = require('../deferred_to_promise'); -const closeDatabase = name => - new Promise((resolve, reject) => { - const request = window.indexedDB.open(name); - request.onblocked = () => { - reject(new Error(`Database '${name}' blocked`)); - }; - request.onupgradeneeded = (event) => { - reject(new Error('Unexpected database upgraded needed:' + - ` oldVersion: ${event.oldVersion}, newVersion: ${event.newVersion}`)); - }; - request.onerror = (event) => { - reject(event.target.error); - }; - request.onsuccess = (event) => { - const connection = event.target.result; - connection.close(); - resolve(); - }; - }); +const closeDatabase = ({ Backbone } = {}) => + deferredToPromise(Backbone.sync('closeall')) exports.runMigrations = async ({ Backbone, database } = {}) => { if (!isObject(Backbone) || !isObject(Backbone.Collection) || @@ -44,5 +27,6 @@ exports.runMigrations = async ({ Backbone, database } = {}) => { }))(); await deferredToPromise(migrationCollection.fetch({ limit: 1 })); - await closeDatabase(database.id); + console.log('Close database connection'); + await closeDatabase({ Backbone }); }; From 016432826bec416da6cea886ff53f6eddf94918c Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Wed, 28 Mar 2018 10:54:01 -0400 Subject: [PATCH 32/56] 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 000000000..c287d5109 --- /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 f5d3301df..e554d7208 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 af1c70af8..948d1802d 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 000000000..9d1cc018e --- /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(); + }); +}; From 6aea36240d52ae412ab2e396f3570603d03550dd Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Wed, 28 Mar 2018 13:00:52 -0400 Subject: [PATCH 33/56] Rename `closeDatabase` to `closeDatabaseConnection` --- js/modules/migrations/run_migrations.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/modules/migrations/run_migrations.js b/js/modules/migrations/run_migrations.js index 948d1802d..ac130d847 100644 --- a/js/modules/migrations/run_migrations.js +++ b/js/modules/migrations/run_migrations.js @@ -7,7 +7,7 @@ const isString = require('lodash/isString'); const { deferredToPromise } = require('../deferred_to_promise'); -const closeDatabase = ({ Backbone } = {}) => +const closeDatabaseConnection = ({ Backbone } = {}) => deferredToPromise(Backbone.sync('closeall')); exports.runMigrations = async ({ Backbone, database } = {}) => { @@ -28,5 +28,5 @@ exports.runMigrations = async ({ Backbone, database } = {}) => { await deferredToPromise(migrationCollection.fetch({ limit: 1 })); console.log('Close database connection'); - await closeDatabase({ Backbone }); + await closeDatabaseConnection({ Backbone }); }; From c5c94bc3ab0deb8a6848d93943287f10f4bd1948 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Wed, 28 Mar 2018 13:05:49 -0400 Subject: [PATCH 34/56] Extract `getMigrationVersions` --- js/modules/migrations/run_migrations.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/js/modules/migrations/run_migrations.js b/js/modules/migrations/run_migrations.js index ac130d847..84635542a 100644 --- a/js/modules/migrations/run_migrations.js +++ b/js/modules/migrations/run_migrations.js @@ -3,6 +3,8 @@ const isFunction = require('lodash/isFunction'); const isObject = require('lodash/isObject'); const isString = require('lodash/isString'); +const head = require('lodash/head'); +const last = require('lodash/last'); const { deferredToPromise } = require('../deferred_to_promise'); @@ -30,3 +32,17 @@ exports.runMigrations = async ({ Backbone, database } = {}) => { console.log('Close database connection'); await closeDatabaseConnection({ Backbone }); }; + +const getMigrationVersions = (database) => { + if (!isObject(database) || !Array.isArray(database.migrations)) { + throw new TypeError('"database" is required'); + } + + const firstMigration = head(database.migrations); + const lastMigration = last(database.migrations); + + const firstVersion = firstMigration ? parseInt(firstMigration.version, 10) : null; + const lastVersion = lastMigration ? parseInt(lastMigration.version, 10) : null; + + return { firstVersion, lastVersion }; +}; From 417511ffd2785ef7bcc43bd6361a98c7af1d0a88 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Wed, 28 Mar 2018 13:09:09 -0400 Subject: [PATCH 35/56] Add `database.getVersion` --- js/modules/database.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/js/modules/database.js b/js/modules/database.js index c287d5109..f89bac595 100644 --- a/js/modules/database.js +++ b/js/modules/database.js @@ -31,3 +31,9 @@ exports.completeTransaction = transaction => transaction.addEventListener('complete', () => resolve()); }); +exports.getVersion = async (name) => { + const connection = await exports.open(name); + const { version } = connection; + connection.close(); + return version; +}; From 921c3dba7ca18ad4ba284d6f86bee5ec343c54fa Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Wed, 28 Mar 2018 13:10:54 -0400 Subject: [PATCH 36/56] Skip migrations that have already been applied --- js/modules/migrations/run_migrations.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/js/modules/migrations/run_migrations.js b/js/modules/migrations/run_migrations.js index 84635542a..b66057c19 100644 --- a/js/modules/migrations/run_migrations.js +++ b/js/modules/migrations/run_migrations.js @@ -6,6 +6,7 @@ const isString = require('lodash/isString'); const head = require('lodash/head'); const last = require('lodash/last'); +const db = require('../database'); const { deferredToPromise } = require('../deferred_to_promise'); @@ -23,6 +24,25 @@ exports.runMigrations = async ({ Backbone, database } = {}) => { throw new TypeError('"database" is required'); } + const { + firstVersion: firstMigrationVersion, + lastVersion: lastMigrationVersion, + } = getMigrationVersions(database); + + const databaseVersion = await db.getVersion(database.id); + const isAlreadyUpgraded = databaseVersion >= lastMigrationVersion; + + console.log('Database state', { + firstMigrationVersion, + lastMigrationVersion, + databaseVersion, + isAlreadyUpgraded, + }); + + if (isAlreadyUpgraded) { + return; + } + const migrationCollection = new (Backbone.Collection.extend({ database, storeName: 'items', From efe3cd67fcc425101270661b7dd6033c3aa12339 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Wed, 28 Mar 2018 14:45:07 -0400 Subject: [PATCH 37/56] Allow attachment migration run on higher database version --- js/background.js | 2 +- js/modules/messages_data_migrator.js | 20 ++++++++++++++++---- js/modules/migrations/run_migrations.js | 2 +- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/js/background.js b/js/background.js index 991414f06..3516cb0e7 100644 --- a/js/background.js +++ b/js/background.js @@ -91,7 +91,7 @@ await MessageDataMigrator.processAll({ Backbone, databaseName: database.name, - databaseVersion: database.version, + minDatabaseVersion: database.version, upgradeMessageSchema, }); diff --git a/js/modules/messages_data_migrator.js b/js/modules/messages_data_migrator.js index e554d7208..a331aac80 100644 --- a/js/modules/messages_data_migrator.js +++ b/js/modules/messages_data_migrator.js @@ -77,7 +77,7 @@ exports.processNext = async ({ exports.processAll = async ({ Backbone, databaseName, - databaseVersion, + minDatabaseVersion, upgradeMessageSchema, } = {}) => { if (!isObject(Backbone)) { @@ -88,15 +88,27 @@ exports.processAll = async ({ throw new TypeError('"databaseName" must be a string'); } - if (!isNumber(databaseVersion)) { - throw new TypeError('"databaseVersion" must be a number'); + if (!isNumber(minDatabaseVersion)) { + throw new TypeError('"minDatabaseVersion" must be a number'); } if (!isFunction(upgradeMessageSchema)) { throw new TypeError('"upgradeMessageSchema" is required'); } - const connection = await database.open(databaseName, databaseVersion); + const connection = await database.open(databaseName); + const databaseVersion = connection.version; + const isValidDatabaseVersion = databaseVersion >= minDatabaseVersion; + console.log('Database status', { + databaseVersion, + isValidDatabaseVersion, + minDatabaseVersion, + }); + if (!isValidDatabaseVersion) { + throw new Error(`Expected database version (${databaseVersion})` + + ` to be at least ${minDatabaseVersion}`); + } + const isComplete = await settings.isAttachmentMigrationComplete(connection); console.log('Attachment migration status:', isComplete ? 'complete' : 'incomplete'); if (isComplete) { diff --git a/js/modules/migrations/run_migrations.js b/js/modules/migrations/run_migrations.js index b66057c19..ce3e3cee1 100644 --- a/js/modules/migrations/run_migrations.js +++ b/js/modules/migrations/run_migrations.js @@ -32,7 +32,7 @@ exports.runMigrations = async ({ Backbone, database } = {}) => { const databaseVersion = await db.getVersion(database.id); const isAlreadyUpgraded = databaseVersion >= lastMigrationVersion; - console.log('Database state', { + console.log('Database status', { firstMigrationVersion, lastMigrationVersion, databaseVersion, From 5910f84af4d0ca8752f4e7652756f518adc82985 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Wed, 28 Mar 2018 16:05:36 -0400 Subject: [PATCH 38/56] Remove outdated documentation --- js/modules/types/attachment.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/js/modules/types/attachment.js b/js/modules/types/attachment.js index d37a95454..07e8c825d 100644 --- a/js/modules/types/attachment.js +++ b/js/modules/types/attachment.js @@ -17,7 +17,6 @@ const { migrateDataToFileSystem } = require('./attachment/migrate_data_to_file_s // key: ArrayBuffer // size: integer // thumbnail: ArrayBuffer -// schemaVersion: integer // } // // Outgoing message attachment fields @@ -26,7 +25,6 @@ const { migrateDataToFileSystem } = require('./attachment/migrate_data_to_file_s // data: ArrayBuffer // fileName: string // size: integer -// schemaVersion: integer // } // Returns true if `rawAttachment` is a valid attachment based on our current schema. From a18e46281770cca3f662ea1f328082f3a7c19103 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Wed, 28 Mar 2018 17:04:17 -0400 Subject: [PATCH 39/56] Move migrations to `Signal.Migrations` --- js/background.js | 2 +- js/database.js | 2 +- preload.js | 9 ++++----- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/js/background.js b/js/background.js index 3516cb0e7..38d1c683c 100644 --- a/js/background.js +++ b/js/background.js @@ -20,7 +20,7 @@ const { Migrations0DatabaseWithAttachmentData, Migrations1DatabaseWithoutAttachmentData, - } = window.Signal.Database; + } = window.Signal.Migrations; const { Views } = window.Signal; // Implicitly used in `indexeddb-backbonejs-adapter`: diff --git a/js/database.js b/js/database.js index db0688d35..6b1bb9f93 100644 --- a/js/database.js +++ b/js/database.js @@ -6,7 +6,7 @@ (function () { 'use strict'; - const { Migrations1DatabaseWithoutAttachmentData } = window.Signal.Database; + const { Migrations1DatabaseWithoutAttachmentData } = window.Signal.Migrations; window.Whisper = window.Whisper || {}; window.Whisper.Database = window.Whisper.Database || {}; diff --git a/preload.js b/preload.js index 9509f30fe..4ff2e4d41 100644 --- a/preload.js +++ b/preload.js @@ -124,16 +124,15 @@ window.Signal = {}; window.Signal.Backup = require('./js/modules/backup'); window.Signal.Crypto = require('./js/modules/crypto'); - window.Signal.Database = {}; - window.Signal.Database.Migrations0DatabaseWithAttachmentData = - require('./js/modules/migrations/migrations_0_database_with_attachment_data'); - window.Signal.Database.Migrations1DatabaseWithoutAttachmentData = - require('./js/modules/migrations/migrations_1_database_without_attachment_data'); window.Signal.Logs = require('./js/modules/logs'); window.Signal.Migrations = {}; window.Signal.Migrations.loadAttachmentData = Attachment.loadData(readAttachmentData); window.Signal.Migrations.deleteAttachmentData = Attachment.deleteData(deleteAttachmentData); window.Signal.Migrations.upgradeMessageSchema = upgradeMessageSchema; + window.Signal.Migrations.Migrations0DatabaseWithAttachmentData = + require('./js/modules/migrations/migrations_0_database_with_attachment_data'); + window.Signal.Migrations.Migrations1DatabaseWithoutAttachmentData = + require('./js/modules/migrations/migrations_1_database_without_attachment_data'); window.Signal.OS = require('./js/modules/os'); window.Signal.Types = {}; window.Signal.Types.Attachment = Attachment; From 696a144ab72aa032da760adb15690df5af4d1b9e Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Wed, 28 Mar 2018 17:05:21 -0400 Subject: [PATCH 40/56] Add `settings.deleteItem` --- js/modules/settings.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/js/modules/settings.js b/js/modules/settings.js index 9d1cc018e..9cae70735 100644 --- a/js/modules/settings.js +++ b/js/modules/settings.js @@ -61,3 +61,24 @@ const setItem = (connection, key, value) => { resolve(); }); }; + +const deleteItem = (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, 'readwrite'); + const itemsStore = transaction.objectStore(ITEMS_STORE_NAME); + const request = itemsStore.delete(key); + return new Promise((resolve, reject) => { + request.onerror = event => + reject(event.target.error); + + request.onsuccess = () => + resolve(); + }); +}; From 08f73b84207d0016bd062bcfd80413cf5868d68d Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Wed, 28 Mar 2018 17:12:51 -0400 Subject: [PATCH 41/56] Remove last processed index after attachment migration --- js/modules/messages_data_migrator.js | 1 + js/modules/settings.js | 17 ++++++++++------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/js/modules/messages_data_migrator.js b/js/modules/messages_data_migrator.js index a331aac80..24f9d23f0 100644 --- a/js/modules/messages_data_migrator.js +++ b/js/modules/messages_data_migrator.js @@ -179,6 +179,7 @@ exports.processAll = async ({ } while (unprocessedMessages.length > 0); await settings.markAttachmentMigrationComplete(connection); + await settings.deleteAttachmentMigrationLastProcessedIndex(connection); console.log('Close database connection'); connection.close(); diff --git a/js/modules/settings.js b/js/modules/settings.js index 9cae70735..9786be7fc 100644 --- a/js/modules/settings.js +++ b/js/modules/settings.js @@ -8,19 +8,22 @@ const IS_MIGRATION_COMPLETE_KEY = 'attachmentMigration_isComplete'; // Public API exports.getAttachmentMigrationLastProcessedIndex = connection => - getItem(connection, LAST_PROCESSED_INDEX_KEY); + exports._getItem(connection, LAST_PROCESSED_INDEX_KEY); exports.setAttachmentMigrationLastProcessedIndex = (connection, value) => - setItem(connection, LAST_PROCESSED_INDEX_KEY, value); + exports._setItem(connection, LAST_PROCESSED_INDEX_KEY, value); + +exports.deleteAttachmentMigrationLastProcessedIndex = connection => + exports._deleteItem(connection, LAST_PROCESSED_INDEX_KEY); exports.isAttachmentMigrationComplete = async connection => - Boolean(await getItem(connection, IS_MIGRATION_COMPLETE_KEY)); + Boolean(await exports._getItem(connection, IS_MIGRATION_COMPLETE_KEY)); exports.markAttachmentMigrationComplete = connection => - setItem(connection, IS_MIGRATION_COMPLETE_KEY, true); + exports._setItem(connection, IS_MIGRATION_COMPLETE_KEY, true); // Private API -const getItem = (connection, key) => { +exports._getItem = (connection, key) => { if (!isObject(connection)) { throw new TypeError('"connection" is required'); } @@ -41,7 +44,7 @@ const getItem = (connection, key) => { }); }; -const setItem = (connection, key, value) => { +exports._setItem = (connection, key, value) => { if (!isObject(connection)) { throw new TypeError('"connection" is required'); } @@ -62,7 +65,7 @@ const setItem = (connection, key, value) => { }); }; -const deleteItem = (connection, key) => { +exports._deleteItem = (connection, key) => { if (!isObject(connection)) { throw new TypeError('"connection" is required'); } From 21147a20a078f36de56e115d1f3670a121e3268a Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Wed, 28 Mar 2018 17:13:11 -0400 Subject: [PATCH 42/56] Add `sleep` module --- js/modules/sleep.js | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 js/modules/sleep.js diff --git a/js/modules/sleep.js b/js/modules/sleep.js new file mode 100644 index 000000000..4cc0fc61f --- /dev/null +++ b/js/modules/sleep.js @@ -0,0 +1,4 @@ +/* global setTimeout */ + +exports.sleep = ms => + new Promise(resolve => setTimeout(resolve, ms)); From 02354ce65593a366ec69e63134fb412e2c86e29b Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Wed, 28 Mar 2018 17:13:41 -0400 Subject: [PATCH 43/56] Expose `Signal.Database` module --- preload.js | 1 + 1 file changed, 1 insertion(+) diff --git a/preload.js b/preload.js index 4ff2e4d41..97d0d3c10 100644 --- a/preload.js +++ b/preload.js @@ -124,6 +124,7 @@ window.Signal = {}; window.Signal.Backup = require('./js/modules/backup'); window.Signal.Crypto = require('./js/modules/crypto'); + window.Signal.Database = require('./js/modules/database'); window.Signal.Logs = require('./js/modules/logs'); window.Signal.Migrations = {}; window.Signal.Migrations.loadAttachmentData = Attachment.loadData(readAttachmentData); From ce8fd3d84755c25a7a839830ecc505589bff7965 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Wed, 28 Mar 2018 17:13:55 -0400 Subject: [PATCH 44/56] Expose `Signal.Settings` module --- preload.js | 1 + 1 file changed, 1 insertion(+) diff --git a/preload.js b/preload.js index 97d0d3c10..54944514e 100644 --- a/preload.js +++ b/preload.js @@ -135,6 +135,7 @@ window.Signal.Migrations.Migrations1DatabaseWithoutAttachmentData = require('./js/modules/migrations/migrations_1_database_without_attachment_data'); window.Signal.OS = require('./js/modules/os'); + window.Signal.Settings = require('./js/modules/settings'); window.Signal.Types = {}; window.Signal.Types.Attachment = Attachment; window.Signal.Types.Errors = require('./js/modules/types/errors'); From 30037e53087a14992f69169b2e5ab6884602dba6 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Wed, 28 Mar 2018 18:14:26 -0400 Subject: [PATCH 45/56] Reduce attachment migration batch size to 1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This prevents ‘Maximum IPC message size exceeded’ due to IDB `getAll` operation. - https://github.com/zincbase/zincdb/issues/17 - https://cs.chromium.org/chromium/src/content/browser/indexed_db/indexed_db_database.cc?q=%22Maximum+IPC+message+size+exceeded%22&sq=package:chromium&l=1160 --- js/modules/messages_data_migrator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/modules/messages_data_migrator.js b/js/modules/messages_data_migrator.js index 24f9d23f0..43730020b 100644 --- a/js/modules/messages_data_migrator.js +++ b/js/modules/messages_data_migrator.js @@ -19,7 +19,7 @@ const { deferredToPromise } = require('./deferred_to_promise'); const MESSAGES_STORE_NAME = 'messages'; -const NUM_MESSAGES_PER_BATCH = 50; +const NUM_MESSAGES_PER_BATCH = 1; exports.processNext = async ({ BackboneMessage, From d3c9de4712cd90dff1c1654ece39575f7ce3fe94 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Wed, 28 Mar 2018 18:16:35 -0400 Subject: [PATCH 46/56] Add debug module Lets us generate large conversations with large attachments. --- js/modules/debug.js | 132 ++++++++++++++++++++++++++++++++++++++++++++ preload.js | 1 + 2 files changed, 133 insertions(+) create mode 100644 js/modules/debug.js diff --git a/js/modules/debug.js b/js/modules/debug.js new file mode 100644 index 000000000..7a3f7fd28 --- /dev/null +++ b/js/modules/debug.js @@ -0,0 +1,132 @@ +const isFunction = require('lodash/isFunction'); +const isNumber = require('lodash/isNumber'); +const isObject = require('lodash/isObject'); +const isString = require('lodash/isString'); +const random = require('lodash/random'); +const range = require('lodash/range'); +const sample = require('lodash/sample'); + +const Message = require('./types/message'); +const { deferredToPromise } = require('./deferred_to_promise'); +const { sleep } = require('./sleep'); + + +// See: https://en.wikipedia.org/wiki/Fictitious_telephone_number#North_American_Numbering_Plan +const SENDER_ID = '+12126647665'; + +exports.createConversation = async ({ + ConversationController, + numMessages, + WhisperMessage, +} = {}) => { + if (!isObject(ConversationController) || + !isFunction(ConversationController.getOrCreateAndWait)) { + throw new TypeError('"ConversationController" is required'); + } + + if (!isNumber(numMessages) || numMessages <= 0) { + throw new TypeError('"numMessages" must be a positive number'); + } + + if (!isFunction(WhisperMessage)) { + throw new TypeError('"WhisperMessage" is required'); + } + + const conversation = + await ConversationController.getOrCreateAndWait(SENDER_ID, 'private'); + conversation.set({ + active_at: Date.now(), + unread: numMessages, + }); + await deferredToPromise(conversation.save()); + + const conversationId = conversation.get('id'); + + await Promise.all(range(0, numMessages).map(async (index) => { + await sleep(index * 100); + console.log(`Create message ${index + 1}`); + const message = new WhisperMessage(createRandomMessage({ conversationId })); + return deferredToPromise(message.save()); + })); +}; + +const SAMPLE_MESSAGES = [ + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + 'Integer et rutrum leo, eu ultrices ligula.', + 'Nam vel aliquam quam.', + 'Suspendisse posuere nunc vitae pulvinar lobortis.', + 'Nunc et sapien ex.', + 'Duis nec neque eu arcu ultrices ullamcorper in et mauris.', + 'Praesent mi felis, hendrerit a nulla id, mattis consectetur est.', + 'Duis venenatis posuere est sit amet congue.', + 'Vestibulum vitae sapien ultricies, auctor purus vitae, laoreet lacus.', + 'Fusce laoreet nisi dui, a bibendum metus consequat in.', + 'Nulla sed iaculis odio, sed lobortis lacus.', + 'Etiam massa felis, gravida at nibh viverra, tincidunt convallis justo.', + 'Maecenas ut egestas urna.', + 'Pellentesque consectetur mattis imperdiet.', + 'Maecenas pulvinar efficitur justo a cursus.', +]; + +const ATTACHMENT_SAMPLE_RATE = 0.33; +const createRandomMessage = ({ conversationId } = {}) => { + if (!isString(conversationId)) { + throw new TypeError('"conversationId" must be a string'); + } + + const sentAt = Date.now() - random(100 * 24 * 60 * 60 * 1000); + const receivedAt = sentAt + random(30 * 1000); + + const hasAttachment = Math.random() <= ATTACHMENT_SAMPLE_RATE; + const attachments = hasAttachment + ? [createRandomInMemoryAttachment()] : []; + const type = sample(['incoming', 'outgoing']); + const commonProperties = { + attachments, + body: sample(SAMPLE_MESSAGES), + conversationId, + received_at: receivedAt, + sent_at: sentAt, + timestamp: receivedAt, + type, + }; + + const message = (() => { + switch (type) { + case 'incoming': + return Object.assign({}, commonProperties, { + flags: 0, + source: conversationId, + sourceDevice: 1, + }); + case 'outgoing': + return Object.assign({}, commonProperties, { + delivered: 1, + delivered_to: [conversationId], + expireTimer: 0, + recipients: [conversationId], + sent_to: [conversationId], + synced: true, + }); + default: + throw new TypeError(`Unknown message type: '${type}'`); + } + })(); + + return Message.initializeSchemaVersion(message); +}; + +const MEGA_BYTE = 1e6; +const createRandomInMemoryAttachment = () => { + const numBytes = (1 + Math.ceil((Math.random() * 50))) * MEGA_BYTE; + const array = new Uint32Array(numBytes).fill(1); + const data = array.buffer; + const fileName = Math.random().toString().slice(2); + + return { + contentType: 'application/octet-stream', + data, + fileName, + size: numBytes, + }; +}; diff --git a/preload.js b/preload.js index 54944514e..99e8d5f88 100644 --- a/preload.js +++ b/preload.js @@ -125,6 +125,7 @@ window.Signal.Backup = require('./js/modules/backup'); window.Signal.Crypto = require('./js/modules/crypto'); window.Signal.Database = require('./js/modules/database'); + window.Signal.Debug = require('./js/modules/debug'); window.Signal.Logs = require('./js/modules/logs'); window.Signal.Migrations = {}; window.Signal.Migrations.loadAttachmentData = Attachment.loadData(readAttachmentData); From a4ecf1a9d6a597e602bb37a29e15bd5cd8b08866 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Thu, 29 Mar 2018 13:11:32 -0400 Subject: [PATCH 47/56] Define constant after creating idle detector --- js/background.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/js/background.js b/js/background.js index 38d1c683c..c8bb9171c 100644 --- a/js/background.js +++ b/js/background.js @@ -104,8 +104,9 @@ console.log('Storage fetch'); storage.fetch(); - const NUM_MESSAGE_UPGRADES_PER_IDLE = 2; const idleDetector = new IdleDetector(); + + const NUM_MESSAGE_UPGRADES_PER_IDLE = 2; idleDetector.on('idle', async () => { const results = await MessageDataMigrator.processNext({ BackboneMessage: Whisper.Message, From 0c40f3562387f4061ad6463a83c6b86614ea38b8 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Thu, 29 Mar 2018 13:12:18 -0400 Subject: [PATCH 48/56] Document disadvantage of fetching messages without index --- js/modules/messages_data_migrator.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/js/modules/messages_data_migrator.js b/js/modules/messages_data_migrator.js index 43730020b..1e2a5b309 100644 --- a/js/modules/messages_data_migrator.js +++ b/js/modules/messages_data_migrator.js @@ -238,6 +238,8 @@ const _fetchMessagesRequiringSchemaUpgrade = })); }; +// NOTE: Named ‘dangerous’ because it is not as efficient as using our +// `messages` `schemaVersion` index: const _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex = ({ connection, count, lastIndex } = {}) => { if (!isObject(connection)) { From 77f8f598de6259d2eb887e2c2ca38a6e16a2cc01 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Thu, 29 Mar 2018 15:08:23 -0400 Subject: [PATCH 49/56] Add `disk-usage.sh` script for testing --- .../disk-usage.sh | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100755 scripts/features/2193-migrate-attachments-on-startup/disk-usage.sh diff --git a/scripts/features/2193-migrate-attachments-on-startup/disk-usage.sh b/scripts/features/2193-migrate-attachments-on-startup/disk-usage.sh new file mode 100755 index 000000000..ffde28a24 --- /dev/null +++ b/scripts/features/2193-migrate-attachments-on-startup/disk-usage.sh @@ -0,0 +1,18 @@ +#!/bin/bash +ROOT=$1 + +if [[ "$1" == "" ]]; then + echo "Usage: $(basename "$0") " + exit 1 +fi + +while true +do + echo -n "$(date -u +"%Y-%m-%dT%H:%M:%SZ ")" + du -sm "$ROOT/attachments.noindex" + + echo -n "$(date -u +"%Y-%m-%dT%H:%M:%SZ ")" + du -sm "$ROOT/IndexedDB" + + sleep 1 +done From c67c2a858a2b5df134b533b55a2976e8fd854d85 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Thu, 29 Mar 2018 15:38:05 -0400 Subject: [PATCH 50/56] Remove Backbone references for attachment migration It has to run without any other migrations interfering. --- js/modules/messages_data_migrator.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/js/modules/messages_data_migrator.js b/js/modules/messages_data_migrator.js index 1e2a5b309..2071f69df 100644 --- a/js/modules/messages_data_migrator.js +++ b/js/modules/messages_data_migrator.js @@ -75,15 +75,10 @@ exports.processNext = async ({ }; exports.processAll = async ({ - Backbone, databaseName, minDatabaseVersion, upgradeMessageSchema, } = {}) => { - if (!isObject(Backbone)) { - throw new TypeError('"Backbone" is required'); - } - if (!isString(databaseName)) { throw new TypeError('"databaseName" must be a string'); } From 1f8556b049f705079105993894933defb8bffe95 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Thu, 29 Mar 2018 15:44:37 -0400 Subject: [PATCH 51/56] Remove unused `createCollection` --- ...rations_0_database_with_attachment_data.js | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/js/modules/migrations/migrations_0_database_with_attachment_data.js b/js/modules/migrations/migrations_0_database_with_attachment_data.js index 02cd097ff..c47f032a5 100644 --- a/js/modules/migrations/migrations_0_database_with_attachment_data.js +++ b/js/modules/migrations/migrations_0_database_with_attachment_data.js @@ -1,6 +1,3 @@ -const isFunction = require('lodash/isFunction'); -const isObject = require('lodash/isObject'); -const isString = require('lodash/isString'); const last = require('lodash/last'); const { runMigrations } = require('./run_migrations'); @@ -153,24 +150,6 @@ const database = { exports.run = ({ Backbone } = {}) => runMigrations({ Backbone, database }); -exports.createCollection = ({ Backbone, storeName }) => { - if (!isObject(Backbone) || !isObject(Backbone.Collection) || - !isFunction(Backbone.Collection.extend)) { - throw new TypeError('"Backbone" is required'); - } - - if (!isString(storeName)) { - throw new TypeError('"database" is required'); - } - - const collection = new (Backbone.Collection.extend({ - database, - storeName, - }))(); - - return collection; -}; - exports.getDatabase = () => ({ name: database.id, version: last(migrations).version, From 0fdc1140dd3956e72c38b1bcee517eff7ff5de6c Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Thu, 29 Mar 2018 16:15:23 -0400 Subject: [PATCH 52/56] Add `Database.getCount` function --- js/modules/database.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/js/modules/database.js b/js/modules/database.js index f89bac595..f780a4031 100644 --- a/js/modules/database.js +++ b/js/modules/database.js @@ -4,6 +4,9 @@ // and using promises. Revisit use of `idb` dependency as it might cover // this functionality. +const isObject = require('lodash/isObject'); + + exports.open = (name, version) => { const request = indexedDB.open(name, version); return new Promise((resolve, reject) => { @@ -37,3 +40,17 @@ exports.getVersion = async (name) => { connection.close(); return version; }; + +exports.getCount = async ({ store } = {}) => { + if (!isObject(store)) { + throw new TypeError('"store" is required'); + } + + const request = store.count(); + return new Promise((resolve, reject) => { + request.onerror = event => + reject(event.target.error); + request.onsuccess = event => + resolve(event.target.result); + }); +}; From 11f98474baeec18704065a65f7a650ad6456d9b4 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Thu, 29 Mar 2018 16:21:52 -0400 Subject: [PATCH 53/56] Capture how many messages we have to process --- js/modules/messages_data_migrator.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/js/modules/messages_data_migrator.js b/js/modules/messages_data_migrator.js index 2071f69df..7357b9a6b 100644 --- a/js/modules/messages_data_migrator.js +++ b/js/modules/messages_data_migrator.js @@ -110,6 +110,12 @@ exports.processAll = async ({ return; } + let numTotalMessages = null; + // eslint-disable-next-line more/no-then + getNumMessages({ connection }).then((numMessages) => { + numTotalMessages = numMessages; + }); + const migrationStartTime = Date.now(); let unprocessedMessages = []; let totalMessagesProcessed = 0; @@ -165,6 +171,7 @@ exports.processAll = async ({ lastProcessedIndex, numUnprocessedMessages, numCumulativeMessagesProcessed: totalMessagesProcessed, + numTotalMessages, fetchDuration, saveDuration, upgradeDuration, @@ -266,3 +273,16 @@ const _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex = reject(event.target.error); }); }; + +const getNumMessages = async ({ connection } = {}) => { + if (!isObject(connection)) { + throw new TypeError('"connection" is required'); + } + + const transaction = connection.transaction(MESSAGES_STORE_NAME, 'readonly'); + const messagesStore = transaction.objectStore(MESSAGES_STORE_NAME); + const numTotalMessages = await database.getCount({ store: messagesStore }); + await database.completeTransaction(transaction); + + return numTotalMessages; +}; From b7b6195cfc4bf558179fa90cab573eacf3101a54 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Mon, 2 Apr 2018 12:10:29 -0400 Subject: [PATCH 54/56] Extract IIFE into separate function --- js/modules/debug.js | 45 +++++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/js/modules/debug.js b/js/modules/debug.js index 7a3f7fd28..cfa39cd45 100644 --- a/js/modules/debug.js +++ b/js/modules/debug.js @@ -91,31 +91,32 @@ const createRandomMessage = ({ conversationId } = {}) => { type, }; - const message = (() => { - switch (type) { - case 'incoming': - return Object.assign({}, commonProperties, { - flags: 0, - source: conversationId, - sourceDevice: 1, - }); - case 'outgoing': - return Object.assign({}, commonProperties, { - delivered: 1, - delivered_to: [conversationId], - expireTimer: 0, - recipients: [conversationId], - sent_to: [conversationId], - synced: true, - }); - default: - throw new TypeError(`Unknown message type: '${type}'`); - } - })(); - + const message = _createMessage({ commonProperties, conversationId, type }); return Message.initializeSchemaVersion(message); }; +const _createMessage = ({ commonProperties, conversationId, type } = {}) => { + switch (type) { + case 'incoming': + return Object.assign({}, commonProperties, { + flags: 0, + source: conversationId, + sourceDevice: 1, + }); + case 'outgoing': + return Object.assign({}, commonProperties, { + delivered: 1, + delivered_to: [conversationId], + expireTimer: 0, + recipients: [conversationId], + sent_to: [conversationId], + synced: true, + }); + default: + throw new TypeError(`Unknown message type: '${type}'`); + } +}; + const MEGA_BYTE = 1e6; const createRandomInMemoryAttachment = () => { const numBytes = (1 + Math.ceil((Math.random() * 50))) * MEGA_BYTE; From d9be6a0f9446e2b3942675002a0a7a017647ed85 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Mon, 2 Apr 2018 15:08:53 -0400 Subject: [PATCH 55/56] Destructure Lodash `require`s --- app/attachments.js | 6 +++--- app/menu.js | 2 +- js/modules/database.js | 2 +- js/modules/debug.js | 16 +++++++++------- js/modules/messages_data_migrator.js | 12 +++++++----- ...migrations_0_database_with_attachment_data.js | 2 +- js/modules/migrations/run_migrations.js | 13 ++++++++----- js/modules/privacy.js | 10 ++++++---- js/modules/settings.js | 3 +-- js/modules/types/attachment.js | 3 +-- .../attachment/migrate_data_to_file_system.js | 10 ++++++---- js/modules/types/message.js | 2 +- js/modules/types/schema_version.js | 2 +- 13 files changed, 46 insertions(+), 37 deletions(-) diff --git a/app/attachments.js b/app/attachments.js index 0b7b5a068..46dca8589 100644 --- a/app/attachments.js +++ b/app/attachments.js @@ -1,9 +1,9 @@ const crypto = require('crypto'); -const fse = require('fs-extra'); -const isArrayBuffer = require('lodash/isArrayBuffer'); -const isString = require('lodash/isString'); const path = require('path'); + +const fse = require('fs-extra'); const toArrayBuffer = require('to-arraybuffer'); +const { isArrayBuffer, isString } = require('lodash'); const PATH = 'attachments.noindex'; diff --git a/app/menu.js b/app/menu.js index 16c0fe23f..ecc1f654b 100644 --- a/app/menu.js +++ b/app/menu.js @@ -1,4 +1,4 @@ -const isString = require('lodash/isString'); +const { isString } = require('lodash'); exports.createTemplate = (options, messages) => { diff --git a/js/modules/database.js b/js/modules/database.js index f780a4031..24c20555e 100644 --- a/js/modules/database.js +++ b/js/modules/database.js @@ -4,7 +4,7 @@ // and using promises. Revisit use of `idb` dependency as it might cover // this functionality. -const isObject = require('lodash/isObject'); +const { isObject } = require('lodash'); exports.open = (name, version) => { diff --git a/js/modules/debug.js b/js/modules/debug.js index cfa39cd45..75e4e6513 100644 --- a/js/modules/debug.js +++ b/js/modules/debug.js @@ -1,10 +1,12 @@ -const isFunction = require('lodash/isFunction'); -const isNumber = require('lodash/isNumber'); -const isObject = require('lodash/isObject'); -const isString = require('lodash/isString'); -const random = require('lodash/random'); -const range = require('lodash/range'); -const sample = require('lodash/sample'); +const { + isFunction, + isNumber, + isObject, + isString, + random, + range, + sample, +} = require('lodash'); const Message = require('./types/message'); const { deferredToPromise } = require('./deferred_to_promise'); diff --git a/js/modules/messages_data_migrator.js b/js/modules/messages_data_migrator.js index 7357b9a6b..f97872389 100644 --- a/js/modules/messages_data_migrator.js +++ b/js/modules/messages_data_migrator.js @@ -6,11 +6,13 @@ /* 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 { + isFunction, + isNumber, + isObject, + isString, + last, +} = require('lodash'); const database = require('./database'); const Message = require('./types/message'); diff --git a/js/modules/migrations/migrations_0_database_with_attachment_data.js b/js/modules/migrations/migrations_0_database_with_attachment_data.js index c47f032a5..2c42b9e90 100644 --- a/js/modules/migrations/migrations_0_database_with_attachment_data.js +++ b/js/modules/migrations/migrations_0_database_with_attachment_data.js @@ -1,4 +1,4 @@ -const last = require('lodash/last'); +const { last } = require('lodash'); const { runMigrations } = require('./run_migrations'); diff --git a/js/modules/migrations/run_migrations.js b/js/modules/migrations/run_migrations.js index ce3e3cee1..cc54a408c 100644 --- a/js/modules/migrations/run_migrations.js +++ b/js/modules/migrations/run_migrations.js @@ -1,10 +1,13 @@ /* eslint-env browser */ -const isFunction = require('lodash/isFunction'); -const isObject = require('lodash/isObject'); -const isString = require('lodash/isString'); -const head = require('lodash/head'); -const last = require('lodash/last'); +const { + head, + isFunction, + isObject, + isString, + last, +} = require('lodash'); + const db = require('../database'); const { deferredToPromise } = require('../deferred_to_promise'); diff --git a/js/modules/privacy.js b/js/modules/privacy.js index 9e5f26646..1a90884c5 100644 --- a/js/modules/privacy.js +++ b/js/modules/privacy.js @@ -2,10 +2,12 @@ const Path = require('path'); -const compose = require('lodash/fp/compose'); -const escapeRegExp = require('lodash/escapeRegExp'); -const isRegExp = require('lodash/isRegExp'); -const isString = require('lodash/isString'); +const { + escapeRegExp, + isRegExp, + isString, +} = require('lodash'); +const { compose } = require('lodash/fp'); const PHONE_NUMBER_PATTERN = /\+\d{7,12}(\d{3})/g; diff --git a/js/modules/settings.js b/js/modules/settings.js index 9786be7fc..d99bfdeb9 100644 --- a/js/modules/settings.js +++ b/js/modules/settings.js @@ -1,5 +1,4 @@ -const isObject = require('lodash/isObject'); -const isString = require('lodash/isString'); +const { isObject, isString } = require('lodash'); const ITEMS_STORE_NAME = 'items'; diff --git a/js/modules/types/attachment.js b/js/modules/types/attachment.js index 07e8c825d..c807afc2b 100644 --- a/js/modules/types/attachment.js +++ b/js/modules/types/attachment.js @@ -1,5 +1,4 @@ -const isFunction = require('lodash/isFunction'); -const isString = require('lodash/isString'); +const { isFunction, isString } = require('lodash'); const MIME = require('./mime'); const { arrayBufferToBlob, blobToArrayBuffer, dataURLToBlob } = require('blob-util'); diff --git a/js/modules/types/attachment/migrate_data_to_file_system.js b/js/modules/types/attachment/migrate_data_to_file_system.js index ed21cb2ab..d2709d90f 100644 --- a/js/modules/types/attachment/migrate_data_to_file_system.js +++ b/js/modules/types/attachment/migrate_data_to_file_system.js @@ -1,7 +1,9 @@ -const isArrayBuffer = require('lodash/isArrayBuffer'); -const isFunction = require('lodash/isFunction'); -const isUndefined = require('lodash/isUndefined'); -const omit = require('lodash/omit'); +const { + isArrayBuffer, + isFunction, + isUndefined, + omit, +} = require('lodash'); // type Context :: { diff --git a/js/modules/types/message.js b/js/modules/types/message.js index a43e9c664..d501532e7 100644 --- a/js/modules/types/message.js +++ b/js/modules/types/message.js @@ -1,4 +1,4 @@ -const isFunction = require('lodash/isFunction'); +const { isFunction } = require('lodash'); const Attachment = require('./attachment'); const Errors = require('./errors'); diff --git a/js/modules/types/schema_version.js b/js/modules/types/schema_version.js index 3a0d08980..058a36a1e 100644 --- a/js/modules/types/schema_version.js +++ b/js/modules/types/schema_version.js @@ -1,4 +1,4 @@ -const isNumber = require('lodash/isNumber'); +const { isNumber } = require('lodash'); exports.isValid = value => From bfbeedab5ce1cdbff14f3709e5749bd912ed8aa6 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Mon, 2 Apr 2018 15:26:23 -0400 Subject: [PATCH 56/56] Temporarily disable post-attachment migration migrations --- js/background.js | 28 +++++++++---------- js/database.js | 4 +-- ...rations_0_database_with_attachment_data.js | 6 ++-- ...ions_1_database_without_attachment_data.js | 16 +++++------ 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/js/background.js b/js/background.js index c8bb9171c..5c72a636f 100644 --- a/js/background.js +++ b/js/background.js @@ -19,7 +19,7 @@ const { upgradeMessageSchema } = window.Signal.Migrations; const { Migrations0DatabaseWithAttachmentData, - Migrations1DatabaseWithoutAttachmentData, + // Migrations1DatabaseWithoutAttachmentData, } = window.Signal.Migrations; const { Views } = window.Signal; @@ -86,20 +86,20 @@ console.log('Migrate database with attachments'); await Migrations0DatabaseWithAttachmentData.run({ Backbone }); - console.log('Migrate attachments to disk'); - const database = Migrations0DatabaseWithAttachmentData.getDatabase(); - await MessageDataMigrator.processAll({ - Backbone, - databaseName: database.name, - minDatabaseVersion: database.version, - upgradeMessageSchema, - }); + // console.log('Migrate attachments to disk'); + // const database = Migrations0DatabaseWithAttachmentData.getDatabase(); + // await MessageDataMigrator.processAll({ + // Backbone, + // databaseName: database.name, + // minDatabaseVersion: database.version, + // upgradeMessageSchema, + // }); - console.log('Migrate database without attachments'); - await Migrations1DatabaseWithoutAttachmentData.run({ - Backbone, - database: Whisper.Database, - }); + // console.log('Migrate database without attachments'); + // await Migrations1DatabaseWithoutAttachmentData.run({ + // Backbone, + // database: Whisper.Database, + // }); console.log('Storage fetch'); storage.fetch(); diff --git a/js/database.js b/js/database.js index 6b1bb9f93..242e91a7b 100644 --- a/js/database.js +++ b/js/database.js @@ -6,7 +6,7 @@ (function () { 'use strict'; - const { Migrations1DatabaseWithoutAttachmentData } = window.Signal.Migrations; + const { Migrations0DatabaseWithAttachmentData } = window.Signal.Migrations; window.Whisper = window.Whisper || {}; window.Whisper.Database = window.Whisper.Database || {}; @@ -123,5 +123,5 @@ request.onsuccess = resolve; })); - Whisper.Database.migrations = Migrations1DatabaseWithoutAttachmentData.migrations; + Whisper.Database.migrations = Migrations0DatabaseWithAttachmentData.migrations; }()); diff --git a/js/modules/migrations/migrations_0_database_with_attachment_data.js b/js/modules/migrations/migrations_0_database_with_attachment_data.js index 2c42b9e90..a12e788ff 100644 --- a/js/modules/migrations/migrations_0_database_with_attachment_data.js +++ b/js/modules/migrations/migrations_0_database_with_attachment_data.js @@ -8,7 +8,7 @@ const { runMigrations } = require('./run_migrations'); // any expensive operations, e.g. modifying all messages / attachments, etc., as // it may cause out-of-memory errors for users with long histories: // https://github.com/signalapp/Signal-Desktop/issues/2163 -const migrations = [ +exports.migrations = [ { version: '12.0', migrate(transaction, next) { @@ -144,7 +144,7 @@ const migrations = [ const database = { id: 'signal', nolog: true, - migrations, + migrations: exports.migrations, }; exports.run = ({ Backbone } = {}) => @@ -152,5 +152,5 @@ exports.run = ({ Backbone } = {}) => exports.getDatabase = () => ({ name: database.id, - version: last(migrations).version, + version: last(exports.migrations).version, }); diff --git a/js/modules/migrations/migrations_1_database_without_attachment_data.js b/js/modules/migrations/migrations_1_database_without_attachment_data.js index b10106ef2..a4fb3e870 100644 --- a/js/modules/migrations/migrations_1_database_without_attachment_data.js +++ b/js/modules/migrations/migrations_1_database_without_attachment_data.js @@ -2,14 +2,14 @@ const { runMigrations } = require('./run_migrations'); exports.migrations = [ - { - version: 18, - async migrate(transaction, next) { - console.log('Migration 18'); - console.log('Attachments stored on disk'); - next(); - }, - }, + // { + // version: 18, + // async migrate(transaction, next) { + // console.log('Migration 18'); + // console.log('Attachments stored on disk'); + // next(); + // }, + // }, ]; exports.run = runMigrations;