diff --git a/app/common/state/immutableUtil.js b/app/common/state/immutableUtil.js index 0d3316d61a4..c048e5fdc86 100644 --- a/app/common/state/immutableUtil.js +++ b/app/common/state/immutableUtil.js @@ -29,6 +29,15 @@ const api = { makeImmutable: (obj) => { return api.isImmutable(obj) ? obj : Immutable.fromJS(obj) + }, + + deleteImmutablePaths: (obj, paths) => { + return paths.reduce((result, path) => { + if (path.constructor === Array) { + return obj.deleteIn(path) + } + return obj.delete(path) + }, obj) } } diff --git a/app/index.js b/app/index.js index e8fd4dc0196..106a3c8ef4a 100644 --- a/app/index.js +++ b/app/index.js @@ -94,19 +94,19 @@ const defaultProtocols = ['http', 'https'] let loadAppStatePromise = SessionStore.loadAppState() // Some settings must be set right away on startup, those settings should be handled here. -loadAppStatePromise.then((initialState) => { +loadAppStatePromise.then((initialImmutableState) => { telemetry.setCheckpointAndReport('state-loaded') const {HARDWARE_ACCELERATION_ENABLED, SMOOTH_SCROLL_ENABLED, SEND_CRASH_REPORTS} = require('../js/constants/settings') - if (initialState.settings[HARDWARE_ACCELERATION_ENABLED] === false) { + if (initialImmutableState.getIn('settings', HARDWARE_ACCELERATION_ENABLED) === false) { app.disableHardwareAcceleration() } - if (initialState.settings[SEND_CRASH_REPORTS] !== false) { + if (initialImmutableState.getIn(['settings', SEND_CRASH_REPORTS]) !== false) { console.log('Crash reporting enabled') CrashHerald.init() } else { console.log('Crash reporting disabled') } - if (initialState.settings[SMOOTH_SCROLL_ENABLED] === false) { + if (initialImmutableState.getIn(['settings', SMOOTH_SCROLL_ENABLED]) === false) { app.commandLine.appendSwitch('disable-smooth-scrolling') } }) @@ -158,18 +158,17 @@ app.on('ready', () => { appActions.networkDisconnected() }) - loadAppStatePromise.then((initialState) => { + loadAppStatePromise.then((initialImmutableState) => { // Do this after loading the state // For tests we always want to load default app state - const loadedPerWindowState = initialState.perWindowState - delete initialState.perWindowState + const loadedPerWindowImmutableState = initialImmutableState.get('perWindowState') + initialImmutableState = initialImmutableState.delete('perWindowState') // Restore map order after load - let state = Immutable.fromJS(initialState) - appActions.setState(state) - setImmediate(() => perWindowStateLoaded(loadedPerWindowState)) + appActions.setState(initialImmutableState) + setImmediate(() => perWindowStateLoaded(loadedPerWindowImmutableState)) }) - const perWindowStateLoaded = (loadedPerWindowState) => { + const perWindowStateLoaded = (loadedPerWindowImmutableState) => { // TODO(bridiver) - this shold be refactored into reducers // DO NOT ADD ANYHING TO THIS LIST // See tabsReducer.js for app state init example @@ -184,12 +183,12 @@ app.on('ready', () => { AdInsertion.init() PDFJS.init() - if (!loadedPerWindowState || loadedPerWindowState.length === 0) { + if (!loadedPerWindowImmutableState || loadedPerWindowImmutableState.size === 0) { if (!CmdLine.newWindowURL()) { appActions.newWindow() } } else { - loadedPerWindowState.forEach((wndState) => { + loadedPerWindowImmutableState.forEach((wndState) => { appActions.newWindow(undefined, undefined, wndState) }) } @@ -304,12 +303,12 @@ app.on('ready', () => { // DO NOT TO THIS LIST - see above // We need the initial state to read the UPDATE_TO_PREVIEW_RELEASES preference - loadAppStatePromise.then((initialState) => { + loadAppStatePromise.then((initialImmutableState) => { updater.init( process.platform, process.arch, process.env.BRAVE_UPDATE_VERSION || app.getVersion(), - initialState.settings[settings.UPDATE_TO_PREVIEW_RELEASES] + initialImmutableState.getIn('settings', settings.UPDATE_TO_PREVIEW_RELEASES) ) // This is fired by a menu entry diff --git a/app/sessionStore.js b/app/sessionStore.js index d51ea865b6b..1c17a3c3975 100644 --- a/app/sessionStore.js +++ b/app/sessionStore.js @@ -15,6 +15,8 @@ const fs = require('fs-extra') const path = require('path') const electron = require('electron') const os = require('os') +const assert = require('assert') +const Immutable = require('immutable') const app = electron.app // Constants @@ -35,7 +37,7 @@ const filtering = require('./filtering') const autofill = require('./autofill') const {navigatableTypes} = require('../js/lib/appUrlUtil') const Channel = require('./channel') -const {makeImmutable} = require('./common/state/immutableUtil') +const {isImmutable, makeImmutable, deleteImmutablePaths} = require('./common/state/immutableUtil') const {getSetting} = require('../js/settings') const platformUtil = require('./common/lib/platformUtil') const historyUtil = require('./common/lib/historyUtil') @@ -57,38 +59,44 @@ const getStoragePath = () => { /** * Saves the specified immutable browser state to storage. * - * @param {object} payload - Application state as per + * @param {object} immutablePayload - Application immutable state as per * https://github.com/brave/browser/wiki/Application-State * (not immutable data) * @return a promise which resolves when the state is saved */ -module.exports.saveAppState = (payload, isShutdown) => { +module.exports.saveAppState = (immutablePayload, isShutdown) => { + assert(isImmutable(immutablePayload)) + return new Promise((resolve, reject) => { // Don't persist private frames let startupModeSettingValue = getSetting(settings.STARTUP_MODE) const savePerWindowState = startupModeSettingValue == null || startupModeSettingValue === 'lastTime' - if (payload.perWindowState && savePerWindowState) { - payload.perWindowState.forEach((wndPayload) => { - wndPayload.frames = wndPayload.frames.filter((frame) => !frame.isPrivate) + if (immutablePayload.get('perWindowState') && savePerWindowState) { + immutablePayload.get('perWindowState').forEach((immutableWndPayload, i) => { + const frames = immutableWndPayload.get('frames').filter((frame) => !frame.get('isPrivate')) + immutableWndPayload = immutableWndPayload.set('frames', frames) + immutablePayload = immutablePayload.setIn(['perWindowState', i], immutableWndPayload) }) } else { - delete payload.perWindowState + immutablePayload = immutablePayload.delete('perWindowState') } try { - payload = module.exports.cleanAppData(payload, isShutdown) - payload.cleanedOnShutdown = isShutdown + immutablePayload = module.exports.cleanAppData(immutablePayload, isShutdown) + immutablePayload.set('cleanedOnShutdown', isShutdown) } catch (e) { - payload.cleanedOnShutdown = false + immutablePayload.set('cleanedOnShutdown', false) } - payload.lastAppVersion = app.getVersion() + immutablePayload.set('lastAppVersion', app.getVersion()) if (isShutdown) { module.exports.cleanSessionDataOnShutdown() } - muon.file.writeImportant(getStoragePath(), JSON.stringify(payload), (success) => { + const storagePath = getStoragePath() + const json = JSON.stringify(immutablePayload) + muon.file.writeImportant(storagePath, json, (success) => { if (success) { resolve() } else { @@ -100,169 +108,185 @@ module.exports.saveAppState = (payload, isShutdown) => { /** * Cleans session data from unwanted values. + * @param immutablePerWindowData - Per window data in ImmutableJS format + * @return ImmutableJS cleaned window data */ -module.exports.cleanPerWindowData = (perWindowData, isShutdown) => { - if (!perWindowData) { - perWindowData = {} +module.exports.cleanPerWindowData = (immutablePerWindowData, isShutdown) => { + if (!immutablePerWindowData) { + immutablePerWindowData = Immutable.Map() } + + assert(isImmutable(immutablePerWindowData)) + // delete the frame index because tabId is per-session - delete perWindowData.framesInternal - // Hide the context menu when we restore. - delete perWindowData.contextMenuDetail - // Don't save preview frame since they are only related to hovering on a tab - delete perWindowData.previewFrameKey - // Don't save widevine panel detail - delete perWindowData.widevinePanelDetail - // Don't save preview tab pages - if (perWindowData.ui && perWindowData.ui.tabs) { - delete perWindowData.ui.tabs.previewTabPageIndex - } - // Don't restore add/edit dialog - delete perWindowData.bookmarkDetail - // Don't restore bravery panel - delete perWindowData.braveryPanelDetail - // Don't restore drag data and clearBrowsingDataPanel's visibility - if (perWindowData.ui) { + immutablePerWindowData = immutablePerWindowData.delete('framesInternal') + + immutablePerWindowData = deleteImmutablePaths(immutablePerWindowData, [ + // Hide the context menu when we restore. + 'contextMenuDetail', + // Don't save preview frame since they are only related to hovering on a tab + 'previewFrameKey', + // Don't save widevine panel detail + 'widevinePanelDetail', + // Don't save preview tab pages + ['ui', 'tabs', 'previewTabPageIndex'], + // Don't restore add/edit dialog + 'bookmarkDetail', + // Don't restore bravery panel + 'braveryPanelDetail', + // Don't restore drag data and clearBrowsingDataPanel's visibility // This is no longer stored, we can remove this line eventually - delete perWindowData.ui.isFocused - delete perWindowData.ui.mouseInTitlebar - delete perWindowData.ui.mouseInFrame - delete perWindowData.ui.dragging - delete perWindowData.ui.isClearBrowsingDataPanelVisible + ['ui', 'isFocused'], + ['ui', 'mouseInTitlebar'], + ['ui', 'mouseInFrame'], + ['ui', 'dragging'], + ['ui', 'isClearBrowsingDataPanelVisible'] + ]) + + if (!immutablePerWindowData.get('frames')) { + immutablePerWindowData = immutablePerWindowData.set('frames', Immutable.List()) } - perWindowData.frames = perWindowData.frames || [] + let newKey = 0 - const cleanFrame = (frame) => { + const cleanFrame = (immutableFrame) => { newKey++ // Reset the ids back to sequential numbers - if (frame.key === perWindowData.activeFrameKey) { - perWindowData.activeFrameKey = newKey + if (immutableFrame.get('key') === immutablePerWindowData.get('activeFrameKey')) { + immutablePerWindowData = immutablePerWindowData.set('activeFrameKey', newKey) } else { // For now just set everything to unloaded unless it's the active frame - frame.unloaded = true + immutableFrame = immutableFrame.set('unloaded', true) } - frame.key = newKey + immutableFrame = immutableFrame.set('key', newKey) // Set the frame src to the last visited location // or else users will see the first visited URL. // Pinned location always get reset to what they are - frame.src = frame.pinnedLocation || frame.location + immutableFrame = immutableFrame.set('src', immutableFrame.get('pinnedLocation') || immutableFrame.get('location')) // If a blob is present for the thumbnail, create the object URL - if (frame.thumbnailBlob) { + if (immutableFrame.get('thumbnailBlob')) { try { - frame.thumbnailUrl = window.URL.createObjectURL(frame.thumbnailBlob) + immutableFrame.set('thumbnailUrl', window.URL.createObjectURL(immutableFrame.get('thumbnailBlob'))) } catch (e) { - delete frame.thumbnailUrl + immutableFrame = immutableFrame.delete('thumbnailUrl') } } - // Delete lists of blocked sites - delete frame.trackingProtection - delete frame.httpsEverywhere - delete frame.adblock - delete frame.noScript - - // clean up any legacy frame opening props - delete frame.openInForeground - delete frame.disposition - - // Guest instance ID's are not valid after restarting. - // Electron won't know about them. - delete frame.guestInstanceId - - // Tab ids are per-session and should not be persisted - delete frame.tabId - - // Do not show the audio indicator until audio starts playing - delete frame.audioMuted - delete frame.audioPlaybackActive - // Let's not assume wknow anything about loading - delete frame.loading - // Always re-determine the security data - delete frame.security - // Value is only used for local storage - delete frame.isActive - // Hide modal prompts. - delete frame.modalPromptDetail - // Remove HTTP basic authentication requests. - delete frame.basicAuthDetail - // Remove open search details - delete frame.searchDetail - // Remove find in page details - if (frame.findDetail) { - delete frame.findDetail.numberOfMatches - delete frame.findDetail.activeMatchOrdinal - delete frame.findDetail.internalFindStatePresent - } - delete frame.findbarShown - // Don't restore full screen state - delete frame.isFullScreen - delete frame.showFullScreenWarning - // Don't store child tab open ordering since keys - // currently get re-generated when session store is - // restored. We will be able to keep this once we - // don't regenerate new frame keys when opening storage. - delete frame.parentFrameKey - // Delete the active shortcut details - delete frame.activeShortcut - delete frame.activeShortcutDetails - - if (frame.navbar && frame.navbar.urlbar) { - if (frame.navbar.urlbar.suggestions) { - frame.navbar.urlbar.suggestions.selectedIndex = null - frame.navbar.urlbar.suggestions.suggestionList = null + immutableFrame = deleteImmutablePaths(immutableFrame, [ + // Delete lists of blocked sites + 'trackingProtection', + 'httpsEverywhere', + 'adblock', + 'noScript', + // clean up any legacy frame opening props + 'openInForeground', + 'disposition', + // Guest instance ID's are not valid after restarting. + // Electron won't know about them. + 'guestInstanceId', + // Tab ids are per-session and should not be persisted + 'tabId', + // Do not show the audio indicator until audio starts playing + 'audioMuted', + 'audioPlaybackActive', + // Let's not assume wknow anything about loading + 'loading', + // Always re-determine the security data + 'security', + // Value is only used for local storage + 'isActive', + // Hide modal prompts. + 'modalPromptDetail', + // Remove HTTP basic authentication requests. + 'basicAuthDetail', + // Remove open search details + 'searchDetail', + // Remove find in page details + ['findDetail', 'numberOfMatches'], + ['findDetail', 'activeMatchOrdinal'], + ['findDetail', 'internalFindStatePresent'], + 'findbarShown', + // Don't restore full screen state + 'isFullScreen', + 'showFullScreenWarning', + // Don't store child tab open ordering since keys + // currently get re-generated when session store is + // restored. We will be able to keep this once we + // don't regenerate new frame keys when opening storage. + 'parentFrameKey', + // Delete the active shortcut details + 'activeShortcut', + 'activeShortcutDetails' + ]) + + if (immutableFrame.get('navbar') && immutableFrame.getIn(['navbar', 'urlbar'])) { + if (immutableFrame.getIn(['navbar', 'urlbar', 'suggestions'])) { + immutableFrame = immutableFrame.setIn(['navbar', 'urlbar', 'suggestions', 'selectedIndex'], null) + immutableFrame = immutableFrame.setIn(['navbar', 'urlbar', 'suggestions', 'suggestionList'], null) } - delete frame.navbar.urlbar.searchDetail + immutableFrame = immutableFrame.deleteIn(['navbar', 'urlbar', 'searchDetail']) } } const clearHistory = isShutdown && getSetting(settings.SHUTDOWN_CLEAR_HISTORY) === true if (clearHistory) { - perWindowData.closedFrames = [] + immutablePerWindowData = immutablePerWindowData.set('closedFrames', Immutable.List()) } // Clean closed frame data before frames because the keys are re-ordered // and the new next key is calculated in windowStore.js based on // the max frame key ID. - if (perWindowData.closedFrames) { - perWindowData.closedFrames.forEach(cleanFrame) - } - if (perWindowData.frames) { + if (immutablePerWindowData.closedFrames) { + immutablePerWindowData = + immutablePerWindowData.get('closedFrames').reduce((immutablePerWindowData, immutableFrame, ordinal) => { + const cleanImmutableFrame = cleanFrame(immutableFrame) + return immutablePerWindowData.setIn(['closedFrames', ordinal - 1], cleanImmutableFrame) + }, immutablePerWindowData) + } + if (immutablePerWindowData.get('frames')) { // Don't restore pinned locations because they will be auto created by the app state change event - perWindowData.frames = perWindowData.frames - .filter((frame) => !frame.pinnedLocation) - perWindowData.frames.forEach(cleanFrame) - } + immutablePerWindowData.set('frames', + immutablePerWindowData.get('frames') + .filter((frame) => !frame.get('pinnedLocation'))) + immutablePerWindowData = + immutablePerWindowData.get('frames').reduce((immutablePerWindowData, immutableFrame, ordinal) => { + const cleanImmutableFrame = cleanFrame(immutableFrame) + return immutablePerWindowData.setIn(['frames', ordinal - 1], cleanImmutableFrame) + }, immutablePerWindowData) + } + return immutablePerWindowData } /** * Cleans app data before it's written to disk. - * @param {Object} data - top-level app data + * @param {Object} data - top-level app data in ImmutableJS format * @param {Object} isShutdown - true if the data is being cleared for a shutdown * WARNING: getPrefs is only available in this function when isShutdown is true + * @return Immutable JS cleaned up data */ -module.exports.cleanAppData = (data, isShutdown) => { - // make a copy - // TODO(bridiver) use immutable - data = makeImmutable(data).toJS() +module.exports.cleanAppData = (immutableData, isShutdown) => { + assert(isImmutable(immutableData)) // Don't show notifications from the last session - data.notifications = [] + immutableData = immutableData.set('notifications', Immutable.List()) // Delete temp site settings - data.temporarySiteSettings = {} + immutableData = immutableData.set('temporarySiteSettings', Immutable.Map()) - if (data.settings && data.settings[settings.CHECK_DEFAULT_ON_STARTUP] === true) { + if (immutableData.getIn(['settings', settings.CHECK_DEFAULT_ON_STARTUP]) === true) { // Delete defaultBrowserCheckComplete state since this is checked on startup - delete data.defaultBrowserCheckComplete + immutableData = immutableData.delete('defaultBrowserCheckComplete') } // Delete Recovery status on shut down try { - delete data.ui.about.preferences.recoverySucceeded + immutableData = immutableData.deleteIn(['ui', 'about', 'preferences', 'recoverySucceeded']) } catch (e) {} - if (data.perWindowState) { - data.perWindowState.forEach((perWindowState) => - module.exports.cleanPerWindowData(perWindowState, isShutdown)) + const perWindowStateList = immutableData.get('perWindowState') + if (perWindowStateList) { + perWindowStateList.forEach((immutablePerWindowState, i) => { + const cleanedImmutablePerWindowState = module.exports.cleanPerWindowData(immutablePerWindowState, isShutdown) + immutableData = immutableData.setIn(['perWindowState', i], cleanedImmutablePerWindowState) + }) } const clearAutocompleteData = isShutdown && getSetting(settings.SHUTDOWN_CLEAR_AUTOCOMPLETE_DATA) === true if (clearAutocompleteData) { @@ -276,7 +300,7 @@ module.exports.cleanAppData = (data, isShutdown) => { if (clearAutofillData) { autofill.clearAutofillData() const date = new Date().getTime() - data.autofill = { + immutableData = immutableData.set('autofill', Immutable.fromJS({ addresses: { guid: [], timestamp: date @@ -285,94 +309,93 @@ module.exports.cleanAppData = (data, isShutdown) => { guid: [], timestamp: date } - } - } - if (data.dragData) { - delete data.dragData + })) } - if (data.sync) { + immutableData = immutableData.delete('dragData') + + if (immutableData.get('sync')) { // clear sync site cache - data.sync.objectsById = {} + immutableData = immutableData.deleteIn(['sync', 'objectsById'], Immutable.Map()) } const clearSiteSettings = isShutdown && getSetting(settings.SHUTDOWN_CLEAR_SITE_SETTINGS) === true if (clearSiteSettings) { - data.siteSettings = {} + immutableData = immutableData.set('siteSettings', Immutable.Map()) } // Delete expired Flash and NoScript allow-once approvals let now = Date.now() - for (var host in data.siteSettings) { - let expireTime = data.siteSettings[host].flash + + immutableData.get('siteSettings').forEach((value, host) => { + let expireTime = value.get('flash') if (typeof expireTime === 'number' && expireTime < now) { - delete data.siteSettings[host].flash + immutableData = immutableData.deleteIn(['siteSettings', host, 'flash']) } - let noScript = data.siteSettings[host].noScript + let noScript = immutableData.getIn(['siteSettings', host, 'noScript']) if (typeof noScript === 'number') { - delete data.siteSettings[host].noScript + immutableData = immutableData.deleteIn(['siteSettings', host, 'noScript']) } // Don't persist any noScript exceptions - delete data.siteSettings[host].noScriptExceptions + immutableData = immutableData.deleteIn(['siteSettings', host, 'noScriptExceptions']) // Don't write runInsecureContent to session - delete data.siteSettings[host].runInsecureContent + immutableData = immutableData.deleteIn(['siteSettings', host, 'runInsecureContent']) // If the site setting is empty, delete it for privacy - if (Object.keys(data.siteSettings[host]).length === 0) { - delete data.siteSettings[host] + if (Array.from(immutableData.getIn(['siteSettings', host]).keys()).length) { + immutableData = immutableData.deleteIn('siteSettings', host) } - } - if (data.sites) { + }) + + if (immutableData.get('sites')) { const clearHistory = isShutdown && getSetting(settings.SHUTDOWN_CLEAR_HISTORY) === true if (clearHistory) { - data.historySites = {} - if (data.about) { - delete data.about.history - delete data.about.newtab - } + immutableData = immutableData.set('historySites', Immutable.Map()) + immutableData = deleteImmutablePaths(immutableData, + ['data', 'history'], + ['data', 'newtab']) } } - if (data.downloads) { + + if (immutableData.get('downloads')) { const clearDownloads = isShutdown && getSetting(settings.SHUTDOWN_CLEAR_DOWNLOADS) === true if (clearDownloads) { - delete data.downloads + immutableData = immutableData.delete('downloads') } else { // Always at least delete downloaded items older than a week const dateOffset = 7 * 24 * 60 * 60 * 1000 const lastWeek = new Date().getTime() - dateOffset - Object.keys(data.downloads).forEach((downloadId) => { - if (data.downloads[downloadId].startTime < lastWeek) { - delete data.downloads[downloadId] + Array.from(immutableData.get('downloads').keys()).forEach((downloadId) => { + if (immutableData.getIn(['downloads', downloadId, 'startTime']) < lastWeek) { + immutableData = immutableData.deleteIn(['downloads', downloadId]) } else { - const state = data.downloads[downloadId].state + const state = immutableData.getIn(['downloads', downloadId, 'state']) if (state === downloadStates.IN_PROGRESS || state === downloadStates.PAUSED) { - data.downloads[downloadId].state = downloadStates.INTERRUPTED + immutableData = immutableData.setIn(['downloads', downloadId, 'state'], downloadStates.INTERRUPTED) } } }) } } - if (data.menu) { - delete data.menu - } + immutableData = immutableData.delete('menu') try { - data = tabState.getPersistentState(data).toJS() + immutableData = tabState.getPersistentState(immutableData) } catch (e) { - delete data.tabs + immutableData = immutableData.delete('tabs') console.log('cleanAppData: error calling tabState.getPersistentState: ', e) } try { - data = windowState.getPersistentState(data).toJS() + immutableData = windowState.getPersistentState(immutableData) } catch (e) { - delete data.windows + immutableData = immutableData.delete('windows') console.log('cleanAppData: error calling windowState.getPersistentState: ', e) } - if (data.extensions) { - Object.keys(data.extensions).forEach((extensionId) => { - delete data.extensions[extensionId].tabs + if (immutableData.get('extensions')) { + Array.from(immutableData.get('extensions').keys()).forEach((extensionId) => { + immutableData = immutableData.delete('extensions', extensionId, 'tabs') }) } - return data + return immutableData } /** @@ -411,7 +434,7 @@ const safeGetVersion = (fieldName, getFieldVersion) => { /** * version information (shown on about:brave) */ -const setVersionInformation = (data) => { +const setVersionInformation = (immutableData) => { const versionFields = [ ['Brave', app.getVersion], ['rev', Channel.browserLaptopRev], @@ -431,11 +454,11 @@ const setVersionInformation = (data) => { versionInformation[versionField.name] = versionField.version }) - data.about = data.about || {} - data.about.brave = { - versionInformation: versionInformation + if (!immutableData.get('about')) { + immutableData = immutableData.set('about', Immutable.Map()) } - return data + immutableData = immutableData.setIn(['about', 'brave', 'versionInformation'], Immutable.fromJS(versionInformation)) + return immutableData } const sortBookmarkOrder = (bookmarkOrder) => { @@ -674,8 +697,8 @@ module.exports.runPreMigrations = (data) => { return data } -module.exports.runPostMigrations = (data) => { - return data +module.exports.runPostMigrations = (immutableData) => { + return immutableData } module.exports.runImportDefaultSettings = (data) => { @@ -727,22 +750,26 @@ module.exports.loadAppState = () => { if (loaded) { data = module.exports.runPreMigrations(data) - + } + data = module.exports.runImportDefaultSettings(data) + let immutableData = makeImmutable(data) + if (loaded) { // Clean app data here if it wasn't cleared on shutdown - if (data.cleanedOnShutdown !== true || data.lastAppVersion !== app.getVersion()) { - data = module.exports.cleanAppData(data, false) + if (immutableData.get('cleanedOnShutdown') !== true || immutableData.get('lastAppVersion') !== app.getVersion()) { + immutableData = module.exports.cleanAppData(immutableData, false) } - data = Object.assign(module.exports.defaultAppState(), data) - data.cleanedOnShutdown = false + + immutableData = immutableData.merge(module.exports.defaultAppState(), immutableData) + immutableData = immutableData.set('cleanedOnShutdown', false) // Always recalculate the update status - if (data.updates) { - const updateStatus = data.updates.status - delete data.updates.status + if (immutableData.get('updates')) { + const updateStatus = immutableData.getIn(['updates', 'status']) + immutableData = immutableData.deleteIn(['updates', 'status']) // The process always restarts after an update so if the state // indicates that a restart isn't wanted, close right away. if (updateStatus === UpdateStatus.UPDATE_APPLYING_NO_RESTART) { - module.exports.saveAppState(data, true).then(() => { + module.exports.saveAppState(immutableData, true).then(() => { // Exit immediately without doing the session store saving stuff // since we want the same state saved except for the update status app.exit(0) @@ -751,15 +778,14 @@ module.exports.loadAppState = () => { } } - data = module.exports.runPostMigrations(data) - data = module.exports.runImportDefaultSettings(data) + immutableData = module.exports.runPostMigrations(immutableData) } - data = setVersionInformation(data) + immutableData = setVersionInformation(immutableData) - locale.init(data.settings[settings.LANGUAGE]).then((locale) => { + locale.init(immutableData.getIn(['settings', settings.LANGUAGE])).then((locale) => { app.setLocale(locale) - resolve(data) + resolve(immutableData) }) }) } diff --git a/app/sessionStoreShutdown.js b/app/sessionStoreShutdown.js index 763fabd862e..8d274822bb8 100644 --- a/app/sessionStoreShutdown.js +++ b/app/sessionStoreShutdown.js @@ -13,9 +13,11 @@ const async = require('async') const messages = require('../js/constants/messages') const appActions = require('../js/actions/appActions') const platformUtil = require('./common/lib/platformUtil') +const Immutable = require('immutable') +const {makeImmutable} = require('./common/state/immutableUtil') // Used to collect the per window state when shutting down the application -let perWindowState +let immutablePerWindowState let sessionStateStoreComplete let sessionStateStoreCompleteCallback let saveAppStateTimeout @@ -26,13 +28,13 @@ let isAllWindowsClosed let sessionStateSaveInterval // Stores the last window state for each requested window in case a hung window happens, // we'll at least have the last known window state. -let windowStateCache +let immutableWindowStateCache let sessionStoreQueue let appStore // Useful for automated tests const reset = () => { - perWindowState = [] + immutablePerWindowState = Immutable.List() sessionStateStoreComplete = false if (saveAppStateTimeout) { clearTimeout(saveAppStateTimeout) @@ -43,7 +45,7 @@ const reset = () => { lastWindowThatWasClosedState = undefined isAllWindowsClosed = false sessionStateSaveInterval = null - windowStateCache = {} + immutableWindowStateCache = Immutable.Map() if (sessionStateStoreCompleteCallback) { sessionStateStoreCompleteCallback() } @@ -73,10 +75,8 @@ const saveAppState = (forceSave = false) => { app.exit(0) } - const appState = appStore.getState().toJS() - appState.perWindowState = perWindowState - - const receivedAllWindows = perWindowState.length === BrowserWindow.getAllWindows().length + let immutableAppState = appStore.getState().set('perWindowState', immutablePerWindowState) + const receivedAllWindows = immutablePerWindowState.size === BrowserWindow.getAllWindows().length if (receivedAllWindows) { clearTimeout(saveAppStateTimeout) } @@ -85,7 +85,7 @@ const saveAppState = (forceSave = false) => { return } - return sessionStore.saveAppState(appState, shuttingDown).catch((e) => { + return sessionStore.saveAppState(immutableAppState, shuttingDown).catch((e) => { logSaveAppStateError(e) }).then(() => { if (receivedAllWindows || forceSave) { @@ -96,21 +96,21 @@ const saveAppState = (forceSave = false) => { if (shuttingDown) { // If the status is still UPDATE_AVAILABLE then the user wants to quit // and not restart - if (appState.updates && (appState.updates.status === updateStatus.UPDATE_AVAILABLE || - appState.updates.status === updateStatus.UPDATE_AVAILABLE_DEFERRED)) { + if (immutableAppState.get('updates') && (immutableAppState.getIn(['updates', 'status']) === updateStatus.UPDATE_AVAILABLE || + immutableAppState.getIn(['updates', 'status']) === updateStatus.UPDATE_AVAILABLE_DEFERRED)) { // In this case on win32, the process doesn't try to auto restart, so avoid the user // having to open the app twice. Maybe squirrel detects the app is already shutting down. if (platformUtil.isWindows()) { - appState.updates.status = updateStatus.UPDATE_APPLYING_RESTART + immutableAppState.setIn(['updates', 'status'], updateStatus.UPDATE_APPLYING_RESTART) } else { - appState.updates.status = updateStatus.UPDATE_APPLYING_NO_RESTART + immutableAppState.setIn(['updates', 'status'], updateStatus.UPDATE_APPLYING_NO_RESTART) } } // If there's an update to apply, then do it here. // Otherwise just quit. - if (appState.updates && (appState.updates.status === updateStatus.UPDATE_APPLYING_NO_RESTART || - appState.updates.status === updateStatus.UPDATE_APPLYING_RESTART)) { + if (immutableAppState.get('updates') && (immutableAppState.getIn(['updates', 'status']) === updateStatus.UPDATE_APPLYING_NO_RESTART || + immutableAppState.getIn(['updates', 'status']) === updateStatus.UPDATE_APPLYING_RESTART)) { updater.quitAndInstall() } else { app.quit() @@ -132,11 +132,11 @@ const initiateSessionStateSave = () => { sessionStoreQueue.push((cb) => { sessionStateStoreComplete = false sessionStateStoreCompleteCallback = cb - perWindowState.length = 0 + immutablePerWindowState = Immutable.List() // quit triggered by window-all-closed should save last window state if (isAllWindowsClosed && lastWindowThatWasClosedState) { - perWindowState.push(lastWindowThatWasClosedState) + immutablePerWindowState = immutablePerWindowState.push(lastWindowThatWasClosedState) saveAppState(true) } else if (BrowserWindow.getAllWindows().length > 0) { ++windowCloseRequestId @@ -148,9 +148,9 @@ const initiateSessionStateSave = () => { // In this case just save session store for the windows that we have already. saveAppStateTimeout = setTimeout(() => { // Rewrite perwindowstate here - perWindowState = windowIds - .filter((windowId) => windowStateCache[windowId]) - .map((windowId) => windowStateCache[windowId]) + immutablePerWindowState = Immutable.fromJS(windowIds + .filter((windowId) => immutableWindowStateCache.get('windowId')) + .map((windowId) => immutableWindowStateCache.get('windowId'))) saveAppState(true) }, appConfig.quitTimeout) } else { @@ -163,11 +163,11 @@ const removeWindowFromCache = (windowId) => { if (shuttingDown) { return } - delete windowStateCache[windowId] + immutableWindowStateCache = immutableWindowStateCache.delete('windowId') } -const initWindowCacheState = (windowId, windowState) => { - windowStateCache[windowId] = Object.assign({}, windowState) +const initWindowCacheState = (windowId, immutableWindowState) => { + immutableWindowStateCache = immutableWindowStateCache.set('windowId', immutableWindowState) } app.on('before-quit', (e) => { @@ -199,14 +199,15 @@ const startSessionSaveInterval = () => { // User initiated exit using File->Quit ipcMain.on(messages.RESPONSE_WINDOW_STATE, (evt, data, id) => { + const immutableWindowState = makeImmutable(data) const senderWindowId = evt.sender.getOwnerBrowserWindow().id if (id !== windowCloseRequestId) { return } if (data) { - perWindowState.push(data) - windowStateCache[senderWindowId] = data + immutablePerWindowState = immutablePerWindowState.push(immutableWindowState) + immutableWindowStateCache = immutableWindowStateCache.set(senderWindowId, immutableWindowState) } saveAppState() }) diff --git a/js/constants/appConfig.js b/js/constants/appConfig.js index 6c8dc164ac9..d643cfc1387 100644 --- a/js/constants/appConfig.js +++ b/js/constants/appConfig.js @@ -18,7 +18,7 @@ module.exports = { name: 'Brave', contactUrl: 'mailto:support+laptop@brave.com', quitTimeout: isTest ? 3 * 1000 : 10 * 1000, - sessionSaveInterval: 1000 * 60 * 5, + sessionSaveInterval: 10000, resourceNames: { ADBLOCK: 'adblock', SAFE_BROWSING: 'safeBrowsing', diff --git a/js/stores/appStore.js b/js/stores/appStore.js index 865b95376b3..345b8d87e01 100644 --- a/js/stores/appStore.js +++ b/js/stores/appStore.js @@ -39,7 +39,7 @@ const {zoomLevel} = require('../../app/common/constants/toolbarUserInterfaceScal const {initWindowCacheState} = require('../../app/sessionStoreShutdown') // state helpers -const {makeImmutable} = require('../../app/common/state/immutableUtil') +const {isImmutable, makeImmutable} = require('../../app/common/state/immutableUtil') const basicAuthState = require('../../app/common/state/basicAuthState') const extensionState = require('../../app/common/state/extensionState') const aboutNewTabState = require('../../app/common/state/aboutNewTabState') @@ -76,10 +76,11 @@ const navbarHeight = () => { /** * Determine window dimensions (width / height) */ -const setWindowDimensions = (browserOpts, defaults, windowState) => { - if (windowState.ui && windowState.ui.size) { - browserOpts.width = firstDefinedValue(browserOpts.width, windowState.ui.size[0]) - browserOpts.height = firstDefinedValue(browserOpts.height, windowState.ui.size[1]) +const setWindowDimensions = (browserOpts, defaults, immutableWindowState) => { + assert(isImmutable(immutableWindowState)) + if (immutableWindowState.getIn(['ui', 'size'])) { + browserOpts.width = firstDefinedValue(browserOpts.width, immutableWindowState.getIn(['ui', 'size', 0])) + browserOpts.height = firstDefinedValue(browserOpts.height, immutableWindowState.getIn(['ui', 'size', 1])) } browserOpts.width = firstDefinedValue(browserOpts.width, browserOpts.innerWidth, defaults.width) // height and innerHeight are the frame webview size @@ -97,15 +98,15 @@ const setWindowDimensions = (browserOpts, defaults, windowState) => { /** * Determine window position (x / y) */ -const setWindowPosition = (browserOpts, defaults, windowState) => { +const setWindowPosition = (browserOpts, defaults, immutableWindowState) => { if (browserOpts.positionByMouseCursor) { const screenPos = electron.screen.getCursorScreenPoint() browserOpts.x = screenPos.x browserOpts.y = screenPos.y - } else if (windowState.ui && windowState.ui.position) { + } else if (immutableWindowState.getIn(['ui', 'position'])) { // Position comes from window state - browserOpts.x = firstDefinedValue(browserOpts.x, windowState.ui.position[0]) - browserOpts.y = firstDefinedValue(browserOpts.y, windowState.ui.position[1]) + browserOpts.x = firstDefinedValue(browserOpts.x, immutableWindowState.getIn(['ui', 'position', 0])) + browserOpts.y = firstDefinedValue(browserOpts.y, immutableWindowState.getIn(['ui', 'position', 1])) } else if (typeof defaults.x === 'number' && typeof defaults.y === 'number') { // Position comes from the default position browserOpts.x = firstDefinedValue(browserOpts.x, defaults.x) @@ -121,11 +122,11 @@ const setWindowPosition = (browserOpts, defaults, windowState) => { const createWindow = (action) => { const frameOpts = (action.frameOpts && action.frameOpts.toJS()) || {} let browserOpts = (action.browserOpts && action.browserOpts.toJS()) || {} - const windowState = action.restoredState || {} + const immutableWindowState = action.restoredState || Immutable.Map() const defaults = windowDefaults() - browserOpts = setWindowDimensions(browserOpts, defaults, windowState) - browserOpts = setWindowPosition(browserOpts, defaults, windowState) + browserOpts = setWindowDimensions(browserOpts, defaults, immutableWindowState) + browserOpts = setWindowPosition(browserOpts, defaults, immutableWindowState) delete browserOpts.left delete browserOpts.top @@ -203,39 +204,40 @@ const createWindow = (action) => { setImmediate(() => { let mainWindow = new BrowserWindow(Object.assign(windowProps, browserOpts, {disposition: frameOpts.disposition})) - initWindowCacheState(mainWindow.id, action.restoredState) + let restoredImmutableWindowState = action.restoredState + initWindowCacheState(mainWindow.id, restoredImmutableWindowState) // initialize frames state - let frames = [] - if (action.restoredState) { - frames = action.restoredState.frames - action.restoredState.frames = [] - action.restoredState.tabs = [] + let frames = Immutable.List() + if (restoredImmutableWindowState) { + frames = restoredImmutableWindowState.get('frames') + restoredImmutableWindowState = restoredImmutableWindowState.set('frames', Immutable.List()) + restoredImmutableWindowState = restoredImmutableWindowState.set('tabs', Immutable.List()) } else { if (frameOpts && Object.keys(frameOpts).length > 0) { if (frameOpts.forEach) { - frames = frameOpts + frames = Immutable.toJS(frameOpts) } else { frames.push(frameOpts) } } else if (startupSetting === 'homePage' && homepageSetting) { - frames = homepageSetting.split('|').map((homepage) => { + frames = Immutable.fromJS(homepageSetting.split('|').map((homepage) => { return { location: homepage } - }) + })) } } - if (frames.length === 0) { - frames = [{}] + if (frames.size === 0) { + frames = Immutable.fromJS([{}]) } - if (windowState.ui && windowState.ui.isMaximized) { + if (immutableWindowState.getIn(['ui', 'isMaximized'])) { mainWindow.maximize() } - if (windowState.ui && windowState.ui.isFullScreen) { + if (immutableWindowState.getIn(['ui', 'isFullScreen'])) { mainWindow.setFullScreen(true) } @@ -249,8 +251,8 @@ const createWindow = (action) => { id: mainWindow.id }, appState: appState.toJS(), - frames, - windowState: action.restoredState}) + frames: frames.toJS(), + windowState: restoredImmutableWindowState.toJS()}) e.sender.sendShared(messages.INITIALIZE_WINDOW, mem) if (action.cb) { @@ -271,11 +273,6 @@ class AppStore extends EventEmitter { return appState } - emitFullWindowState (wnd) { - wnd.webContents.send(messages.APP_STATE_CHANGE, { state: appState.toJS() }) - lastEmittedState = appState - } - emitChanges (emitFullState) { if (lastEmittedState) { const d = diff(lastEmittedState, appState) diff --git a/test/unit/app/sessionStoreShutdownTest.js b/test/unit/app/sessionStoreShutdownTest.js index fe6c2073658..e2fde54dd6b 100644 --- a/test/unit/app/sessionStoreShutdownTest.js +++ b/test/unit/app/sessionStoreShutdownTest.js @@ -114,15 +114,15 @@ describe('sessionStoreShutdown unit tests', function () { }) it('works for first closed window', function () { - const windowState = { a: 1 } + const windowState = Immutable.fromJS({ a: 1 }) fakeElectron.ipcMain.send(messages.LAST_WINDOW_STATE, {}, windowState) process.emit(messages.UNDO_CLOSED_WINDOW) assert(this.newWindowStub.calledOnce) assert.deepEqual(this.newWindowStub.getCall(0).args[2], windowState) }) it('works for subsequent windows', function () { - const windowState1 = { b: 1 } - const windowState2 = { x: 2 } + const windowState1 = Immutable.fromJS({ b: 1 }) + const windowState2 = Immutable.fromJS({ x: 2 }) fakeElectron.ipcMain.send(messages.LAST_WINDOW_STATE, {}, windowState1) fakeElectron.ipcMain.send(messages.LAST_WINDOW_STATE, {}, windowState2) process.emit(messages.UNDO_CLOSED_WINDOW) @@ -165,7 +165,7 @@ describe('sessionStoreShutdown unit tests', function () { const saveAppStateStub = sinon.stub(sessionStore, 'saveAppState', (state) => { assert.equal(saveAppStateStub.calledOnce, true) saveAppStateStub.restore() - assert.equal(state.perWindowState.length, 0) + assert.equal(state.get('perWindowState').size, 0) cb() return Promise.resolve() }) @@ -175,14 +175,14 @@ describe('sessionStoreShutdown unit tests', function () { }) it('remembers last closed window with no windows (Win32)', function (cb) { isWindows = true - const windowState = { a: 1 } + const windowState = Immutable.fromJS({ a: 1 }) fakeElectron.ipcMain.send(messages.LAST_WINDOW_STATE, {}, windowState) fakeElectron.app.emit('window-all-closed') const saveAppStateStub = sinon.stub(sessionStore, 'saveAppState', (state) => { isWindows = false assert.equal(saveAppStateStub.calledOnce, true) saveAppStateStub.restore() - assert.equal(state.perWindowState.length, 1) + assert.equal(state.get('perWindowState').size, 1) cb() return Promise.resolve() }) @@ -191,13 +191,13 @@ describe('sessionStoreShutdown unit tests', function () { this.clock.tick(1) }) it('remembers last closed window with no windows (Linux)', function (cb) { - const windowState = { a: 1 } + const windowState = Immutable.fromJS({ a: 1 }) fakeElectron.ipcMain.send(messages.LAST_WINDOW_STATE, {}, windowState) fakeElectron.app.emit('window-all-closed') const saveAppStateStub = sinon.stub(sessionStore, 'saveAppState', (state) => { assert.equal(saveAppStateStub.calledOnce, true) saveAppStateStub.restore() - assert.equal(state.perWindowState.length, 1) + assert.equal(state.get('perWindowState').size, 1) cb() return Promise.resolve() }) @@ -207,14 +207,14 @@ describe('sessionStoreShutdown unit tests', function () { }) it('remembers last closed window with no windows (macOS)', function (cb) { isDarwin = true - const windowState = { a: 1 } + const windowState = Immutable.fromJS({ a: 1 }) fakeElectron.ipcMain.send(messages.LAST_WINDOW_STATE, {}, windowState) fakeElectron.app.emit('window-all-closed') const saveAppStateStub = sinon.stub(sessionStore, 'saveAppState', (state) => { isDarwin = false assert.equal(saveAppStateStub.calledOnce, true) saveAppStateStub.restore() - assert.equal(state.perWindowState.length, 0) + assert.equal(state.get('perWindowState').size, 0) cb() return Promise.resolve() }) @@ -247,7 +247,7 @@ describe('sessionStoreShutdown unit tests', function () { const saveAppStateStub = sinon.stub(sessionStore, 'saveAppState', (state) => { assert.equal(saveAppStateStub.calledOnce, true) saveAppStateStub.restore() - assert.equal(state.perWindowState.length, 1) + assert.equal(state.get('perWindowState').size, 1) cb() return Promise.resolve() }) @@ -259,7 +259,7 @@ describe('sessionStoreShutdown unit tests', function () { const saveAppStateStub = sinon.stub(sessionStore, 'saveAppState', (state) => { assert.equal(saveAppStateStub.calledOnce, true) saveAppStateStub.restore() - assert.deepEqual(state.perWindowState, [{a: 1}]) + assert.deepEqual(state.get('perWindowState').toJS(), [{a: 1}]) cb() return Promise.resolve() }) @@ -307,7 +307,7 @@ describe('sessionStoreShutdown unit tests', function () { const saveAppStateStub = sinon.stub(sessionStore, 'saveAppState', (state) => { assert.equal(saveAppStateStub.calledOnce, true) saveAppStateStub.restore() - assert.equal(state.perWindowState.length, 3) + assert.equal(state.get('perWindowState').size, 3) cb() return Promise.resolve() }) @@ -319,7 +319,7 @@ describe('sessionStoreShutdown unit tests', function () { const saveAppStateStub = sinon.stub(sessionStore, 'saveAppState', (state) => { assert.equal(saveAppStateStub.calledOnce, true) saveAppStateStub.restore() - assert.deepEqual(state.perWindowState, [{a: 1}, {b: 2}, {c: 3}]) + assert.deepEqual(state.get('perWindowState').toJS(), [{a: 1}, {b: 2}, {c: 3}]) cb() return Promise.resolve() }) @@ -334,7 +334,7 @@ describe('sessionStoreShutdown unit tests', function () { const saveAppStateStub = sinon.stub(sessionStore, 'saveAppState', (state) => { assert.equal(saveAppStateStub.called, true) saveAppStateStub.restore() - assert.deepEqual(state.perWindowState, [{a: 5}, {b: 2}]) + assert.deepEqual(state.get('perWindowState').toJS(), [{a: 5}, {b: 2}]) cb() return Promise.resolve() })