UNPKG

redux-store-watch

Version:

A watcher for redux store.

222 lines (174 loc) 7.84 kB
'use strict'; var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var _get = require('hodash.get'); var ACTION = function ACTION(name) { return 'WATCH_VALUE_CHANGED' + (name ? ': ' + name : ''); }; /** * */ module.exports = function (store, config) { return new StoreWatcher(store || globalConfig.store, config); }; var globalConfig = {}; module.exports.configureGlobal = function (config) { globalConfig.store = config.store; globalConfig.shouldDispatch = config.shouldDispatch === true; globalConfig.shouldLog = config.shouldLog === true; globalConfig.requireName = config.requireName === true; }; // This establishes a private namespace. var namespace = new WeakMap(); function p(object) { if (!namespace.has(object)) namespace.set(object, {}); return namespace.get(object); } /** * */ var StoreWatcher = function () { function StoreWatcher(store, config) { var _this = this; _classCallCheck(this, StoreWatcher); config = config || {}; p(this).store = store; p(this).settings = { shouldDispatch: config.shouldDispatch === true, shouldLog: config.shouldLog === true, requireName: config.requireName === true || globalConfig.requireName }; p(this).watchedSelectors = new Map(); // Mapping paths to their selectors. p(this).pathSelectorMapping = {}; // We store the previous state. Because the state should be immutable, this // means we're only keeping at minimum a single object reference in memory, // whereas most others remain unchanged references between states. // This is used for diffing given paths. p(this).previousState = store.getState(); // For selectors, we must store the previous results found for a given // selector keyed by the selector itself. p(this).previousSelectorValues = new WeakMap(); // Bind Private methods. p(this).handleSelectorChanges = handleSelectorChanges.bind(this); // Subscribe to store. p(this).unsubscribe = store.subscribe(function () { var currentState = p(_this).store.getState(); var previousState = p(_this).previousState; // Redefine previous state as current state. p(_this).previousState = currentState; // On store action, handle changes to watched store selector. p(_this).handleSelectorChanges(currentState, previousState); }); } /** * Watch a given string path or a selector's values on store action. Strict * equality is used under the assumption that immutable store is employed. * config.checkEqual can optionally be passed if there is a need for something * other than strict equality. */ _createClass(StoreWatcher, [{ key: 'watch', value: function watch(pathOrSelector, changeListener, config) { config = config || {}; if (!pathOrSelector) throw new Error('StoreWatcher watch must be provided pathOrSelector.'); if (!isFunction(changeListener)) throw new Error('StoreWatcher watch must be provided a function changeListener.'); var handler = { changeListener: changeListener, checkEqual: config.checkEqual, shouldDispatch: config.shouldDispatch, shouldLog: config.shouldLog, meta: {} }; var name = config.name; // If we've been provided a string, we convert it to a selector. if (isString(pathOrSelector)) { var path = pathOrSelector; handler.meta.path = path; if (!name) name = path; var mapping = p(this).pathSelectorMapping; // Create or find existing selector for the given path. if (!mapping[path]) { // Create selector and add mapping of path to the selector to avoid // having to create multiple selectors if multiple parties interested in same path. mapping[path] = function (state) { return _get(state, path); }; } pathOrSelector = mapping[path]; } if (!name && p(this).settings.requireName) throw new Error('Redux Store Watch .watch defined without name while configured to requireName.'); handler.meta.name = name; var selector = pathOrSelector; handler.meta.selector = selector; handler.meta.selectorString = selector.toString(); if (!p(this).watchedSelectors.get(selector)) { p(this).watchedSelectors.set(selector, []); } p(this).watchedSelectors.get(selector).push(handler); // Initialize current value of selector as previous value. if (config.initializeValue !== false) p(this).previousSelectorValues.set(selector, callSelector(selector, p(this).store.getState())); } /** * Unsubscribe from store. */ }, { key: 'remove', value: function remove() { p(this).unsubscribe(); } /** * */ }, { key: 'getStore', value: function getStore() { return p(this).store || globalConfig.store; } }]); return StoreWatcher; }(); // Private methods. /** * Check for change between previous selector result and current selector result. * Something of note is that the previous state value is explicitly saved, even * though it could just be checked again by selecting from the previousState. * The reason this is is because it is a very small memory footprint, likely * just reference to a part of the state already in memory, which provides the * additional benefit of letting your selector reference some other arbitrary * state. If all you care about is checking if something has changed since then, * you may do so. */ function handleSelectorChanges(currentState, previousState) { var _this2 = this; p(this).watchedSelectors.forEach(function (handlers, selector) { var currentValue = callSelector(selector, currentState); var previousValue = p(_this2).previousSelectorValues.get(selector); // Redefine previous selector's result to reflect current selector. p(_this2).previousSelectorValues.set(selector, currentValue); handlers.forEach(function (handler) { // Check whether or not the values are equal, using equality checker custom // to handler, else strict equality. var areEqual = handler.checkEqual ? handler.checkEqual(currentValue, previousValue) : currentValue === previousValue; if (areEqual) return; var changeListener = handler.changeListener; var action = { type: ACTION(handler.meta.name), meta: handler.meta, previousValue: previousValue, currentValue: currentValue }; if (handler.shouldDispatch || p(_this2).settings.shouldDispatch || globalConfig.shouldDispatch) p(_this2).store.dispatch(action); if (handler.shouldLog || p(_this2).settings.shouldLog || globalConfig.shouldLog) console.log(action); changeListener(currentValue, previousValue, currentState, previousState); }); }); } // Utilities. var asString = Object.prototype.toString; function isString(subject) { return asString.call(subject) == '[object String]'; } function isFunction(subject) { return asString.call(subject) == '[object Function]'; } function callSelector(selector, state) { try { return selector(state); } catch (err) {} }