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.js51
-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
-rw-r--r--devtools/client/shared/redux/moz.build14
-rw-r--r--devtools/client/shared/redux/non-react-subscriber.js153
16 files changed, 672 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 000000000..baacf428e
--- /dev/null
+++ b/devtools/client/shared/redux/create-store.js
@@ -0,0 +1,51 @@
+/* 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 { thunk } = require("./middleware/thunk");
+const { waitUntilService } = require("./middleware/wait-service");
+const { task } = require("./middleware/task");
+const { log } = require("./middleware/log");
+const { promise } = require("./middleware/promise");
+const { history } = require("./middleware/history");
+
+/**
+ * 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:
+ * - log: log all dispatched actions to console
+ * - history: an array to store every action in. Should only be
+ * used in tests.
+ * - middleware: array of middleware to be included in the redux store
+ */
+module.exports = (opts = {}) => {
+ const middleware = [
+ task,
+ thunk,
+ 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.history) {
+ middleware.push(history(opts.history));
+ }
+
+ if (opts.middleware) {
+ opts.middleware.forEach(fn => middleware.push(fn));
+ }
+
+ if (opts.log) {
+ middleware.push(log);
+ }
+
+ return applyMiddleware(...middleware)(createStore);
+};
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;
diff --git a/devtools/client/shared/redux/moz.build b/devtools/client/shared/redux/moz.build
new file mode 100644
index 000000000..02b1f6bd6
--- /dev/null
+++ b/devtools/client/shared/redux/moz.build
@@ -0,0 +1,14 @@
+# -*- 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',
+ 'non-react-subscriber.js',
+)
diff --git a/devtools/client/shared/redux/non-react-subscriber.js b/devtools/client/shared/redux/non-react-subscriber.js
new file mode 100644
index 000000000..0bb3f0b8f
--- /dev/null
+++ b/devtools/client/shared/redux/non-react-subscriber.js
@@ -0,0 +1,153 @@
+/* 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 file defines functions to add the ability for redux reducers
+ * to broadcast specific state changes to a non-React UI. You should
+ * *never* use this for new code that uses React, as it violates the
+ * core principals of a functional UI. This should only be used when
+ * migrating old code to redux, because it allows you to use redux
+ * with event-listening UI elements. The typical way to set all of
+ * this up is this:
+ *
+ * const emitter = makeEmitter();
+ * let store = createStore(combineEmittingReducers(
+ * reducers,
+ * emitter.emit
+ * ));
+ * store = enhanceStoreWithEmitter(store, emitter);
+ *
+ * Now reducers will receive a 3rd argument, `emit`, for emitting
+ * events, and the store has an `on` function for listening to them.
+ * For example, a reducer can now do this:
+ *
+ * function update(state = initialState, action, emitChange) {
+ * if (action.type === constants.ADD_BREAKPOINT) {
+ * const id = action.breakpoint.id;
+ * emitChange('add-breakpoint', action.breakpoint);
+ * return state.merge({ [id]: action.breakpoint });
+ * }
+ * return state;
+ * }
+ *
+ * `emitChange` is *not* synchronous, the state changes will be
+ * broadcasted *after* all reducers are run and the state has been
+ * updated.
+ *
+ * Now, a non-React widget can do this:
+ *
+ * store.on('add-breakpoint', breakpoint => { ... });
+ */
+
+const { combineReducers } = require("devtools/client/shared/vendor/redux");
+
+/**
+ * Make an emitter that is meant to be used in redux reducers. This
+ * does not run listeners immediately when an event is emitted; it
+ * waits until all reducers have run and the store has updated the
+ * state, and then fires any enqueued events. Events *are* fired
+ * synchronously, but just later in the process.
+ *
+ * This is important because you never want the UI to be updating in
+ * the middle of a reducing process. Reducers will fire these events
+ * in the middle of generating new state, but the new state is *not*
+ * available from the store yet. So if the UI executes in the middle
+ * of the reducing process and calls `getState()` to get something
+ * from the state, it will get stale state.
+ *
+ * We want the reducing and the UI updating phases to execute
+ * atomically and independent from each other.
+ *
+ * @param {Function} stillAliveFunc
+ * A function that indicates the app is still active. If this
+ * returns false, changes will stop being broadcasted.
+ */
+function makeStateBroadcaster(stillAliveFunc) {
+ const listeners = {};
+ let enqueuedChanges = [];
+
+ return {
+ onChange: (name, cb) => {
+ if (!listeners[name]) {
+ listeners[name] = [];
+ }
+ listeners[name].push(cb);
+ },
+
+ offChange: (name, cb) => {
+ listeners[name] = listeners[name].filter(listener => listener !== cb);
+ },
+
+ emitChange: (name, payload) => {
+ enqueuedChanges.push([name, payload]);
+ },
+
+ subscribeToStore: store => {
+ store.subscribe(() => {
+ if (stillAliveFunc()) {
+ enqueuedChanges.forEach(([name, payload]) => {
+ if (listeners[name]) {
+ listeners[name].forEach(listener => {
+ listener(payload);
+ });
+ }
+ });
+ enqueuedChanges = [];
+ }
+ });
+ }
+ };
+}
+
+/**
+ * Make a store fire any enqueued events whenever the state changes,
+ * and add an `on` function to allow users to listen for specific
+ * events.
+ *
+ * @param {Object} store
+ * @param {Object} broadcaster
+ * @return {Object}
+ */
+function enhanceStoreWithBroadcaster(store, broadcaster) {
+ broadcaster.subscribeToStore(store);
+ store.onChange = broadcaster.onChange;
+ store.offChange = broadcaster.offChange;
+ return store;
+}
+
+/**
+ * Function that takes a hash of reducers, like `combineReducers`, and
+ * an `emitChange` function and returns a function to be used as a
+ * reducer for a Redux store. This allows all reducers defined here to
+ * receive a third argument, the `emitChange` function, for
+ * event-based subscriptions from within reducers.
+ *
+ * @param {Object} reducers
+ * @param {Function} emitChange
+ * @return {Function}
+ */
+function combineBroadcastingReducers(reducers, emitChange) {
+ // Wrap each reducer with a wrapper function that calls
+ // the reducer with a third argument, an `emitChange` function.
+ // Use this rather than a new custom top level reducer that would ultimately
+ // have to replicate redux's `combineReducers` so we only pass in correct
+ // state, the error checking, and other edge cases.
+ function wrapReduce(newReducers, key) {
+ newReducers[key] = (state, action) => {
+ return reducers[key](state, action, emitChange);
+ };
+ return newReducers;
+ }
+
+ return combineReducers(
+ Object.keys(reducers).reduce(wrapReduce, Object.create(null))
+ );
+}
+
+module.exports = {
+ makeStateBroadcaster,
+ enhanceStoreWithBroadcaster,
+ combineBroadcastingReducers
+};