Bug 1364150 - Introduce debounce middleware; r=bgrins
MozReview-Commit-ID: Hu3rQ3iJHzP
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/debounce.js
@@ -0,0 +1,96 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+/**
+ * Redux middleware for debouncing actions.
+ *
+ * Schedules actions with { meta: { debounce: true } } to be delayed
+ * by wait milliseconds. If another action is fired during this
+ * time-frame both actions are inserted into a queue and delayed.
+ * Maximum delay is defined by maxWait argument.
+ *
+ * Handling more actions at once results in better performance since
+ * components need to be re-rendered less often.
+ *
+ * @param string wait Wait for specified amount of milliseconds
+ * before executing an action. The time is used
+ * to collect more actions and handle them all
+ * at once.
+ * @param string maxWait Max waiting time. It's used in case of
+ * a long stream of actions.
+ */
+function debounceActions(wait, maxWait) {
+ let queuedActions = [];
+
+ return store => next => {
+ let debounced = debounce(() => {
+ next(batchActions(queuedActions));
+ queuedActions = [];
+ }, wait, maxWait);
+
+ return action => {
+ if (!action.meta || !action.meta.debounce) {
+ return next(action);
+ }
+
+ if (!wait || !maxWait) {
+ return next(action);
+ }
+
+ if (action.type == BATCH_ACTIONS) {
+ queuedActions.push(...action.actions);
+ } else {
+ queuedActions.push(action);
+ }
+
+ return debounced();
+ };
+ };
+}
+
+function debounce(cb, wait, maxWait) {
+ let timeout, maxTimeout;
+ let doFunction = () => {
+ clearTimeout(timeout);
+ clearTimeout(maxTimeout);
+ timeout = maxTimeout = null;
+ cb();
+ };
+
+ return () => {
+ return new Promise(resolve => {
+ let onTimeout = () => {
+ doFunction();
+ resolve();
+ };
+
+ clearTimeout(timeout);
+
+ timeout = setTimeout(onTimeout, wait);
+ if (!maxTimeout) {
+ maxTimeout = setTimeout(onTimeout, maxWait);
+ }
+ });
+ };
+}
+
+const BATCH_ACTIONS = Symbol("BATCH_ACTIONS");
+
+/**
+ * Action creator for action-batching.
+ */
+function batchActions(batchedActions, debounceFlag = true) {
+ return {
+ type: BATCH_ACTIONS,
+ meta: { debounce: debounceFlag },
+ actions: batchedActions,
+ };
+}
+
+module.exports = {
+ BATCH_ACTIONS,
+ batchActions,
+ debounceActions,
+};
--- a/devtools/client/shared/redux/middleware/moz.build
+++ b/devtools/client/shared/redux/middleware/moz.build
@@ -1,15 +1,16 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DevToolsModules(
+ 'debounce.js',
'history.js',
'log.js',
'promise.js',
'task.js',
'thunk.js',
'wait-service.js',
)
deleted file mode 100644
--- a/devtools/client/webconsole/new-console-output/actions/enhancers.js
+++ /dev/null
@@ -1,20 +0,0 @@
-/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
-/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-const { BATCH_ACTIONS } = require("../constants");
-
-function batchActions(batchedActions) {
- return {
- type: BATCH_ACTIONS,
- actions: batchedActions,
- };
-}
-
-module.exports = {
- batchActions
-};
--- a/devtools/client/webconsole/new-console-output/actions/index.js
+++ b/devtools/client/webconsole/new-console-output/actions/index.js
@@ -2,17 +2,16 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const actionModules = [
- require("./enhancers"),
require("./filters"),
require("./messages"),
require("./ui"),
];
const actions = Object.assign({}, ...actionModules);
module.exports = actions;
--- a/devtools/client/webconsole/new-console-output/actions/messages.js
+++ b/devtools/client/webconsole/new-console-output/actions/messages.js
@@ -5,17 +5,17 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {
prepareMessage
} = require("devtools/client/webconsole/new-console-output/utils/messages");
const { IdGenerator } = require("devtools/client/webconsole/new-console-output/utils/id-generator");
-const { batchActions } = require("devtools/client/webconsole/new-console-output/actions/enhancers");
+const { batchActions } = require("devtools/client/shared/redux/middleware/debounce");
const {
MESSAGE_ADD,
NETWORK_MESSAGE_UPDATE,
MESSAGES_CLEAR,
MESSAGE_OPEN,
MESSAGE_CLOSE,
MESSAGE_TYPE,
MESSAGE_TABLE_RECEIVE,
--- a/devtools/client/webconsole/new-console-output/actions/moz.build
+++ b/devtools/client/webconsole/new-console-output/actions/moz.build
@@ -1,12 +1,11 @@
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DevToolsModules(
- 'enhancers.js',
'filters.js',
'index.js',
'messages.js',
'ui.js',
)
--- a/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
+++ b/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
@@ -4,16 +4,17 @@
"use strict";
// React & Redux
const React = require("devtools/client/shared/vendor/react");
const ReactDOM = require("devtools/client/shared/vendor/react-dom");
const { Provider } = require("devtools/client/shared/vendor/react-redux");
const actions = require("devtools/client/webconsole/new-console-output/actions/index");
+const { batchActions } = require("devtools/client/shared/redux/middleware/debounce");
const { createContextMenu } = require("devtools/client/webconsole/new-console-output/utils/context-menu");
const { configureStore } = require("devtools/client/webconsole/new-console-output/store");
const EventEmitter = require("devtools/shared/event-emitter");
const ConsoleOutput = React.createFactory(require("devtools/client/webconsole/new-console-output/components/console-output"));
const FilterBar = React.createFactory(require("devtools/client/webconsole/new-console-output/components/filter-bar"));
let store = null;
@@ -167,17 +168,17 @@ NewConsoleOutputWrapper.prototype = {
});
}
return Promise.resolve();
},
dispatchMessagesAdd: function (messages) {
const batchedActions = messages.map(message => actions.messageAdd(message));
- store.dispatch(actions.batchActions(batchedActions));
+ store.dispatch(batchActions(batchedActions));
},
dispatchMessagesClear: function () {
store.dispatch(actions.messagesClear());
},
dispatchTimestampsToggle: function (enabled) {
store.dispatch(actions.timestampsToggle(enabled));
@@ -199,17 +200,17 @@ NewConsoleOutputWrapper.prototype = {
return store;
}
};
function batchedMessageAdd(action) {
queuedActions.push(action);
if (!throttledDispatchTimeout) {
throttledDispatchTimeout = setTimeout(() => {
- store.dispatch(actions.batchActions(queuedActions));
+ store.dispatch(batchActions(queuedActions));
queuedActions = [];
throttledDispatchTimeout = null;
}, 50);
}
}
// Exports from this module
module.exports = NewConsoleOutputWrapper;
--- a/devtools/client/webconsole/new-console-output/store.js
+++ b/devtools/client/webconsole/new-console-output/store.js
@@ -8,20 +8,22 @@ const {PrefState} = require("devtools/cl
const {UiState} = require("devtools/client/webconsole/new-console-output/reducers/ui");
const {
applyMiddleware,
compose,
createStore
} = require("devtools/client/shared/vendor/redux");
const { thunk } = require("devtools/client/shared/redux/middleware/thunk");
const {
+ BATCH_ACTIONS
+} = require("devtools/client/shared/redux/middleware/debounce");
+const {
MESSAGE_ADD,
MESSAGES_CLEAR,
REMOVED_MESSAGES_CLEAR,
- BATCH_ACTIONS,
PREFS,
} = require("devtools/client/webconsole/new-console-output/constants");
const { reducers } = require("./reducers/index");
const Services = require("Services");
function configureStore(hud, options = {}) {
const logLimit = options.logLimit
|| Math.max(Services.prefs.getIntPref("devtools.hud.loglimit"), 1);
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/middleware/debounce.test.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const expect = require("expect");
+
+const {
+ debounceActions,
+} = require("devtools/client/shared/redux/middleware/debounce");
+
+describe("Debounce Middleware", () => {
+ let nextArgs = [];
+ const fakeStore = {};
+ const fakeNext = (...args) => {
+ nextArgs.push(args);
+ };
+
+ beforeEach(() => {
+ nextArgs = [];
+ });
+
+ it("should pass the intercepted action to next", () => {
+ const fakeAction = {
+ type: "FAKE_ACTION"
+ };
+
+ debounceActions()(fakeStore)(fakeNext)(fakeAction);
+
+ expect(nextArgs.length).toEqual(1);
+ expect(nextArgs[0]).toEqual([fakeAction]);
+ });
+
+ it("should debounce if specified", () => {
+ const fakeAction = {
+ type: "FAKE_ACTION",
+ meta: {
+ debounce: true
+ }
+ };
+
+ const executed = debounceActions(1, 1)(fakeStore)(fakeNext)(fakeAction);
+ expect(nextArgs.length).toEqual(0);
+
+ return executed.then(() => {
+ expect(nextArgs.length).toEqual(1);
+ });
+ });
+
+ it("should have no effect if no timeout", () => {
+ const fakeAction = {
+ type: "FAKE_ACTION",
+ meta: {
+ debounce: true
+ }
+ };
+
+ debounceActions()(fakeStore)(fakeNext)(fakeAction);
+ expect(nextArgs.length).toEqual(1);
+ expect(nextArgs[0]).toEqual([fakeAction]);
+ });
+});