diff --git a/js/actions/appActions.js b/js/actions/appActions.js index daa56e7c69c..24d2a147cf0 100644 --- a/js/actions/appActions.js +++ b/js/actions/appActions.js @@ -7,6 +7,27 @@ const AppDispatcher = require('../dispatcher/appDispatcher') const AppConstants = require('../constants/appConstants') const appActions = { + /** + * Calls the supplied callback function when dispatching is complete for all stores. This + * method does not wait for UI or other updates that are triggered as a result of the + * state changes triggered by the action + * + * Usage: appActions.onDispatchComplete(fn).newWindow(...) + * + * @param {function} cb - Callback + */ + onDispatchComplete: function (cb) { + let handler = { + get (target, propKey, receiver) { + const origMethod = target[propKey] + return function (...args) { + return AppDispatcher.notifyOnDispatchComplete(origMethod.bind(this, ...args), cb) + } + } + } + return new Proxy(this, handler) + }, + /** * Dispatches an event to the main process to replace the app state * This is called from the main process on startup before anything else diff --git a/js/components/braveryPanel.js b/js/components/braveryPanel.js index 06bf0a25492..04116615f53 100644 --- a/js/components/braveryPanel.js +++ b/js/components/braveryPanel.js @@ -144,8 +144,9 @@ class BraveryPanel extends ImmutableComponent { if (setting !== 'noScript' && (parsedUrl.protocol === 'https:' || parsedUrl.protocol === 'http:')) { ruleKey = `https?://${parsedUrl.host}` } - appActions.changeSiteSetting(ruleKey, setting, e.target.value) - this.onReload() + appActions.onDispatchComplete(() => { + this.onReload() + }).changeSiteSetting(ruleKey, setting, e.target.value) } get displayHost () { const parsedUrl = urlParse(this.props.activeRequestedLocation) diff --git a/js/dispatcher/appDispatcher.js b/js/dispatcher/appDispatcher.js index c11dfc63692..2ef18b4b98b 100644 --- a/js/dispatcher/appDispatcher.js +++ b/js/dispatcher/appDispatcher.js @@ -5,13 +5,23 @@ const Serializer = require('./serializer') const messages = require('../constants/messages') const electron = process.type === 'renderer' ? global.require('electron') : require('electron') +const uuid = require('uuid').v4 + 'use strict' +const serializePayload = (payload, dispatchId = null) => { + if (dispatchId && !payload.dispatchId) { + payload.dispatchId = dispatchId + } + return Serializer.serialize(payload) +} + class AppDispatcher { constructor () { - this.callbacks = [] - this.promises = [] + this.callbacks = {} + this.promises = {} + this.notifyOnDispatchCompleteFn = null } /** @@ -22,20 +32,21 @@ class AppDispatcher { * @param {function} callback The callback to be registered. * @return {number} The index of the callback within the _callbacks array. */ - register (callback) { + register (callback, token = uuid()) { if (process.type === 'renderer') { const ipc = electron.ipcRenderer - ipc.send('app-dispatcher-register') + ipc.send('app-dispatcher-register', token) } - this.callbacks.push(callback) - return this.callbacks.length - 1 // index + this.callbacks[token] = callback + return token } - unregister (callback) { - const index = this.callbacks.indexOf(callback) - if (index !== -1) { - this.callbacks.splice(index, 1) + unregister (token) { + if (process.type === 'renderer') { + const ipc = electron.ipcRenderer + ipc.send('app-dispatcher-unregister', token) } + delete this.callbacks[token] } /** @@ -51,31 +62,61 @@ class AppDispatcher { if (payload.actionType === undefined) { throw new Error('Dispatcher: Undefined action for payload', payload) } - // First create array of promises for callbacks to reference. - const resolves = [] - const rejects = [] - this.promises = this.callbacks.map(function (_, i) { - return new Promise(function (resolve, reject) { - resolves[i] = resolve - rejects[i] = reject + + // First create map of promises for callbacks to reference. + const resolves = {} + const rejects = {} + for (var token in this.callbacks) { + this.promises[token] = new Promise(function (resolve, reject) { + resolves[token] = resolve + rejects[token] = reject }) - }) + } + + let dispatchId = uuid() + + if (this.notifyOnDispatchCompleteFn && process.type === 'renderer') { + let cb = this.notifyOnDispatchCompleteFn + const ipc = electron.ipcRenderer + // wait for all the local handlers + let dispatchedCallback = (evt, dispatchedActionId) => { + // in the renderer process we only have to wait + // for the main process + if (dispatchId === dispatchedActionId) { + this.waitFor(Object.keys(this.promises), cb) + ipc.removeListener('action-dispatched', dispatchedCallback) + } + } + ipc.on('action-dispatched', dispatchedCallback) + } + // Dispatch to callbacks and resolve/reject promises. - this.callbacks.forEach(function (callback, i) { + for (token in this.callbacks) { + let callback = this.callbacks[token] // Callback can return an obj, to resolve, or a promise, to chain. // See waitFor() for why this might be useful. Promise.resolve(callback(payload)).then(function () { - resolves[i](payload) + resolves[token](payload) }, function () { - rejects[i](new Error('Dispatcher callback unsuccessful')) + rejects[token](new Error('Dispatcher callback unsuccessful')) }) - }) - this.promises = [] + } if (process.type === 'renderer') { const ipc = electron.ipcRenderer - ipc.send(messages.DISPATCH_ACTION, Serializer.serialize(payload)) + ipc.send(messages.DISPATCH_ACTION, serializePayload(payload, dispatchId), !!this.notifyOnDispatchCompleteFn) } + + this.promises = {} + + return dispatchId + } + + notifyOnDispatchComplete (fn, cb) { + this.notifyOnDispatchCompleteFn = cb + const returnVal = fn() + this.notifyOnDispatchCompleteFn = null + return returnVal } waitFor (promiseIndexes, callback) { @@ -89,50 +130,58 @@ const appDispatcher = new AppDispatcher() if (process.type === 'browser') { const electron = require('electron') const ipcMain = electron.ipcMain - ipcMain.on('app-dispatcher-register', (event) => { + ipcMain.on('app-dispatcher-unregister', (event, token) => { + appDispatcher.unregister(token) + }) + ipcMain.on('app-dispatcher-register', (event, token) => { let registrant = event.sender const callback = function (payload) { try { if (registrant.isDestroyed()) { - appDispatcher.unregister(callback) + appDispatcher.unregister(token) } else { - registrant.send(messages.DISPATCH_ACTION, Serializer.serialize(payload)) + registrant.send(messages.DISPATCH_ACTION, serializePayload(payload)) } } catch (e) { console.error('unregistering callback', e) - appDispatcher.unregister(callback) + appDispatcher.unregister(token) } } event.sender.on('crashed', () => { - appDispatcher.unregister(callback) + appDispatcher.unregister(token) }) event.sender.on('destroyed', () => { - appDispatcher.unregister(callback) + appDispatcher.unregister(token) }) - appDispatcher.register(callback) + appDispatcher.register(callback, token) }) - ipcMain.on(messages.DISPATCH_ACTION, (event, payload) => { + ipcMain.on(messages.DISPATCH_ACTION, (event, payload, notifyOnDispatchComplete) => { payload = Serializer.deserialize(payload) let queryInfo = payload.queryInfo || payload.frameProps || (payload.queryInfo = {}) queryInfo = queryInfo.toJS ? queryInfo.toJS() : queryInfo + let sender = event.sender if (event.sender.hostWebContents) { + sender = event.sender.hostWebContents // received from an extension // only extension messages will have a hostWebContents - let win = require('electron').BrowserWindow.fromWebContents(event.sender.hostWebContents) + let win = require('electron').BrowserWindow.fromWebContents(sender) // default to the windowId of the hostWebContents queryInfo.windowId = queryInfo.windowId || win.id // add queryInfo if we only had frameProps before payload.queryInfo = queryInfo - appDispatcher.dispatch(payload, event.sender.hostWebContents) + appDispatcher.dispatch(payload, sender) } else { // received from a browser window if (event.sender.id !== queryInfo.windowId) { - appDispatcher.dispatch(payload, event.sender) + appDispatcher.dispatch(payload, sender) } } + if (notifyOnDispatchComplete && payload.dispatchId) { + sender.send('action-dispatched', payload.dispatchId) + } }) }