summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/redux/non-react-subscriber.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/shared/redux/non-react-subscriber.js')
-rw-r--r--devtools/client/shared/redux/non-react-subscriber.js153
1 files changed, 153 insertions, 0 deletions
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
+};