non-react-subscriber.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. "use strict";
  5. /**
  6. * This file defines functions to add the ability for redux reducers
  7. * to broadcast specific state changes to a non-React UI. You should
  8. * *never* use this for new code that uses React, as it violates the
  9. * core principals of a functional UI. This should only be used when
  10. * migrating old code to redux, because it allows you to use redux
  11. * with event-listening UI elements. The typical way to set all of
  12. * this up is this:
  13. *
  14. * const emitter = makeEmitter();
  15. * let store = createStore(combineEmittingReducers(
  16. * reducers,
  17. * emitter.emit
  18. * ));
  19. * store = enhanceStoreWithEmitter(store, emitter);
  20. *
  21. * Now reducers will receive a 3rd argument, `emit`, for emitting
  22. * events, and the store has an `on` function for listening to them.
  23. * For example, a reducer can now do this:
  24. *
  25. * function update(state = initialState, action, emitChange) {
  26. * if (action.type === constants.ADD_BREAKPOINT) {
  27. * const id = action.breakpoint.id;
  28. * emitChange('add-breakpoint', action.breakpoint);
  29. * return state.merge({ [id]: action.breakpoint });
  30. * }
  31. * return state;
  32. * }
  33. *
  34. * `emitChange` is *not* synchronous, the state changes will be
  35. * broadcasted *after* all reducers are run and the state has been
  36. * updated.
  37. *
  38. * Now, a non-React widget can do this:
  39. *
  40. * store.on('add-breakpoint', breakpoint => { ... });
  41. */
  42. const { combineReducers } = require("devtools/client/shared/vendor/redux");
  43. /**
  44. * Make an emitter that is meant to be used in redux reducers. This
  45. * does not run listeners immediately when an event is emitted; it
  46. * waits until all reducers have run and the store has updated the
  47. * state, and then fires any enqueued events. Events *are* fired
  48. * synchronously, but just later in the process.
  49. *
  50. * This is important because you never want the UI to be updating in
  51. * the middle of a reducing process. Reducers will fire these events
  52. * in the middle of generating new state, but the new state is *not*
  53. * available from the store yet. So if the UI executes in the middle
  54. * of the reducing process and calls `getState()` to get something
  55. * from the state, it will get stale state.
  56. *
  57. * We want the reducing and the UI updating phases to execute
  58. * atomically and independent from each other.
  59. *
  60. * @param {Function} stillAliveFunc
  61. * A function that indicates the app is still active. If this
  62. * returns false, changes will stop being broadcasted.
  63. */
  64. function makeStateBroadcaster(stillAliveFunc) {
  65. const listeners = {};
  66. let enqueuedChanges = [];
  67. return {
  68. onChange: (name, cb) => {
  69. if (!listeners[name]) {
  70. listeners[name] = [];
  71. }
  72. listeners[name].push(cb);
  73. },
  74. offChange: (name, cb) => {
  75. listeners[name] = listeners[name].filter(listener => listener !== cb);
  76. },
  77. emitChange: (name, payload) => {
  78. enqueuedChanges.push([name, payload]);
  79. },
  80. subscribeToStore: store => {
  81. store.subscribe(() => {
  82. if (stillAliveFunc()) {
  83. enqueuedChanges.forEach(([name, payload]) => {
  84. if (listeners[name]) {
  85. listeners[name].forEach(listener => {
  86. listener(payload);
  87. });
  88. }
  89. });
  90. enqueuedChanges = [];
  91. }
  92. });
  93. }
  94. };
  95. }
  96. /**
  97. * Make a store fire any enqueued events whenever the state changes,
  98. * and add an `on` function to allow users to listen for specific
  99. * events.
  100. *
  101. * @param {Object} store
  102. * @param {Object} broadcaster
  103. * @return {Object}
  104. */
  105. function enhanceStoreWithBroadcaster(store, broadcaster) {
  106. broadcaster.subscribeToStore(store);
  107. store.onChange = broadcaster.onChange;
  108. store.offChange = broadcaster.offChange;
  109. return store;
  110. }
  111. /**
  112. * Function that takes a hash of reducers, like `combineReducers`, and
  113. * an `emitChange` function and returns a function to be used as a
  114. * reducer for a Redux store. This allows all reducers defined here to
  115. * receive a third argument, the `emitChange` function, for
  116. * event-based subscriptions from within reducers.
  117. *
  118. * @param {Object} reducers
  119. * @param {Function} emitChange
  120. * @return {Function}
  121. */
  122. function combineBroadcastingReducers(reducers, emitChange) {
  123. // Wrap each reducer with a wrapper function that calls
  124. // the reducer with a third argument, an `emitChange` function.
  125. // Use this rather than a new custom top level reducer that would ultimately
  126. // have to replicate redux's `combineReducers` so we only pass in correct
  127. // state, the error checking, and other edge cases.
  128. function wrapReduce(newReducers, key) {
  129. newReducers[key] = (state, action) => {
  130. return reducers[key](state, action, emitChange);
  131. };
  132. return newReducers;
  133. }
  134. return combineReducers(
  135. Object.keys(reducers).reduce(wrapReduce, Object.create(null))
  136. );
  137. }
  138. module.exports = {
  139. makeStateBroadcaster,
  140. enhanceStoreWithBroadcaster,
  141. combineBroadcastingReducers
  142. };