summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/redux/middleware
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/shared/redux/middleware')
-rw-r--r--devtools/client/shared/redux/middleware/history.js23
-rw-r--r--devtools/client/shared/redux/middleware/log.js17
-rw-r--r--devtools/client/shared/redux/middleware/moz.build16
-rw-r--r--devtools/client/shared/redux/middleware/promise.js54
-rw-r--r--devtools/client/shared/redux/middleware/task.js42
-rw-r--r--devtools/client/shared/redux/middleware/test/.eslintrc.js17
-rw-r--r--devtools/client/shared/redux/middleware/test/head.js27
-rw-r--r--devtools/client/shared/redux/middleware/test/test_middleware-task-01.js56
-rw-r--r--devtools/client/shared/redux/middleware/test/test_middleware-task-02.js67
-rw-r--r--devtools/client/shared/redux/middleware/test/test_middleware-task-03.js42
-rw-r--r--devtools/client/shared/redux/middleware/test/xpcshell.ini10
-rw-r--r--devtools/client/shared/redux/middleware/thunk.js19
-rw-r--r--devtools/client/shared/redux/middleware/wait-service.js64
13 files changed, 454 insertions, 0 deletions
diff --git a/devtools/client/shared/redux/middleware/history.js b/devtools/client/shared/redux/middleware/history.js
new file mode 100644
index 000000000..dba88c045
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/history.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";
+
+const flags = require("devtools/shared/flags");
+
+/**
+ * A middleware that stores every action coming through the store in the passed
+ * in logging object. Should only be used for tests, as it collects all
+ * action information, which will cause memory bloat.
+ */
+exports.history = (log = []) => ({ dispatch, getState }) => {
+ if (!flags.testing) {
+ console.warn("Using history middleware stores all actions in state for " +
+ "testing and devtools is not currently running in test " +
+ "mode. Be sure this is intentional.");
+ }
+ return next => action => {
+ log.push(action);
+ next(action);
+ };
+};
diff --git a/devtools/client/shared/redux/middleware/log.js b/devtools/client/shared/redux/middleware/log.js
new file mode 100644
index 000000000..f812f793b
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/log.js
@@ -0,0 +1,17 @@
+/* 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 => {
+ console.log("[DISPATCH]", JSON.stringify(action, null, 2));
+ 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 000000000..a25bfd518
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/moz.build
@@ -0,0 +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(
+ 'history.js',
+ 'log.js',
+ 'promise.js',
+ 'task.js',
+ 'thunk.js',
+ 'wait-service.js',
+)
+
+XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell.ini']
diff --git a/devtools/client/shared/redux/middleware/promise.js b/devtools/client/shared/redux/middleware/promise.js
new file mode 100644
index 000000000..237e41eef
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/promise.js
@@ -0,0 +1,54 @@
+/* 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 uuidgen = require("sdk/util/uuid").uuid;
+const defer = require("devtools/shared/defer");
+const {
+ entries, toObject, executeSoon
+} = require("devtools/shared/DevToolsUtils");
+const PROMISE = exports.PROMISE = "@@dispatch/promise";
+
+function promiseMiddleware({ dispatch, getState }) {
+ return next => action => {
+ if (!(PROMISE in action)) {
+ return next(action);
+ }
+
+ const promiseInst = action[PROMISE];
+ const seqId = uuidgen().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" }));
+
+ // Return the promise so action creators can still compose if they
+ // want to.
+ const deferred = defer();
+ promiseInst.then(value => {
+ executeSoon(() => {
+ dispatch(Object.assign({}, action, {
+ status: "done",
+ value: value
+ }));
+ deferred.resolve(value);
+ });
+ }, error => {
+ executeSoon(() => {
+ dispatch(Object.assign({}, action, {
+ status: "error",
+ error: error.message || error
+ }));
+ deferred.reject(error);
+ });
+ });
+ return deferred.promise;
+ };
+}
+
+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 000000000..c1dd262ee
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/task.js
@@ -0,0 +1,42 @@
+/* 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 { Task } = require("devtools/shared/task");
+const { executeSoon, isGenerator, reportException } = require("devtools/shared/DevToolsUtils");
+const ERROR_TYPE = exports.ERROR_TYPE = "@@redux/middleware/task#error";
+
+/**
+ * A middleware that allows generator thunks (functions) and promise
+ * to be dispatched. If it's a generator, it is called with `dispatch`
+ * and `getState`, allowing the action to create multiple actions (most likely
+ * asynchronously) and yield on each. If called with a promise, calls `dispatch`
+ * on the results.
+ */
+
+function task({ dispatch, getState }) {
+ return next => action => {
+ if (isGenerator(action)) {
+ return Task.spawn(action.bind(null, dispatch, getState))
+ .then(null, handleError.bind(null, dispatch));
+ }
+
+ /*
+ if (isPromise(action)) {
+ return action.then(dispatch, 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/test/.eslintrc.js b/devtools/client/shared/redux/middleware/test/.eslintrc.js
new file mode 100644
index 000000000..0d12cd9a3
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/test/.eslintrc.js
@@ -0,0 +1,17 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../../../.eslintrc.mochitests.js",
+ "globals": {
+ "run_test": true,
+ "run_next_test": true,
+ "equal": true,
+ "do_print": true,
+ "waitUntilState": true
+ },
+ "rules": {
+ // Stop giving errors for run_test
+ "camelcase": "off"
+ }
+};
diff --git a/devtools/client/shared/redux/middleware/test/head.js b/devtools/client/shared/redux/middleware/test/head.js
new file mode 100644
index 000000000..1e5cbff7a
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/test/head.js
@@ -0,0 +1,27 @@
+/* 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 } = Components.utils.import("resource://devtools/shared/Loader.jsm", {});
+const flags = require("devtools/shared/flags");
+
+flags.testing = true;
+
+function waitUntilState(store, predicate) {
+ return new Promise(resolve => {
+ let 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/test/test_middleware-task-01.js b/devtools/client/shared/redux/middleware/test/test_middleware-task-01.js
new file mode 100644
index 000000000..be94560cb
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/test/test_middleware-task-01.js
@@ -0,0 +1,56 @@
+/* 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("devtools/client/shared/vendor/redux");
+const { task } = require("devtools/client/shared/redux/middleware/task");
+
+/**
+ * Tests that task middleware allows dispatching generators, promises and objects
+ * that return actions;
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let store = applyMiddleware(task)(createStore)(reducer);
+
+ store.dispatch(fetch1("generator"));
+ yield waitUntilState(store, () => store.getState().length === 1);
+ equal(store.getState()[0].data, "generator",
+ "task middleware async dispatches an action via generator");
+
+ store.dispatch(fetch2("sync"));
+ yield waitUntilState(store, () => store.getState().length === 2);
+ equal(store.getState()[1].data, "sync",
+ "task middleware sync dispatches an action via sync");
+});
+
+function fetch1(data) {
+ return function* (dispatch, getState) {
+ equal(getState().length, 0, "`getState` is accessible in a generator action");
+ let moreData = yield new Promise(resolve => resolve(data));
+ // Ensure it handles more than one yield
+ moreData = yield new Promise(resolve => resolve(data));
+ dispatch({ type: "fetch1", data: moreData });
+ };
+}
+
+function fetch2(data) {
+ return {
+ type: "fetch2",
+ data
+ };
+}
+
+function reducer(state = [], action) {
+ do_print("Action called: " + action.type);
+ if (["fetch1", "fetch2"].includes(action.type)) {
+ state.push(action);
+ }
+ return [...state];
+}
diff --git a/devtools/client/shared/redux/middleware/test/test_middleware-task-02.js b/devtools/client/shared/redux/middleware/test/test_middleware-task-02.js
new file mode 100644
index 000000000..7e2a88d2c
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/test/test_middleware-task-02.js
@@ -0,0 +1,67 @@
+/* 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("devtools/client/shared/vendor/redux");
+const { task } = require("devtools/client/shared/redux/middleware/task");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let store = applyMiddleware(task)(createStore)(reducer);
+
+ store.dispatch(comboAction());
+ yield waitUntilState(store, () => store.getState().length === 3);
+
+ 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 function* (dispatch, getState) {
+ let data = {};
+ data.async = yield dispatch(fetchAsync("async"));
+ data.sync = yield dispatch(fetchSync("sync"));
+ dispatch({ type: "fetch-done", data });
+ };
+}
+
+function fetchSync(data) {
+ return { type: "fetchSync", data };
+}
+
+function fetchAsync(data) {
+ return function* (dispatch) {
+ dispatch({ type: "fetchAsync-start" });
+ let val = yield new Promise(resolve => resolve(data));
+ dispatch({ type: "fetchAsync-end" });
+ return val;
+ };
+}
+
+function reducer(state = [], action) {
+ do_print("Action called: " + action.type);
+ if (/fetch/.test(action.type)) {
+ state.push(action);
+ }
+ return [...state];
+}
diff --git a/devtools/client/shared/redux/middleware/test/test_middleware-task-03.js b/devtools/client/shared/redux/middleware/test/test_middleware-task-03.js
new file mode 100644
index 000000000..7dc0e5c9d
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/test/test_middleware-task-03.js
@@ -0,0 +1,42 @@
+/* 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("devtools/client/shared/vendor/redux");
+const { task, ERROR_TYPE } = require("devtools/client/shared/redux/middleware/task");
+
+/**
+ * Tests that the middleware handles errors thrown in tasks, and rejected promises.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let store = applyMiddleware(task)(createStore)(reducer);
+
+ store.dispatch(generatorError());
+ yield 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 generatorError() {
+ return function* (dispatch, getState) {
+ let error = "task-middleware-error-generator";
+ throw error;
+ };
+}
+
+function reducer(state = [], action) {
+ do_print("Action called: " + action.type);
+ if (action.type === ERROR_TYPE) {
+ state.push(action);
+ }
+ return [...state];
+}
diff --git a/devtools/client/shared/redux/middleware/test/xpcshell.ini b/devtools/client/shared/redux/middleware/test/xpcshell.ini
new file mode 100644
index 000000000..3836ed1fd
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/test/xpcshell.ini
@@ -0,0 +1,10 @@
+[DEFAULT]
+tags = devtools
+head = head.js
+tail =
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+
+[test_middleware-task-01.js]
+[test_middleware-task-02.js]
+[test_middleware-task-03.js]
diff --git a/devtools/client/shared/redux/middleware/thunk.js b/devtools/client/shared/redux/middleware/thunk.js
new file mode 100644
index 000000000..8f564a033
--- /dev/null
+++ b/devtools/client/shared/redux/middleware/thunk.js
@@ -0,0 +1,19 @@
+/* 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 `dispatch` and `getState`,
+ * allowing the action to create multiple actions (most likely
+ * asynchronously).
+ */
+function thunk({ dispatch, getState }) {
+ return next => action => {
+ return (typeof action === "function")
+ ? action(dispatch, getState)
+ : 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 000000000..93878a312
--- /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) {
+ let readyRequests = [];
+ let 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 (let request of pending) {
+ if (request.predicate(action)) {
+ readyRequests.push(request);
+ } else {
+ stillPending.push(request);
+ }
+ }
+
+ pending = stillPending;
+ for (let request of readyRequests) {
+ request.run(dispatch, getState, action);
+ }
+ }
+
+ return next => action => {
+ if (action.type === NAME) {
+ pending.push(action);
+ return null;
+ }
+ let result = next(action);
+ checkPending(action);
+ return result;
+ };
+}
+exports.waitUntilService = waitUntilService;