123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154 |
- /* 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
- };
|