redux-detector
Version:
Redux enhancer for pure detection of state changes.
289 lines (265 loc) • 11.8 kB
JavaScript
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var ActionTypes = {
INIT: "@@detector/INIT"
};
/**
* Creates detector enhancer that modifies redux store to use it with provided detector.
*
* @param detector Root actions detector
* @param listener
* @returns Store enhancer
*/
function createDetectorEnhancer(detector, listener) {
if (typeof detector !== "function") {
throw new Error("Expected the detector to be a function.");
}
return function detectorEnhancer(next) {
return function detectableStoreCreator(reducer, preloadedState) {
// first create basic store
var store = next(reducer, preloadedState);
// then set initial values in this scope
var prevState = preloadedState;
var currentDetector = detector;
var isDispatchingFromQueue = false;
var actionsQueue = [];
// store detectable adds `replaceDetector` method to it's interface
var detectableStore = __assign(__assign({}, store), { replaceDetector: function replaceDetector(nextDetector) {
if (typeof nextDetector !== "function") {
throw new Error("Expected the nextDetector to be a function.");
}
currentDetector = nextDetector;
store.dispatch({ type: ActionTypes.INIT });
} });
// create an API for listener
var listenerAPI = {
dispatch: store.dispatch,
getState: store.getState
};
// have to run detector on every state change
detectableStore.subscribe(function detectActions() {
var nextState = detectableStore.getState();
// detect actions by comparing prev and next state
var detectedActions = currentDetector(prevState, nextState) || [];
// convert to array
detectedActions =
Array === detectedActions.constructor
? detectedActions
: [detectedActions];
// add to the actions queue
actionsQueue = actionsQueue.concat(detectedActions);
// store current state as previous for next subscribe call
prevState = nextState;
// dispatch actions for action queue
if (!isDispatchingFromQueue && actionsQueue.length > 0) {
isDispatchingFromQueue = true;
while (actionsQueue.length > 0) {
// get next action from the queue
var action = actionsQueue.shift();
try {
// dispatch next action - it can throw an error so it's wrapped into try-catch block
var result = detectableStore.dispatch(action);
if (result instanceof Promise) {
// prevent UnhandledPromiseRejection error - we can handle it via listener.next but it's optional
result.catch(function () { return null; });
}
// if we have a next listener, call it with a result and the listener API
if (listener && listener.next) {
listener.next(result, action, listenerAPI);
}
}
catch (error) {
// if we have an error listener, call it with an error and the listener API
if (listener && listener.error) {
listener.error(error, action, listenerAPI);
}
}
}
isDispatchingFromQueue = false;
}
});
return detectableStore;
};
};
}
/**
* Creates new DetectorListener
*
* @param onNext callback for successful dispatched action
* @param onError callback for dispatch that throws an error
*/
function createDetectorListener(onNext, onError) {
return {
next: onNext,
error: onError
};
}
/**
* Composes many listeners into on listener. They will be called in the order that they were passed to this function.
*
* @param listeners
*/
function composeDetectorListeners() {
var listeners = [];
for (var _i = 0; _i < arguments.length; _i++) {
listeners[_i] = arguments[_i];
}
return {
next: function (result, action, api) {
return listeners.forEach(function (listener) { return listener.next && listener.next(result, action, api); });
},
error: function (error, action, api) {
return listeners.forEach(function (listener) { return listener.error && listener.error(error, action, api); });
}
};
}
/**
* Compose many action detectors into one detector that aggregates actions returned by given detectors
*/
function composeDetectors() {
var detectors = [];
for (var _i = 0; _i < arguments.length; _i++) {
detectors[_i] = arguments[_i];
}
// check detectors types in runtime
var invalidDetectorsIndexes = detectors
.map(function (detector, index) { return (detector instanceof Function ? -1 : index); })
.filter(function (index) { return index !== -1; });
if (invalidDetectorsIndexes.length) {
throw new Error("Invalid " + invalidDetectorsIndexes.join(", ") + " arguments in composeDetectors function.\n" +
"Detectors should be a 'function' type, " +
("'" + invalidDetectorsIndexes
.map(function (index) { return typeof detectors[index]; })
.join("', '") + "' types passed."));
}
return function composedDetector(prevState, nextState) {
return detectors
.map(function (detector) { return detector(prevState, nextState) || []; })
.reduceRight(function (actions, nextActions) { return actions.concat(nextActions); }, []);
};
}
/**
* Combine detectors to bind them to the local state.
* It allows to create reusable detectors.
*
* @param map Map of detectors bounded to state.
* @returns Combined detector
*/
function combineDetectors(map) {
return function combinedDetector(prevState, nextState) {
return Object.keys(map).reduce(function (reducedActions, key) {
var actions = map[key](prevState ? prevState[key] : undefined, nextState ? nextState[key] : undefined);
if (actions) {
if (actions.constructor !== Array) {
actions = [actions];
}
reducedActions = reducedActions.concat.apply(reducedActions, actions);
}
return reducedActions;
}, []);
};
}
/**
* Maps detector to selected state using selectors. Works perfectly with reselect library.
*/
function mapDetector(selector, detector) {
return function mappedDetector(prevState, nextState) {
return detector(selector(prevState), selector(nextState));
};
}
function mapNextState() {
var selectors = [];
for (var _i = 0; _i < arguments.length; _i++) {
selectors[_i] = arguments[_i];
}
return function mappedNextStateDetector(prevState, nextState) {
return selectors.reduce(function (state, selector) { return selector(state); }, nextState);
};
}
function mapPrevState() {
var selectors = [];
for (var _i = 0; _i < arguments.length; _i++) {
selectors[_i] = arguments[_i];
}
return function mappedPrevStateDetector(prevState) {
return selectors.reduce(function (state, selector) { return selector(state); }, prevState);
};
}
function composeIf(condition, actions) {
return function conditionalDetector(prevState, nextState) {
if (condition(prevState, nextState)) {
return actions(prevState, nextState);
}
};
}
/**
* Composes condition detectors into one detector using "and" operation on detectors output.
*/
function composeAnd() {
var detectors = [];
for (var _i = 0; _i < arguments.length; _i++) {
detectors[_i] = arguments[_i];
}
return function composedAndDetector(prevState, nextState) {
return detectors.length
? detectors.every(function (detector) { return detector(prevState, nextState); })
: false;
};
}
/**
* Composes condition detectors into one detector using "or" operation on detectors output.
*/
function composeOr() {
var detectors = [];
for (var _i = 0; _i < arguments.length; _i++) {
detectors[_i] = arguments[_i];
}
return function composedOrDetector(prevState, nextState) {
return detectors.some(function (detector) { return detector(prevState, nextState); });
};
}
function changed(selector) {
return mapDetector(selector, function (prevState, nextState) { return prevState !== nextState; });
}
function changedAndFalsy(selector) {
return mapDetector(selector, function (prevState, nextState) { return prevState !== nextState && !nextState; });
}
function changedAndTruthy(selector) {
return mapDetector(selector, function (prevState, nextState) { return prevState !== nextState && !!nextState; });
}
function changedToFalsy(selector) {
return mapDetector(selector, function (prevState, nextState) { return !!prevState && !nextState; });
}
function changedToTruthy(selector) {
return mapDetector(selector, function (prevState, nextState) { return !prevState && !!nextState; });
}
function isEqual(selector, expectedNextState) {
return mapDetector(selector, function (prevState, nextState) { return nextState === expectedNextState; });
}
function isFalsy(selector) {
return mapDetector(selector, function (prevState, nextState) { return !nextState; });
}
function isTruthy(selector) {
return mapDetector(selector, function (prevState, nextState) { return !!nextState; });
}
export { changed, changedAndFalsy, changedAndTruthy, changedToFalsy, changedToTruthy, combineDetectors, composeAnd, composeDetectorListeners, composeDetectors, composeIf, composeOr, createDetectorEnhancer, createDetectorListener, isEqual, isFalsy, isTruthy, mapDetector, mapNextState, mapPrevState };