Bug 1364150 - Introduce debounce middleware; r=bgrins draft
authorBrian Grinstead <bgrinstead@mozilla.com>
Thu, 11 May 2017 12:40:20 -0700
changeset 598030 1f3d8b079a930eca7e4e807d6833e60258ef2bcf
parent 597883 e1e4a481b7e88dce163b9cccc2fb72032023befa
child 634401 e0994db10b538e183f9d739b185bcc2019e00880
push id65122
push userjodvarko@mozilla.com
push dateWed, 21 Jun 2017 10:16:45 +0000
reviewersbgrins
bugs1364150
milestone56.0a1
Bug 1364150 - Introduce debounce middleware; r=bgrins MozReview-Commit-ID: Hu3rQ3iJHzP
devtools/client/shared/redux/middleware/debounce.js
devtools/client/shared/redux/middleware/moz.build
devtools/client/webconsole/new-console-output/actions/enhancers.js
devtools/client/webconsole/new-console-output/actions/index.js
devtools/client/webconsole/new-console-output/actions/messages.js
devtools/client/webconsole/new-console-output/actions/moz.build
devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
devtools/client/webconsole/new-console-output/store.js
devtools/client/webconsole/new-console-output/test/middleware/debounce.test.js
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]);
+  });
+});