redux-logic
Version:
Redux middleware for organizing all your business logic. Intercept actions and perform async processing.
326 lines (314 loc) • 13.1 kB
JavaScript
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;
}, []);
}