undox icon indicating copy to clipboard operation
undox copied to clipboard

feature request: limit, filter, preserve

Open Buggytheclown opened this issue 3 years ago • 3 comments

First of all, thanks for your work, it helped us a lot. We start using undox in prod.

Features I am missing:

  1. limit - some reducers are tricky and take a little time to execute. Undo takes almost 60ms for a 200-action history. The user can perform 20 actions per minute.
  2. filter - I don't want to include some actions (for example, open / close a menu) in the history
  3. preserve - for example FETCH_SUCCESS action must be in history to replay the history correctly. But I don't want to let the user undo the FETCH_SUCCESS action.

As a work around i wrap the undox reducer. Let me know if you open to Pull requests

import { UndoxTypes, Action, Reducer, UndoxState } from 'undox';
import _flow from 'lodash/flow';

function getHistoryAction<T, S extends UndoxState<T, A>, A extends Action>({
  state,
  offset,
}: {
  state: S;
  offset: number;
}) {
  return state.history[state.index + offset];
}

type PreserveConfig = { preserve?: (action: Action | Action[]) => boolean };
const undoxEnhancerPreserve = <T, S extends UndoxState<T, A>, A extends Action>({
  preserve = () => false,
}: PreserveConfig) => (reducer: Reducer<S, A>) => (state: S, action: A) => {
  if (
    state &&
    action.type === UndoxTypes.UNDO &&
    preserve(getHistoryAction({ state, offset: 0 }))
  ) {
    return state;
  }
  return reducer(state, action);
};

type FilterConfig = { filter?: (action: Action) => boolean };
const undoxEnhancerFilter = <T, S extends UndoxState<T, A>, A extends Action>({
  filter = () => true,
}: FilterConfig) => (reducer: Reducer<S, A>) => (state: S, action: A) => {
  if (!state || Object.values(UndoxTypes).includes(action.type) || filter(action)) {
    return reducer(state, action);
  }

  const newState = reducer(state, action);
  const oldUndoxState = { index: state.index, history: state.history };
  return {
    ...newState,
    ...oldUndoxState,
  };
};

type LimitConfig = { limit?: { maxCount: number; resetStateAction: string } };
const undoxEnhancerLimit = <T, S extends UndoxState<T, A>, A extends Action>({
  limit,
}: LimitConfig) => (reducer: Reducer<S, A>) => (state: S, action: A) => {
  const newState = reducer(state, action);
  if (limit && newState.history.length > limit.maxCount) {
    const limitedActionsHistoryLength = Math.floor(limit.maxCount / 2);

    const removedActionsHistory = newState.history.slice(
      0,
      newState.history.length - limitedActionsHistoryLength,
    );
    const removedActionsHistoryState = (removedActionsHistory.flat() as A[]).reduce(
      reducer,
      undefined,
    );

    const preservedActionsHistory = newState.history.slice(
      newState.history.length - limitedActionsHistoryLength,
    );
    const newHistory = [
      { type: limit.resetStateAction, payload: removedActionsHistoryState?.present },
      ...preservedActionsHistory,
    ];

    return {
      ...newState,
      history: newHistory,
      index: newHistory.length - 1,
    };
  }
  return newState;
};

export const undoxEnhancer = <S, A extends Action>(
  reducer: Reducer<S, A>,
  config: PreserveConfig & FilterConfig & LimitConfig = {},
) => {
  return _flow(
    undoxEnhancerPreserve(config),
    undoxEnhancerFilter(config),
    undoxEnhancerLimit(config),
  )(reducer);
};

  const reducer = undoxEnhancer(undox(counterReducer, init()), {
    limit: { maxCount: 6, resetStateAction: actionTypes.REINIT },
    filter: ({ type }) => HISTORICAL_ACTIONS_TYPES.has(type),
    preserve: ({ type }) => CAN_NOT_UNDO_ACTIONS_TYPES.has(type),
  });

Buggytheclown avatar Jan 15 '21 09:01 Buggytheclown