UNPKG

redux-logic

Version:

Redux middleware for organizing all your business logic. Intercept actions and perform async processing.

326 lines (314 loc) 13.1 kB
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } import "core-js/modules/es.symbol.js"; import "core-js/modules/es.symbol.description.js"; import "core-js/modules/es.symbol.iterator.js"; import "core-js/modules/es.symbol.to-primitive.js"; import "core-js/modules/es.error.cause.js"; import "core-js/modules/es.array.concat.js"; import "core-js/modules/es.array.filter.js"; import "core-js/modules/es.array.iterator.js"; import "core-js/modules/es.array.map.js"; import "core-js/modules/es.array.push.js"; import "core-js/modules/es.date.to-primitive.js"; import "core-js/modules/es.function.name.js"; import "core-js/modules/es.number.constructor.js"; import "core-js/modules/es.object.get-own-property-descriptor.js"; import "core-js/modules/es.object.get-own-property-descriptors.js"; import "core-js/modules/es.object.keys.js"; import "core-js/modules/es.object.to-string.js"; import "core-js/modules/es.string.iterator.js"; import "core-js/modules/es.string.sub.js"; import "core-js/modules/esnext.iterator.constructor.js"; import "core-js/modules/esnext.iterator.filter.js"; import "core-js/modules/esnext.iterator.for-each.js"; import "core-js/modules/esnext.iterator.map.js"; import "core-js/modules/esnext.iterator.reduce.js"; import "core-js/modules/esnext.iterator.some.js"; import "core-js/modules/web.dom-collections.for-each.js"; import "core-js/modules/web.dom-collections.iterator.js"; import { Subject, BehaviorSubject } from 'rxjs'; import { map, scan, takeWhile } from 'rxjs/operators'; import wrapper from './logicWrapper'; import { identityFn, stringifyType } from './utils'; var debug = function debug(/* ...args */) {}; var OP_INIT = 'init'; // initial monitor op before anything else /** Builds a redux middleware for handling logic (created with createLogic). It also provides a way to inject runtime dependencies that will be provided to the logic for use during its execution hooks. This middleware has two additional methods: - `addLogic(arrLogic)` adds additional logic dynamically - `replaceLogic(arrLogic)` replaces all logic, existing logic should still complete @param {array} arrLogic array of logic items (each created with createLogic) used in the middleware. The order in the array indicates the order they will be called in the middleware. @param {object} deps optional runtime dependencies that will be injected into the logic hooks. Anything from config to instances of objects or connections can be provided here. This can simply testing. Reserved property names: getState, action, and ctx. @returns {function} redux middleware with additional methods addLogic and replaceLogic */ export default function createLogicMiddleware() { var arrLogic = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; var deps = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; if (!Array.isArray(arrLogic)) { throw new Error('createLogicMiddleware needs to be called with an array of logic items'); } var duplicateLogic = findDuplicates(arrLogic); if (duplicateLogic.length) { throw new Error("duplicate logic, indexes: ".concat(duplicateLogic)); } var actionSrc$ = new Subject(); // mw action stream var monitor$ = new Subject(); // monitor all activity var lastPending$ = new BehaviorSubject({ op: OP_INIT }); monitor$.pipe(scan(function (acc, x) { // append a pending logic count var pending = acc.pending || 0; switch (x.op // eslint-disable-line default-case ) { case 'top': // action at top of logic stack case 'begin': // starting into a logic pending += 1; break; case 'end': // completed from a logic case 'bottom': // action cleared bottom of logic stack case 'nextDisp': // action changed type and dispatched case 'filtered': // action filtered case 'dispatchError': // error when dispatching case 'cancelled': // action cancelled before intercept complete // dispCancelled is not included here since // already accounted for in the 'end' op pending -= 1; break; default: } return _objectSpread(_objectSpread({}, x), {}, { pending: pending }); }, { pending: 0 })).subscribe(lastPending$); // pipe to lastPending var savedStore; var savedNext; var actionEnd$; var logicSub; var logicCount = 0; // used for implicit naming var savedLogicArr = arrLogic; // keep for uniqueness check function mw(store) { if (savedStore && savedStore !== store) { throw new Error('cannot assign logicMiddleware instance to multiple stores, create separate instance for each'); } savedStore = store; return function (next) { savedNext = next; var _applyLogic = applyLogic(arrLogic, savedStore, savedNext, logicSub, actionSrc$, deps, logicCount, monitor$), action$ = _applyLogic.action$, sub = _applyLogic.sub, cnt = _applyLogic.logicCount; actionEnd$ = action$; logicSub = sub; logicCount = cnt; return function (action) { debug('starting off', action); monitor$.next({ action: action, op: 'top' }); actionSrc$.next(action); return action; }; }; } /** observable to monitor flow in logic */ mw.monitor$ = monitor$; /** Resolve promise when all in-flight actions are complete passing through fn if provided @param {function} fn optional fn() which is invoked on completion @return {promise} promise resolves when all are complete */ mw.whenComplete = function whenComplete() { var fn = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : identityFn; return lastPending$.pipe( // tap(x => console.log('wc', x)), /* keep commented out */ takeWhile(function (x) { return x.pending; }), map(function /* x */ () { return undefined; }) // not passing along anything ).toPromise().then(fn); }; /** add additional deps after createStore has been run. Useful for dynamically injecting dependencies for the hooks. Throws an error if it tries to override an existing dependency with a new value or instance. @param {object} additionalDeps object of dependencies to add @return {undefined} */ mw.addDeps = function addDeps(additionalDeps) { if (_typeof(additionalDeps) !== 'object') { throw new Error('addDeps should be called with an object'); } Object.keys(additionalDeps).forEach(function (k) { var existing = deps[k]; var newValue = additionalDeps[k]; if (typeof existing !== 'undefined' && // previously existing dep existing !== newValue) { // no override throw new Error("addDeps cannot override an existing dep value: ".concat(k)); } // eslint-disable-next-line no-param-reassign deps[k] = newValue; }); }; /** add logic after createStore has been run. Useful for dynamically loading bundles at runtime. Existing state in logic is preserved. @param {array} arrNewLogic array of logic items to add @return {object} object with a property logicCount set to the count of logic items */ mw.addLogic = function addLogic(arrNewLogic) { if (!arrNewLogic.length) { return { logicCount: logicCount }; } var combinedLogic = savedLogicArr.concat(arrNewLogic); var duplicateLogic = findDuplicates(combinedLogic); if (duplicateLogic.length) { throw new Error("duplicate logic, indexes: ".concat(duplicateLogic)); } var _applyLogic2 = applyLogic(arrNewLogic, savedStore, savedNext, logicSub, actionEnd$, deps, logicCount, monitor$), action$ = _applyLogic2.action$, sub = _applyLogic2.sub, cnt = _applyLogic2.logicCount; actionEnd$ = action$; logicSub = sub; logicCount = cnt; savedLogicArr = combinedLogic; debug('added logic'); return { logicCount: cnt }; }; mw.mergeNewLogic = function mergeNewLogic(arrMergeLogic) { // check for duplicates within the arrMergeLogic first var duplicateLogic = findDuplicates(arrMergeLogic); if (duplicateLogic.length) { throw new Error("duplicate logic, indexes: ".concat(duplicateLogic)); } // filter out any refs that match existing logic, then addLogic var arrNewLogic = arrMergeLogic.filter(function (x) { return savedLogicArr.indexOf(x) === -1; }); return mw.addLogic(arrNewLogic); }; /** replace all existing logic with a new array of logic. In-flight requests should complete. Logic state will be reset. @param {array} arrRepLogic array of replacement logic items @return {object} object with a property logicCount set to the count of logic items */ mw.replaceLogic = function replaceLogic(arrRepLogic) { var duplicateLogic = findDuplicates(arrRepLogic); if (duplicateLogic.length) { throw new Error("duplicate logic, indexes: ".concat(duplicateLogic)); } var _applyLogic3 = applyLogic(arrRepLogic, savedStore, savedNext, logicSub, actionSrc$, deps, 0, monitor$), action$ = _applyLogic3.action$, sub = _applyLogic3.sub, cnt = _applyLogic3.logicCount; actionEnd$ = action$; logicSub = sub; logicCount = cnt; savedLogicArr = arrRepLogic; debug('replaced logic'); return { logicCount: cnt }; }; return mw; } function applyLogic(arrLogic, store, next, sub, actionIn$, deps, startLogicCount, monitor$) { if (!store || !next) { throw new Error('store is not defined'); } if (sub) { sub.unsubscribe(); } var wrappedLogic = arrLogic.map(function (logic, idx) { var namedLogic = naming(logic, idx + startLogicCount); return wrapper(namedLogic, store, deps, monitor$); }); var actionOut$ = wrappedLogic.reduce(function (acc$, wep) { return wep(acc$); }, actionIn$); var newSub = actionOut$.subscribe(function (action) { debug('actionEnd$', action); try { var result = next(action); debug('result', result); } catch (err) { // eslint-disable-next-line no-console console.error('error in mw dispatch or next call, probably in middlware/reducer/render fn:', err); monitor$.next({ action: action, err: err, op: 'nextError' }); } // at this point, action is the transformed action, not original monitor$.next({ nextAction: action, op: 'bottom' }); }); return { action$: actionOut$, sub: newSub, logicCount: startLogicCount + arrLogic.length }; } /** * Implement default names for logic using type and idx * @param {object} logic named or unnamed logic object * @param {number} idx index in the logic array * @return {object} namedLogic named logic */ function naming(logic, idx) { if (logic.name) { return logic; } return _objectSpread(_objectSpread({}, logic), {}, { name: "L(".concat(stringifyType(logic.type), ")-").concat(idx) }); } /** Find duplicates in arrLogic by checking if ref to same logic object @param {array} arrLogic array of logic to check @return {array} array of indexes to duplicates, empty array if none */ function findDuplicates(arrLogic) { return arrLogic.reduce(function (acc, x1, idx1) { if (arrLogic.some(function (x2, idx2) { return idx1 !== idx2 && x1 === x2; })) { acc.push(idx1); } return acc; }, []); }