redux-store-watch
Version:
A watcher for redux store.
222 lines (174 loc) • 7.84 kB
JavaScript
;
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) {}
}