UNPKG

redux-detector

Version:

Redux enhancer for pure detection of state changes.

289 lines (265 loc) 11.8 kB
/*! ***************************************************************************** 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 };