redux-devtools-instrument
Version:
Redux DevTools instrumentation
509 lines (439 loc) • 17.8 kB
JavaScript
;
exports.__esModule = true;
exports.INIT_ACTION = exports.ActionCreators = exports.ActionTypes = undefined;
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; };
exports.liftAction = liftAction;
exports.liftReducerWith = liftReducerWith;
exports.unliftState = unliftState;
exports.unliftStore = unliftStore;
exports.default = instrument;
var _difference = require('lodash/difference');
var _difference2 = _interopRequireDefault(_difference);
var _union = require('lodash/union');
var _union2 = _interopRequireDefault(_union);
var _isPlainObject = require('lodash/isPlainObject');
var _isPlainObject2 = _interopRequireDefault(_isPlainObject);
var _symbolObservable = require('symbol-observable');
var _symbolObservable2 = _interopRequireDefault(_symbolObservable);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var ActionTypes = exports.ActionTypes = {
PERFORM_ACTION: 'PERFORM_ACTION',
RESET: 'RESET',
ROLLBACK: 'ROLLBACK',
COMMIT: 'COMMIT',
SWEEP: 'SWEEP',
TOGGLE_ACTION: 'TOGGLE_ACTION',
SET_ACTIONS_ACTIVE: 'SET_ACTIONS_ACTIVE',
JUMP_TO_STATE: 'JUMP_TO_STATE',
IMPORT_STATE: 'IMPORT_STATE'
};
/**
* Action creators to change the History state.
*/
var ActionCreators = exports.ActionCreators = {
performAction: function performAction(action) {
if (!(0, _isPlainObject2.default)(action)) {
throw new Error('Actions must be plain objects. ' + 'Use custom middleware for async actions.');
}
if (typeof action.type === 'undefined') {
throw new Error('Actions may not have an undefined "type" property. ' + 'Have you misspelled a constant?');
}
return { type: ActionTypes.PERFORM_ACTION, action: action, timestamp: Date.now() };
},
reset: function reset() {
return { type: ActionTypes.RESET, timestamp: Date.now() };
},
rollback: function rollback() {
return { type: ActionTypes.ROLLBACK, timestamp: Date.now() };
},
commit: function commit() {
return { type: ActionTypes.COMMIT, timestamp: Date.now() };
},
sweep: function sweep() {
return { type: ActionTypes.SWEEP };
},
toggleAction: function toggleAction(id) {
return { type: ActionTypes.TOGGLE_ACTION, id: id };
},
setActionsActive: function setActionsActive(start, end) {
var active = arguments.length <= 2 || arguments[2] === undefined ? true : arguments[2];
return { type: ActionTypes.SET_ACTIONS_ACTIVE, start: start, end: end, active: active };
},
jumpToState: function jumpToState(index) {
return { type: ActionTypes.JUMP_TO_STATE, index: index };
},
importState: function importState(nextLiftedState, noRecompute) {
return { type: ActionTypes.IMPORT_STATE, nextLiftedState: nextLiftedState, noRecompute: noRecompute };
}
};
var INIT_ACTION = exports.INIT_ACTION = { type: '@@INIT' };
/**
* Computes the next entry in the log by applying an action.
*/
function computeNextEntry(reducer, action, state, shouldCatchErrors) {
if (!shouldCatchErrors) {
return { state: reducer(state, action) };
}
var nextState = state;
var nextError = void 0;
try {
nextState = reducer(state, action);
} catch (err) {
nextError = err.toString();
if ((typeof window === 'undefined' ? 'undefined' : _typeof(window)) === 'object' && typeof window.chrome !== 'undefined') {
// In Chrome, rethrowing provides better source map support
setTimeout(function () {
throw err;
});
} else {
console.error(err);
}
}
return {
state: nextState,
error: nextError
};
}
/**
* Runs the reducer on invalidated actions to get a fresh computation log.
*/
function recomputeStates(computedStates, minInvalidatedStateIndex, reducer, committedState, actionsById, stagedActionIds, skippedActionIds, shouldCatchErrors) {
// Optimization: exit early and return the same reference
// if we know nothing could have changed.
if (!computedStates || minInvalidatedStateIndex === -1 || minInvalidatedStateIndex >= computedStates.length && computedStates.length === stagedActionIds.length) {
return computedStates;
}
var nextComputedStates = computedStates.slice(0, minInvalidatedStateIndex);
for (var i = minInvalidatedStateIndex; i < stagedActionIds.length; i++) {
var actionId = stagedActionIds[i];
var action = actionsById[actionId].action;
var previousEntry = nextComputedStates[i - 1];
var previousState = previousEntry ? previousEntry.state : committedState;
var shouldSkip = skippedActionIds.indexOf(actionId) > -1;
var entry = void 0;
if (shouldSkip) {
entry = previousEntry;
} else {
if (shouldCatchErrors && previousEntry && previousEntry.error) {
entry = {
state: previousState,
error: 'Interrupted by an error up the chain'
};
} else {
entry = computeNextEntry(reducer, action, previousState, shouldCatchErrors);
}
}
nextComputedStates.push(entry);
}
return nextComputedStates;
}
/**
* Lifts an app's action into an action on the lifted store.
*/
function liftAction(action) {
return ActionCreators.performAction(action);
}
/**
* Creates a history state reducer from an app's reducer.
*/
function liftReducerWith(reducer, initialCommittedState, monitorReducer, options) {
var initialLiftedState = {
monitorState: monitorReducer(undefined, {}),
nextActionId: 1,
actionsById: { 0: liftAction(INIT_ACTION) },
stagedActionIds: [0],
skippedActionIds: [],
committedState: initialCommittedState,
currentStateIndex: 0,
computedStates: []
};
/**
* Manages how the history actions modify the history state.
*/
return function (liftedState, liftedAction) {
var _ref = liftedState || initialLiftedState;
var monitorState = _ref.monitorState;
var actionsById = _ref.actionsById;
var nextActionId = _ref.nextActionId;
var stagedActionIds = _ref.stagedActionIds;
var skippedActionIds = _ref.skippedActionIds;
var committedState = _ref.committedState;
var currentStateIndex = _ref.currentStateIndex;
var computedStates = _ref.computedStates;
if (!liftedState) {
// Prevent mutating initialLiftedState
actionsById = _extends({}, actionsById);
}
function commitExcessActions(n) {
// Auto-commits n-number of excess actions.
var excess = n;
var idsToDelete = stagedActionIds.slice(1, excess + 1);
for (var i = 0; i < idsToDelete.length; i++) {
if (computedStates[i + 1].error) {
// Stop if error is found. Commit actions up to error.
excess = i;
idsToDelete = stagedActionIds.slice(1, excess + 1);
break;
} else {
delete actionsById[idsToDelete[i]];
}
}
skippedActionIds = skippedActionIds.filter(function (id) {
return idsToDelete.indexOf(id) === -1;
});
stagedActionIds = [0].concat(stagedActionIds.slice(excess + 1));
committedState = computedStates[excess].state;
computedStates = computedStates.slice(excess);
currentStateIndex = currentStateIndex > excess ? currentStateIndex - excess : 0;
}
// By default, agressively recompute every state whatever happens.
// This has O(n) performance, so we'll override this to a sensible
// value whenever we feel like we don't have to recompute the states.
var minInvalidatedStateIndex = 0;
switch (liftedAction.type) {
case ActionTypes.RESET:
{
// Get back to the state the store was created with.
actionsById = { 0: liftAction(INIT_ACTION) };
nextActionId = 1;
stagedActionIds = [0];
skippedActionIds = [];
committedState = initialCommittedState;
currentStateIndex = 0;
computedStates = [];
break;
}
case ActionTypes.COMMIT:
{
// Consider the last committed state the new starting point.
// Squash any staged actions into a single committed state.
actionsById = { 0: liftAction(INIT_ACTION) };
nextActionId = 1;
stagedActionIds = [0];
skippedActionIds = [];
committedState = computedStates[currentStateIndex].state;
currentStateIndex = 0;
computedStates = [];
break;
}
case ActionTypes.ROLLBACK:
{
// Forget about any staged actions.
// Start again from the last committed state.
actionsById = { 0: liftAction(INIT_ACTION) };
nextActionId = 1;
stagedActionIds = [0];
skippedActionIds = [];
currentStateIndex = 0;
computedStates = [];
break;
}
case ActionTypes.TOGGLE_ACTION:
{
var _ret = function () {
// Toggle whether an action with given ID is skipped.
// Being skipped means it is a no-op during the computation.
var actionId = liftedAction.id;
var index = skippedActionIds.indexOf(actionId);
if (index === -1) {
skippedActionIds = [actionId].concat(skippedActionIds);
} else {
skippedActionIds = skippedActionIds.filter(function (id) {
return id !== actionId;
});
}
// Optimization: we know history before this action hasn't changed
minInvalidatedStateIndex = stagedActionIds.indexOf(actionId);
return 'break';
}();
if (_ret === 'break') break;
}
case ActionTypes.SET_ACTIONS_ACTIVE:
{
// Toggle whether an action with given ID is skipped.
// Being skipped means it is a no-op during the computation.
var start = liftedAction.start;
var end = liftedAction.end;
var active = liftedAction.active;
var actionIds = [];
for (var i = start; i < end; i++) {
actionIds.push(i);
}if (active) {
skippedActionIds = (0, _difference2.default)(skippedActionIds, actionIds);
} else {
skippedActionIds = (0, _union2.default)(skippedActionIds, actionIds);
}
// Optimization: we know history before this action hasn't changed
minInvalidatedStateIndex = stagedActionIds.indexOf(start);
break;
}
case ActionTypes.JUMP_TO_STATE:
{
// Without recomputing anything, move the pointer that tell us
// which state is considered the current one. Useful for sliders.
currentStateIndex = liftedAction.index;
// Optimization: we know the history has not changed.
minInvalidatedStateIndex = Infinity;
break;
}
case ActionTypes.SWEEP:
{
// Forget any actions that are currently being skipped.
stagedActionIds = (0, _difference2.default)(stagedActionIds, skippedActionIds);
skippedActionIds = [];
currentStateIndex = Math.min(currentStateIndex, stagedActionIds.length - 1);
break;
}
case ActionTypes.PERFORM_ACTION:
{
// Auto-commit as new actions come in.
if (options.maxAge && stagedActionIds.length === options.maxAge) {
commitExcessActions(1);
}
if (currentStateIndex === stagedActionIds.length - 1) {
currentStateIndex++;
}
var _actionId = nextActionId++;
// Mutation! This is the hottest path, and we optimize on purpose.
// It is safe because we set a new key in a cache dictionary.
actionsById[_actionId] = liftedAction;
stagedActionIds = [].concat(stagedActionIds, [_actionId]);
// Optimization: we know that only the new action needs computing.
minInvalidatedStateIndex = stagedActionIds.length - 1;
break;
}
case ActionTypes.IMPORT_STATE:
{
var _liftedAction$nextLif = liftedAction.nextLiftedState;
// Completely replace everything.
monitorState = _liftedAction$nextLif.monitorState;
actionsById = _liftedAction$nextLif.actionsById;
nextActionId = _liftedAction$nextLif.nextActionId;
stagedActionIds = _liftedAction$nextLif.stagedActionIds;
skippedActionIds = _liftedAction$nextLif.skippedActionIds;
committedState = _liftedAction$nextLif.committedState;
currentStateIndex = _liftedAction$nextLif.currentStateIndex;
computedStates = _liftedAction$nextLif.computedStates;
if (liftedAction.noRecompute) {
minInvalidatedStateIndex = Infinity;
}
break;
}
case '@@redux/INIT':
{
// Always recompute states on hot reload and init.
minInvalidatedStateIndex = 0;
if (options.maxAge && stagedActionIds.length > options.maxAge) {
// States must be recomputed before committing excess.
computedStates = recomputeStates(computedStates, minInvalidatedStateIndex, reducer, committedState, actionsById, stagedActionIds, skippedActionIds, options.shouldCatchErrors);
commitExcessActions(stagedActionIds.length - options.maxAge);
// Avoid double computation.
minInvalidatedStateIndex = Infinity;
}
break;
}
default:
{
// If the action is not recognized, it's a monitor action.
// Optimization: a monitor action can't change history.
minInvalidatedStateIndex = Infinity;
break;
}
}
computedStates = recomputeStates(computedStates, minInvalidatedStateIndex, reducer, committedState, actionsById, stagedActionIds, skippedActionIds, options.shouldCatchErrors);
monitorState = monitorReducer(monitorState, liftedAction);
return {
monitorState: monitorState,
actionsById: actionsById,
nextActionId: nextActionId,
stagedActionIds: stagedActionIds,
skippedActionIds: skippedActionIds,
committedState: committedState,
currentStateIndex: currentStateIndex,
computedStates: computedStates
};
};
}
/**
* Provides an app's view into the state of the lifted store.
*/
function unliftState(liftedState) {
var computedStates = liftedState.computedStates;
var currentStateIndex = liftedState.currentStateIndex;
var state = computedStates[currentStateIndex].state;
return state;
}
/**
* Provides an app's view into the lifted store.
*/
function unliftStore(liftedStore, liftReducer) {
var _extends2;
var lastDefinedState = void 0;
function getState() {
var state = unliftState(liftedStore.getState());
if (state !== undefined) {
lastDefinedState = state;
}
return lastDefinedState;
}
return _extends({}, liftedStore, (_extends2 = {
liftedStore: liftedStore,
dispatch: function dispatch(action) {
liftedStore.dispatch(liftAction(action));
return action;
},
getState: getState,
replaceReducer: function replaceReducer(nextReducer) {
liftedStore.replaceReducer(liftReducer(nextReducer));
}
}, _extends2[_symbolObservable2.default] = function () {
return _extends({}, liftedStore[_symbolObservable2.default](), {
subscribe: function subscribe(observer) {
if ((typeof observer === 'undefined' ? 'undefined' : _typeof(observer)) !== 'object') {
throw new TypeError('Expected the observer to be an object.');
}
function observeState() {
if (observer.next) {
observer.next(getState());
}
}
observeState();
var unsubscribe = liftedStore.subscribe(observeState);
return { unsubscribe: unsubscribe };
}
});
}, _extends2));
}
/**
* Redux instrumentation store enhancer.
*/
function instrument() {
var monitorReducer = arguments.length <= 0 || arguments[0] === undefined ? function () {
return null;
} : arguments[0];
var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];
/* eslint-disable no-eq-null */
if (options.maxAge != null && options.maxAge < 2) {
/* eslint-enable */
throw new Error('DevTools.instrument({ maxAge }) option, if specified, ' + 'may not be less than 2.');
}
return function (createStore) {
return function (reducer, initialState, enhancer) {
function liftReducer(r) {
if (typeof r !== 'function') {
if (r && typeof r.default === 'function') {
throw new Error('Expected the reducer to be a function. ' + 'Instead got an object with a "default" field. ' + 'Did you pass a module instead of the default export? ' + 'Try passing require(...).default instead.');
}
throw new Error('Expected the reducer to be a function.');
}
return liftReducerWith(r, initialState, monitorReducer, options);
}
var liftedStore = createStore(liftReducer(reducer), enhancer);
if (liftedStore.liftedStore) {
throw new Error('DevTools instrumentation should not be applied more than once. ' + 'Check your store configuration.');
}
return unliftStore(liftedStore, liftReducer);
};
};
}