summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/redux
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/shared/redux')
-rw-r--r--devtools/client/shared/redux/create-store.js90
-rw-r--r--devtools/client/shared/redux/middleware/debounce.js100
-rw-r--r--devtools/client/shared/redux/middleware/ignore.js38
-rw-r--r--devtools/client/shared/redux/middleware/log.js31
-rw-r--r--devtools/client/shared/redux/middleware/moz.build18
-rw-r--r--devtools/client/shared/redux/middleware/performance-marker.js68
-rw-r--r--devtools/client/shared/redux/middleware/promise.js69
-rw-r--r--devtools/client/shared/redux/middleware/task.js38
-rw-r--r--devtools/client/shared/redux/middleware/thunk.js23
-rw-r--r--devtools/client/shared/redux/middleware/wait-service.js64
-rw-r--r--devtools/client/shared/redux/middleware/xpcshell/.eslintrc.js10
-rw-r--r--devtools/client/shared/redux/middleware/xpcshell/head.js26
-rw-r--r--devtools/client/shared/redux/middleware/xpcshell/test_middleware-task-01.js66
-rw-r--r--devtools/client/shared/redux/middleware/xpcshell/test_middleware-task-02.js86
-rw-r--r--devtools/client/shared/redux/middleware/xpcshell/test_middleware-task-03.js50
-rw-r--r--devtools/client/shared/redux/middleware/xpcshell/xpcshell.toml11
-rw-r--r--devtools/client/shared/redux/moz.build15
-rw-r--r--devtools/client/shared/redux/subscriber.js16
-rw-r--r--devtools/client/shared/redux/visibility-handler-connect.js35
19 files changed, 854 insertions, 0 deletions
diff --git a/devtools/client/shared/redux/create-store.js b/devtools/client/shared/redux/create-store.js
new file mode 100644
index 0000000000..fb348d14dc
--- /dev/null
+++ b/devtools/client/shared/redux/create-store.js
@@ -0,0 +1,90 @@
+/* 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 {
+ combineReducers,
+ createStore,
+ applyMiddleware,
+} = require("resource://devtools/client/shared/vendor/redux.js");
+const {
+ thunk,
+} = require("resource://devtools/client/shared/redux/middleware/thunk.js");
+const {
+ waitUntilService,
+} = require("resource://devtools/client/shared/redux/middleware/wait-service.js");
+const {
+ task,
+} = require("resource://devtools/client/shared/redux/middleware/task.js");
+const {
+ promise,
+} = require("resource://devtools/client/shared/redux/middleware/promise.js");
+const flags = require("resource://devtools/shared/flags.js");
+
+loader.lazyRequireGetter(
+ this,
+ "log",
+ "resource://devtools/client/shared/redux/middleware/log.js",
+ true
+);
+
+/**
+ * This creates a dispatcher with all the standard middleware in place
+ * that all code requires. It can also be optionally configured in
+ * various ways, such as logging and recording.
+ *
+ * @param {object} opts:
+ * - enableTaskMiddleware: if true, include the task middleware
+ * - log: log all dispatched actions to console
+ * - middleware: array of middleware to be included in the redux store
+ * - thunkOptions: object that will be spread within a {dispatch, getState} object,
+ * that will be passed in each thunk action.
+ */
+const createStoreWithMiddleware = (opts = {}) => {
+ const middleware = [];
+ if (opts.enableTaskMiddleware) {
+ middleware.push(task);
+ }
+ middleware.push(
+ thunk(opts.thunkOptions),
+ promise,
+
+ // Order is important: services must go last as they always
+ // operate on "already transformed" actions. Actions going through
+ // them shouldn't have any special fields like promises, they
+ // should just be normal JSON objects.
+ waitUntilService
+ );
+
+ if (opts.middleware) {
+ opts.middleware.forEach(fn => middleware.push(fn));
+ }
+
+ if (opts.log) {
+ middleware.push(log);
+ }
+
+ return applyMiddleware(...middleware)(createStore);
+};
+
+module.exports = (
+ reducers,
+ {
+ shouldLog = false,
+ initialState = undefined,
+ thunkOptions,
+ enableTaskMiddleware = false,
+ } = {}
+) => {
+ const reducer =
+ typeof reducers === "function" ? reducers : combineReducers(reducers);
+
+ const store = createStoreWithMiddleware({
+ enableTaskMiddleware,
+ log: flags.testing && shouldLog,
+ thunkOptions,
+ })(reducer, initialState);
+
+ return store;
+};
diff --git a/devtools/client/shared/redux/middleware/debounce.js b/devtools/client/shared/redux/middleware/debounce.js
new file mode 100644
index 0000000000..fc5625a0fe
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/debounce.js
@@ -0,0 +1,100 @@
+/* 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 => {
+ const 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;
+ const doFunction = () => {
+ clearTimeout(timeout);
+ clearTimeout(maxTimeout);
+ timeout = maxTimeout = null;
+ cb();
+ };
+
+ return () => {
+ return new Promise(resolve => {
+ const 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,
+};
diff --git a/devtools/client/shared/redux/middleware/ignore.js b/devtools/client/shared/redux/middleware/ignore.js
new file mode 100644
index 0000000000..d2e8cd9e24
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/ignore.js
@@ -0,0 +1,38 @@
+/* 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 IGNORING = Symbol("IGNORING");
+const START_IGNORE_ACTION = "START_IGNORE_ACTION";
+
+/**
+ * A middleware that prevents any action of being called once it is activated.
+ * This is useful to apply while destroying a given panel, as it will ignore all calls
+ * to actions, where we usually make our client -> server communications.
+ * This middleware should be declared before any other middleware to to effectively
+ * ignore every actions.
+ */
+function ignore({ getState }) {
+ return next => action => {
+ if (action.type === START_IGNORE_ACTION) {
+ getState()[IGNORING] = true;
+ return null;
+ }
+
+ if (getState()[IGNORING]) {
+ // We only print the action type, and not the whole action object, as it can holds
+ // very complex data that would clutter stdout logs and make them impossible
+ // to parse for treeherder.
+ console.warn("IGNORED REDUX ACTION:", action.type);
+ return null;
+ }
+
+ return next(action);
+ };
+}
+
+module.exports = {
+ ignore,
+ START_IGNORE_ACTION: { type: START_IGNORE_ACTION },
+};
diff --git a/devtools/client/shared/redux/middleware/log.js b/devtools/client/shared/redux/middleware/log.js
new file mode 100644
index 0000000000..4ea09491ca
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/log.js
@@ -0,0 +1,31 @@
+/* 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";
+
+/**
+ * A middleware that logs all actions coming through the system
+ * to the console.
+ */
+function log({ dispatch, getState }) {
+ return next => action => {
+ try {
+ // Only print the action type, rather than printing the whole object
+ console.log("[DISPATCH] action type:", action.type);
+ /*
+ * USE WITH CAUTION!! This will output everything from an action object,
+ * and these can be quite large. Printing out large objects will slow
+ * down tests and cause test failures
+ *
+ * console.log("[DISPATCH]", JSON.stringify(action, null, 2));
+ */
+ } catch (e) {
+ // this occurs if JSON.stringify throws.
+ console.warn(e);
+ console.log("[DISPATCH]", action);
+ }
+ next(action);
+ };
+}
+
+exports.log = log;
diff --git a/devtools/client/shared/redux/middleware/moz.build b/devtools/client/shared/redux/middleware/moz.build
new file mode 100644
index 0000000000..82de1417e6
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/moz.build
@@ -0,0 +1,18 @@
+# -*- 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",
+ "ignore.js",
+ "log.js",
+ "performance-marker.js",
+ "promise.js",
+ "task.js",
+ "thunk.js",
+ "wait-service.js",
+)
+
+XPCSHELL_TESTS_MANIFESTS += ["xpcshell/xpcshell.toml"]
diff --git a/devtools/client/shared/redux/middleware/performance-marker.js b/devtools/client/shared/redux/middleware/performance-marker.js
new file mode 100644
index 0000000000..c829dffe3e
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/performance-marker.js
@@ -0,0 +1,68 @@
+/* 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";
+
+/**
+ * This function returns a middleware, which is responsible for adding markers that will
+ * be visible in performance profiles, and may help investigate performance issues.
+ *
+ * Example usage, adding a marker when console messages are added, and when they are cleared:
+ *
+ * return createPerformanceMarkerMiddleware({
+ * "MESSAGES_ADD": {
+ * label: "WebconsoleAddMessages",
+ * sessionId: 12345,
+ * getMarkerDescription: function({ action, state }) {
+ * const { messages } = action;
+ * const totalMessageCount = state.messages.mutableMessagesById.size;
+ * return `${messages.length} messages handled, store now has ${totalMessageCount} messages`;
+ * },
+ * },
+ * "MESSAGES_CLEARED": {
+ * label: "WebconsoleClearMessages",
+ * sessionId: 12345
+ * },
+ * });
+ *
+ * @param {Object} cases: An object, keyed by action type, that will determine if a
+ * given action will add a marker.
+ * @param {String} cases.{actionType} - The type of the action that will trigger the
+ * marker creation.
+ * @param {String} cases.{actionType}.label - The marker label
+ * @param {Integer} cases.{actionType}.sessionId - The telemetry sessionId. This is used
+ * to be able to distinguish markers coming from different toolboxes.
+ * @param {Function} [cases.{actionType}.getMarkerDescription] - An optional function that
+ * will be called when adding the marker to populate its description. The function
+ * is called with an object holding the action and the state
+ */
+function createPerformanceMarkerMiddleware(cases) {
+ return function (store) {
+ return next => action => {
+ const condition = cases[action.type];
+ const shouldAddProfileMarker = !!condition;
+
+ // Start the marker timer before calling next(action).
+ const startTime = shouldAddProfileMarker ? Cu.now() : null;
+ const newState = next(action);
+
+ if (shouldAddProfileMarker) {
+ ChromeUtils.addProfilerMarker(
+ `${condition.label} ${condition.sessionId}`,
+ startTime,
+ condition.getMarkerDescription
+ ? condition.getMarkerDescription({
+ action,
+ state: store.getState(),
+ })
+ : ""
+ );
+ }
+ return newState;
+ };
+ };
+}
+
+module.exports = {
+ createPerformanceMarkerMiddleware,
+};
diff --git a/devtools/client/shared/redux/middleware/promise.js b/devtools/client/shared/redux/middleware/promise.js
new file mode 100644
index 0000000000..7f88651a61
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/promise.js
@@ -0,0 +1,69 @@
+/* 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";
+
+loader.lazyRequireGetter(
+ this,
+ "generateUUID",
+ "resource://devtools/shared/generate-uuid.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ ["entries", "executeSoon", "toObject"],
+ "resource://devtools/shared/DevToolsUtils.js",
+ true
+);
+
+const PROMISE = (exports.PROMISE = "@@dispatch/promise");
+
+function promiseMiddleware({ dispatch, getState }) {
+ return next => action => {
+ if (!(PROMISE in action)) {
+ return next(action);
+ }
+ // Return the promise so action creators can still compose if they
+ // want to.
+ return new Promise((resolve, reject) => {
+ const promiseInst = action[PROMISE];
+ const seqId = generateUUID().toString();
+
+ // Create a new action that doesn't have the promise field and has
+ // the `seqId` field that represents the sequence id
+ action = Object.assign(
+ toObject(entries(action).filter(pair => pair[0] !== PROMISE)),
+ { seqId }
+ );
+
+ dispatch(Object.assign({}, action, { status: "start" }));
+
+ promiseInst.then(
+ value => {
+ executeSoon(() => {
+ dispatch(
+ Object.assign({}, action, {
+ status: "done",
+ value,
+ })
+ );
+ resolve(value);
+ });
+ },
+ error => {
+ executeSoon(() => {
+ dispatch(
+ Object.assign({}, action, {
+ status: "error",
+ error: error.message || error,
+ })
+ );
+ reject(error);
+ });
+ }
+ );
+ });
+ };
+}
+
+exports.promise = promiseMiddleware;
diff --git a/devtools/client/shared/redux/middleware/task.js b/devtools/client/shared/redux/middleware/task.js
new file mode 100644
index 0000000000..1ffb033589
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/task.js
@@ -0,0 +1,38 @@
+/* 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";
+
+loader.lazyRequireGetter(
+ this,
+ ["executeSoon", "isAsyncFunction", "reportException"],
+ "resource://devtools/shared/DevToolsUtils.js",
+ true
+);
+
+const ERROR_TYPE = (exports.ERROR_TYPE = "@@redux/middleware/task#error");
+
+/**
+ * A middleware that allows async thunks (async functions) to be dispatched.
+ * The middleware is called "task" for historical reasons. TODO: rename?
+ */
+
+function task({ dispatch, getState }) {
+ return next => action => {
+ if (isAsyncFunction(action)) {
+ return action({ dispatch, getState }).catch(
+ handleError.bind(null, dispatch)
+ );
+ }
+ return next(action);
+ };
+}
+
+function handleError(dispatch, error) {
+ executeSoon(() => {
+ reportException(ERROR_TYPE, error);
+ dispatch({ type: ERROR_TYPE, error });
+ });
+}
+
+exports.task = task;
diff --git a/devtools/client/shared/redux/middleware/thunk.js b/devtools/client/shared/redux/middleware/thunk.js
new file mode 100644
index 0000000000..adcca77ede
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/thunk.js
@@ -0,0 +1,23 @@
+/* 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";
+
+/**
+ * A middleware that allows thunks (functions) to be dispatched
+ * If it's a thunk, it is called with an argument that will be an object
+ * containing `dispatch` and `getState` properties, plus any additional properties
+ * defined in the `options` parameters.
+ * This allows the action to create multiple actions (most likely asynchronously).
+ */
+function thunk(options = {}) {
+ return function ({ dispatch, getState }) {
+ return next => action => {
+ return typeof action === "function"
+ ? action({ dispatch, getState, ...options })
+ : next(action);
+ };
+ };
+}
+
+exports.thunk = thunk;
diff --git a/devtools/client/shared/redux/middleware/wait-service.js b/devtools/client/shared/redux/middleware/wait-service.js
new file mode 100644
index 0000000000..9416fd22dd
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/wait-service.js
@@ -0,0 +1,64 @@
+/* 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";
+
+/**
+ * A middleware which acts like a service, because it is stateful
+ * and "long-running" in the background. It provides the ability
+ * for actions to install a function to be run once when a specific
+ * condition is met by an action coming through the system. Think of
+ * it as a thunk that blocks until the condition is met. Example:
+ *
+ * ```js
+ * const services = { WAIT_UNTIL: require('wait-service').NAME };
+ *
+ * { type: services.WAIT_UNTIL,
+ * predicate: action => action.type === constants.ADD_ITEM,
+ * run: (dispatch, getState, action) => {
+ * // Do anything here. You only need to accept the arguments
+ * // if you need them. `action` is the action that satisfied
+ * // the predicate.
+ * }
+ * }
+ * ```
+ */
+const NAME = (exports.NAME = "@@service/waitUntil");
+
+function waitUntilService({ dispatch, getState }) {
+ let pending = [];
+
+ function checkPending(action) {
+ const readyRequests = [];
+ const stillPending = [];
+
+ // Find the pending requests whose predicates are satisfied with
+ // this action. Wait to run the requests until after we update the
+ // pending queue because the request handler may synchronously
+ // dispatch again and run this service (that use case is
+ // completely valid).
+ for (const request of pending) {
+ if (request.predicate(action)) {
+ readyRequests.push(request);
+ } else {
+ stillPending.push(request);
+ }
+ }
+
+ pending = stillPending;
+ for (const request of readyRequests) {
+ request.run(dispatch, getState, action);
+ }
+ }
+
+ return next => action => {
+ if (action.type === NAME) {
+ pending.push(action);
+ return null;
+ }
+ const result = next(action);
+ checkPending(action);
+ return result;
+ };
+}
+exports.waitUntilService = waitUntilService;
diff --git a/devtools/client/shared/redux/middleware/xpcshell/.eslintrc.js b/devtools/client/shared/redux/middleware/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..f69336917a
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/xpcshell/.eslintrc.js
@@ -0,0 +1,10 @@
+/* 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";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ extends: "../../../../../.eslintrc.xpcshell.js",
+};
diff --git a/devtools/client/shared/redux/middleware/xpcshell/head.js b/devtools/client/shared/redux/middleware/xpcshell/head.js
new file mode 100644
index 0000000000..9f4ba4392a
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/xpcshell/head.js
@@ -0,0 +1,26 @@
+/* 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/. */
+
+/* exported waitUntilState */
+
+"use strict";
+
+const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+);
+
+function waitUntilState(store, predicate) {
+ return new Promise(resolve => {
+ const unsubscribe = store.subscribe(check);
+ function check() {
+ if (predicate(store.getState())) {
+ unsubscribe();
+ resolve();
+ }
+ }
+
+ // Fire the check immediately incase the action has already occurred
+ check();
+ });
+}
diff --git a/devtools/client/shared/redux/middleware/xpcshell/test_middleware-task-01.js b/devtools/client/shared/redux/middleware/xpcshell/test_middleware-task-01.js
new file mode 100644
index 0000000000..d8516feecc
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/xpcshell/test_middleware-task-01.js
@@ -0,0 +1,66 @@
+/* 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 {
+ createStore,
+ applyMiddleware,
+} = require("resource://devtools/client/shared/vendor/redux.js");
+const {
+ task,
+} = require("resource://devtools/client/shared/redux/middleware/task.js");
+
+/**
+ * Tests that task middleware allows dispatching generators, promises and objects
+ * that return actions;
+ */
+add_task(async function () {
+ const store = applyMiddleware(task)(createStore)(reducer);
+
+ store.dispatch(fetch1("generator"));
+ await waitUntilState(store, () => store.getState().length === 1);
+ equal(
+ store.getState()[0].data,
+ "generator",
+ "task middleware async dispatches an action via generator"
+ );
+
+ store.dispatch(fetch2("sync"));
+ await waitUntilState(store, () => store.getState().length === 2);
+ equal(
+ store.getState()[1].data,
+ "sync",
+ "task middleware sync dispatches an action via sync"
+ );
+});
+
+function fetch1(data) {
+ return async function ({ dispatch, getState }) {
+ equal(
+ getState().length,
+ 0,
+ "`getState` is accessible in a generator action"
+ );
+ let moreData = await new Promise(resolve => resolve(data));
+ // Ensure it handles more than one yield
+ moreData = await new Promise(resolve => resolve(data));
+ dispatch({ type: "fetch1", data: moreData });
+ };
+}
+
+function fetch2(data) {
+ return {
+ type: "fetch2",
+ data,
+ };
+}
+
+function reducer(state = [], action) {
+ info("Action called: " + action.type);
+ if (["fetch1", "fetch2"].includes(action.type)) {
+ state.push(action);
+ }
+ return [...state];
+}
diff --git a/devtools/client/shared/redux/middleware/xpcshell/test_middleware-task-02.js b/devtools/client/shared/redux/middleware/xpcshell/test_middleware-task-02.js
new file mode 100644
index 0000000000..eaa573a8ae
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/xpcshell/test_middleware-task-02.js
@@ -0,0 +1,86 @@
+/* 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";
+
+/**
+ * Tests that task middleware allows dispatching generators that dispatch
+ * additional sync and async actions.
+ */
+
+const {
+ createStore,
+ applyMiddleware,
+} = require("resource://devtools/client/shared/vendor/redux.js");
+const {
+ task,
+} = require("resource://devtools/client/shared/redux/middleware/task.js");
+
+add_task(async function () {
+ const store = applyMiddleware(task)(createStore)(reducer);
+
+ store.dispatch(comboAction());
+ await waitUntilState(store, () => store.getState().length === 4);
+
+ equal(
+ store.getState()[0].type,
+ "fetchAsync-start",
+ "Async dispatched actions in a generator task are fired"
+ );
+ equal(
+ store.getState()[1].type,
+ "fetchAsync-end",
+ "Async dispatched actions in a generator task are fired"
+ );
+ equal(
+ store.getState()[2].type,
+ "fetchSync",
+ "Return values of yielded sync dispatched actions are correct"
+ );
+ equal(
+ store.getState()[3].type,
+ "fetch-done",
+ "Return values of yielded async dispatched actions are correct"
+ );
+ equal(
+ store.getState()[3].data.sync.data,
+ "sync",
+ "Return values of dispatched sync values are correct"
+ );
+ equal(
+ store.getState()[3].data.async,
+ "async",
+ "Return values of dispatched async values are correct"
+ );
+});
+
+function comboAction() {
+ return async function ({ dispatch, getState }) {
+ const data = {};
+ data.async = await dispatch(fetchAsync("async"));
+ data.sync = await dispatch(fetchSync("sync"));
+ dispatch({ type: "fetch-done", data });
+ };
+}
+
+function fetchSync(data) {
+ return { type: "fetchSync", data };
+}
+
+function fetchAsync(data) {
+ return async function ({ dispatch }) {
+ dispatch({ type: "fetchAsync-start" });
+ const val = await new Promise(resolve => resolve(data));
+ dispatch({ type: "fetchAsync-end" });
+ return val;
+ };
+}
+
+function reducer(state = [], action) {
+ info("Action called: " + action.type);
+ if (/fetch/.test(action.type)) {
+ state.push(action);
+ }
+ return [...state];
+}
diff --git a/devtools/client/shared/redux/middleware/xpcshell/test_middleware-task-03.js b/devtools/client/shared/redux/middleware/xpcshell/test_middleware-task-03.js
new file mode 100644
index 0000000000..94087e31de
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/xpcshell/test_middleware-task-03.js
@@ -0,0 +1,50 @@
+/* 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 {
+ createStore,
+ applyMiddleware,
+} = require("resource://devtools/client/shared/vendor/redux.js");
+const {
+ task,
+ ERROR_TYPE,
+} = require("resource://devtools/client/shared/redux/middleware/task.js");
+
+/**
+ * Tests that the middleware handles errors thrown in tasks, and rejected promises.
+ */
+
+add_task(async function () {
+ const store = applyMiddleware(task)(createStore)(reducer);
+
+ store.dispatch(asyncError());
+ await waitUntilState(store, () => store.getState().length === 1);
+ equal(
+ store.getState()[0].type,
+ ERROR_TYPE,
+ "generator errors dispatch ERROR_TYPE actions"
+ );
+ equal(
+ store.getState()[0].error,
+ "task-middleware-error-generator",
+ "generator errors dispatch ERROR_TYPE actions with error"
+ );
+});
+
+function asyncError() {
+ return async ({ dispatch, getState }) => {
+ const error = "task-middleware-error-generator";
+ throw error;
+ };
+}
+
+function reducer(state = [], action) {
+ info("Action called: " + action.type);
+ if (action.type === ERROR_TYPE) {
+ state.push(action);
+ }
+ return [...state];
+}
diff --git a/devtools/client/shared/redux/middleware/xpcshell/xpcshell.toml b/devtools/client/shared/redux/middleware/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..d1bb2822d3
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/xpcshell/xpcshell.toml
@@ -0,0 +1,11 @@
+[DEFAULT]
+tags = "devtools"
+head = "head.js"
+firefox-appdir = "browser"
+skip-if = ["os == 'android'"]
+
+["test_middleware-task-01.js"]
+
+["test_middleware-task-02.js"]
+
+["test_middleware-task-03.js"]
diff --git a/devtools/client/shared/redux/moz.build b/devtools/client/shared/redux/moz.build
new file mode 100644
index 0000000000..d4c7bc503b
--- /dev/null
+++ b/devtools/client/shared/redux/moz.build
@@ -0,0 +1,15 @@
+# -*- 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/.
+
+DIRS += [
+ "middleware",
+]
+
+DevToolsModules(
+ "create-store.js",
+ "subscriber.js",
+ "visibility-handler-connect.js",
+)
diff --git a/devtools/client/shared/redux/subscriber.js b/devtools/client/shared/redux/subscriber.js
new file mode 100644
index 0000000000..39b0823770
--- /dev/null
+++ b/devtools/client/shared/redux/subscriber.js
@@ -0,0 +1,16 @@
+/* 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";
+
+function registerStoreObserver(store, subscriber) {
+ let oldState = store.getState();
+ store.subscribe(() => {
+ const state = store.getState();
+ subscriber(state, oldState);
+ oldState = state;
+ });
+}
+
+exports.registerStoreObserver = registerStoreObserver;
diff --git a/devtools/client/shared/redux/visibility-handler-connect.js b/devtools/client/shared/redux/visibility-handler-connect.js
new file mode 100644
index 0000000000..85835a1862
--- /dev/null
+++ b/devtools/client/shared/redux/visibility-handler-connect.js
@@ -0,0 +1,35 @@
+/* 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 {
+ createFactory,
+ createElement,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const VisibilityHandler = createFactory(
+ require("resource://devtools/client/shared/components/VisibilityHandler.js")
+);
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+/**
+ * This helper is wrapping Redux's connect() method and applying
+ * HOC (VisibilityHandler component) on whatever component is
+ * originally passed in. The HOC is responsible for not causing
+ * rendering if the owner panel runs in the background.
+ */
+function visibilityHandlerConnect() {
+ const args = [].slice.call(arguments);
+ return component => {
+ return connect(...args)(props => {
+ return VisibilityHandler(null, createElement(component, props));
+ });
+ };
+}
+
+module.exports = {
+ connect: visibilityHandlerConnect,
+};