undoable TS typing (#2374)

* type undoable-related TS

* style change to type declaration
This commit is contained in:
Bruce Martin
2021-08-12 17:14:09 -07:00
committed by GitHub
parent 925b785b1f
commit 4b417cb5a5
5 changed files with 266 additions and 195 deletions
+8 -9
View File
@@ -1,10 +1,12 @@
import { Reducer } from "redux";
import undoable from "../../src/reducers/undoable"; import undoable from "../../src/reducers/undoable";
describe("create", () => { describe("create", () => {
test("no keys", () => { test("no keys", () => {
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2-3 arguments, but got 1. expect(() =>
expect(() => undoable(() => {})).toThrow(); undoable(() => {}, undefined as unknown as string[])
expect(() => undoable(() => {}, null)).toThrow(); ).toThrow();
expect(() => undoable(() => {}, null as unknown as string[])).toThrow();
expect(() => undoable(() => {}, [])).toThrow(); expect(() => undoable(() => {}, [])).toThrow();
expect(() => undoable(() => {}, [], {})).toThrow(); expect(() => undoable(() => {}, [], {})).toThrow();
}); });
@@ -24,8 +26,7 @@ describe("create", () => {
describe("undo", () => { describe("undo", () => {
test("expected state modifications", () => { test("expected state modifications", () => {
const initialState = { a: 0, b: 1000 }; const initialState = { a: 0, b: 1000 };
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. const reducer: Reducer = (state) => ({ a: state.a + 1, b: state.b + 1 });
const reducer = (state: any) => ({ a: state.a + 1, b: state.b + 1 });
const undoableReducer = undoable(reducer, ["a"]); const undoableReducer = undoable(reducer, ["a"]);
const s1 = undoableReducer(initialState, { type: "test" }); const s1 = undoableReducer(initialState, { type: "test" });
@@ -43,10 +44,8 @@ describe("undo", () => {
describe("redo", () => { describe("redo", () => {
const initialState = { a: 0, b: 1000 }; const initialState = { a: 0, b: 1000 };
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. const reducer: Reducer = (state) => ({ a: state.a + 1, b: state.b + 1 });
const reducer = (state: any) => ({ a: state.a + 1, b: state.b + 1 }); let UR: Reducer;
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
let UR: any;
beforeEach(() => { beforeEach(() => {
UR = undoable(reducer, ["a"]); UR = undoable(reducer, ["a"]);
+79 -40
View File
@@ -51,27 +51,55 @@ history state processing. The undoable action object contents, by key:
filter state are entirely at the discretion of the action filter. filter state are entirely at the discretion of the action filter.
*/ */
import { Reducer, AnyAction } from "redux";
import fromEntries from "../util/fromEntries"; import fromEntries from "../util/fromEntries";
const historyKeyPrefix = "@@undoable/"; export const pastKey = "@@undoable/past";
const pastKey = `${historyKeyPrefix}past`; export const futureKey = "@@undoable/future";
const futureKey = `${historyKeyPrefix}future`; export const filterStateKey = "@@undoable/filterState";
const filterStateKey = `${historyKeyPrefix}filterState`; export const filterActionKey = "@@undoable/filterAction";
const filterActionKey = `${historyKeyPrefix}filterAction`; export const pendingKey = "@@undoable/pending";
const pendingKey = `${historyKeyPrefix}pending`;
const defaultHistoryLimit = -100; const defaultHistoryLimit = -100;
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS. export interface UndoableFilterState {
const Undoable = (reducer: any, undoableKeys: any, options = {}) => { [name: string]: unknown;
// @ts-expect-error ts-migrate(2339) FIXME: Property 'debug' does not exist on type '{}'. }
const { debug } = options;
// @ts-expect-error ts-migrate(2339) FIXME: Property 'historyLimit' does not exist on type '{}... Remove this comment to see the full error message export interface UndoableConfig<FilterStateType extends UndoableFilterState> {
debug?: boolean | number;
historyLimit?: number;
actionFilter?: ActionFilterFn<FilterStateType>;
}
export interface UndoableAction<FilterStateType extends UndoableFilterState> {
[filterActionKey]: string;
[filterStateKey]?: FilterStateType;
}
export type ActionFilterFn<FilterStateType extends UndoableFilterState> = (
undoableState: UndoableState<FilterStateType>,
action: AnyAction,
filterState?: FilterStateType
) => UndoableAction<FilterStateType>;
export interface UndoableState<FilterStateType extends UndoableFilterState> {
[pastKey]: [string, unknown][][];
[futureKey]: [string, unknown][][];
[pendingKey]: [string, unknown][] | null;
[filterStateKey]: FilterStateType | undefined;
}
const Undoable = <FilterStateType extends UndoableFilterState>(
reducer: Reducer,
undoableKeys: string[],
options: UndoableConfig<FilterStateType> = {}
): Reducer => {
const debug = options?.debug ?? false;
let { historyLimit } = options; let { historyLimit } = options;
if (!historyLimit) historyLimit = defaultHistoryLimit; if (!historyLimit) historyLimit = defaultHistoryLimit;
if (historyLimit > 0) historyLimit = -historyLimit; if (historyLimit > 0) historyLimit = -historyLimit;
const actionFilter = const actionFilter: ActionFilterFn<FilterStateType> =
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. options?.actionFilter ?? (() => ({ [filterActionKey]: "save" }));
(options as any).actionFilter || (() => ({ [filterActionKey]: "save" }));
if (!Array.isArray(undoableKeys) || undoableKeys.length === 0) if (!Array.isArray(undoableKeys) || undoableKeys.length === 0)
throw new Error("undoable keys array must be specified"); throw new Error("undoable keys array must be specified");
@@ -80,8 +108,9 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
/* /*
Undo the current to previous history Undo the current to previous history
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. function undo(
function undo(currentState: any) { currentState: UndoableState<FilterStateType>
): UndoableState<FilterStateType> {
const past = currentState[pastKey]; const past = currentState[pastKey];
const future = currentState[futureKey]; const future = currentState[futureKey];
if (past.length === 0) return currentState; if (past.length === 0) return currentState;
@@ -89,7 +118,7 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
undoableKeysSet.has(kv[0]) undoableKeysSet.has(kv[0])
); );
const newPast = [...past]; const newPast = [...past];
const newState = newPast.pop(); const newState = newPast.pop() || [];
const newFuture = push(future, currentUndoableState); const newFuture = push(future, currentUndoableState);
const nextState = { const nextState = {
...currentState, ...currentState,
@@ -104,8 +133,9 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
/* /*
Replay future, previously undone. Replay future, previously undone.
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. function redo(
function redo(currentState: any) { currentState: UndoableState<FilterStateType>
): UndoableState<FilterStateType> {
const past = currentState[pastKey] || []; const past = currentState[pastKey] || [];
const future = currentState[futureKey] || []; const future = currentState[futureKey] || [];
if (future.length === 0) return currentState; if (future.length === 0) return currentState;
@@ -113,7 +143,7 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
undoableKeysSet.has(kv[0]) undoableKeysSet.has(kv[0])
); );
const newFuture = [...future]; const newFuture = [...future];
const newState = newFuture.pop(); const newState = newFuture.pop() || [];
const newPast = push(past, currentUndoableState); const newPast = push(past, currentUndoableState);
const nextState = { const nextState = {
...currentState, ...currentState,
@@ -128,13 +158,14 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
/* /*
Clear the history state. No side-effects on current state. Clear the history state. No side-effects on current state.
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. function clear(
function clear(currentState: any) { currentState: UndoableState<FilterStateType>
): UndoableState<FilterStateType> {
return { return {
...currentState, ...currentState,
[pastKey]: [], [pastKey]: [],
[futureKey]: [], [futureKey]: [],
[filterStateKey]: {}, [filterStateKey]: undefined,
[pendingKey]: null, [pendingKey]: null,
}; };
} }
@@ -142,8 +173,11 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
/* /*
Reduce current action, with no history side-effects Reduce current action, with no history side-effects
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. function skip(
function skip(currentState: any, action: any, filterState: any) { currentState: UndoableState<FilterStateType>,
action: AnyAction,
filterState: UndoableFilterState
): UndoableState<FilterStateType> {
const past = currentState[pastKey] || []; const past = currentState[pastKey] || [];
const future = currentState[futureKey] || []; const future = currentState[futureKey] || [];
const pending = currentState[pendingKey]; const pending = currentState[pendingKey];
@@ -160,8 +194,11 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
/* /*
Save current state in the history, then reduce action. Save current state in the history, then reduce action.
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. function save(
function save(currentState: any, action: any, filterState: any) { currentState: UndoableState<FilterStateType>,
action: AnyAction,
filterState: UndoableFilterState
): UndoableState<FilterStateType> {
const past = currentState[pastKey] || []; const past = currentState[pastKey] || [];
const currentUndoableState = Object.entries(currentState).filter((kv) => const currentUndoableState = Object.entries(currentState).filter((kv) =>
undoableKeysSet.has(kv[0]) undoableKeysSet.has(kv[0])
@@ -181,8 +218,9 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
/* /*
Save current state as pending history change. No other side effects. Save current state as pending history change. No other side effects.
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. function stashPending(
function stashPending(currentState: any) { currentState: UndoableState<FilterStateType>
): UndoableState<FilterStateType> {
const currentUndoableState = Object.entries(currentState).filter((kv) => const currentUndoableState = Object.entries(currentState).filter((kv) =>
undoableKeysSet.has(kv[0]) undoableKeysSet.has(kv[0])
); );
@@ -195,8 +233,9 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
/* /*
Cancel pending history state change. No other side effects. Cancel pending history state change. No other side effects.
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. function cancelPending(
function cancelPending(currentState: any) { currentState: UndoableState<FilterStateType>
): UndoableState<FilterStateType> {
return { return {
...currentState, ...currentState,
[pendingKey]: null, [pendingKey]: null,
@@ -206,10 +245,12 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
/* /*
Push pending state onto the history stack Push pending state onto the history stack
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. function applyPending(
function applyPending(currentState: any) { currentState: UndoableState<FilterStateType>
const past = currentState[pastKey] || []; ): UndoableState<FilterStateType> {
const past = currentState[pastKey];
const pendingState = currentState[pendingKey]; const pendingState = currentState[pendingKey];
if (pendingState === null) return currentState;
const newPast = push(past, pendingState, historyLimit); const newPast = push(past, pendingState, historyLimit);
const nextState = { const nextState = {
...currentState, ...currentState,
@@ -221,14 +262,13 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
} }
return ( return (
currentState = { currentState: UndoableState<FilterStateType> = {
[pastKey]: [], [pastKey]: [],
[futureKey]: [], [futureKey]: [],
[filterStateKey]: {}, [filterStateKey]: undefined,
[pendingKey]: null, [pendingKey]: null,
}, },
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. action: AnyAction
action: any
) => { ) => {
if (debug > 1) console.log("---- ACTION", action.type); if (debug > 1) console.log("---- ACTION", action.type);
const aType = action.type; const aType = action.type;
@@ -288,8 +328,7 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
}; };
}; };
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. function push<T = unknown>(arr: T[], val: T, limit?: number) {
function push(arr: any, val: any, limit = undefined) {
/* /*
functional array push, with a max length limit to the new array. functional array push, with a max length limit to the new array.
Like Array.push, except it returns new array and discards as needed Like Array.push, except it returns new array and discards as needed
+99 -79
View File
@@ -1,13 +1,19 @@
import StateMachine from "../util/statemachine"; import { AnyAction } from "redux";
import { StateMachine, FsmActionFn, FsmErrorFn } from "../util/statemachine";
import {
UndoableConfig,
UndoableState,
UndoableFilterState,
UndoableAction,
filterActionKey,
filterStateKey,
} from "./undoable";
import createFsmTransitions from "./undoableFsm"; import createFsmTransitions from "./undoableFsm";
const actionKey = "@@undoable/filterAction";
const stateKey = "@@undoable/filterState";
/* /*
these actions will not affect history these actions will not affect history
*/ */
const skipOnActions = new Set([ const skipOnActions = new Set<string>([
"annoMatrix: init complete", "annoMatrix: init complete",
"url changed", "url changed",
"initial data load start", "initial data load start",
@@ -58,12 +64,12 @@ const skipOnActions = new Set([
identical, repeated occurances of these action types will be debounced. identical, repeated occurances of these action types will be debounced.
Entire action must be identical (all keys). Entire action must be identical (all keys).
*/ */
const debounceOnActions = new Set([]); const debounceOnActions = new Set<string>([]);
/* /*
history will be cleared when these actions occur history will be cleared when these actions occur
*/ */
const clearOnActions = new Set([ const clearOnActions = new Set<string>([
"initial data load complete", "initial data load complete",
"initial data load error", "initial data load error",
]); ]);
@@ -71,7 +77,7 @@ const clearOnActions = new Set([
/* /*
An immediate history save will be done for these An immediate history save will be done for these
*/ */
const saveOnActions = new Set([ const saveOnActions = new Set<string>([
"categorical metadata filter select", "categorical metadata filter select",
"categorical metadata filter deselect", "categorical metadata filter deselect",
"categorical metadata filter all of these", "categorical metadata filter all of these",
@@ -119,35 +125,43 @@ StateMachine - processing complex action handling - see FSM graph for
actual structure, in undoableFsm.js actual structure, in undoableFsm.js
**/ **/
interface MyFilterState extends UndoableFilterState {
prevAction?: AnyAction;
fsm: StateMachine<MyUndoableAction> | null;
}
type MyUndoableAction = UndoableAction<MyFilterState>;
/* /*
Default FSM actions. Used to side-effect transitions in the graph. Default FSM actions. Used to side-effect transitions in the graph.
See graph definition for the transitions that use each. See graph definition for the transitions that use each.
Signature: (fsm, transition, reducerState, reducerAction) => undoableAction Signature: (fsm, transition, reducerState, reducerAction) => undoableAction
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. const stashPending: FsmActionFn<MyUndoableAction> = (
const stashPending = (fsm: any) => ({ fsm: StateMachine<MyUndoableAction>
[actionKey]: "stashPending", ) => ({
[stateKey]: { fsm }, [filterActionKey]: "stashPending",
[filterStateKey]: { fsm },
}); });
const cancelPending = () => ({ const cancelPending: FsmActionFn<MyUndoableAction> = () => ({
[actionKey]: "cancelPending", [filterActionKey]: "cancelPending",
[stateKey]: { fsm: null }, [filterStateKey]: { fsm: null },
}); });
const applyPending = () => ({ const applyPending: FsmActionFn<MyUndoableAction> = () => ({
[actionKey]: "applyPending", [filterActionKey]: "applyPending",
[stateKey]: { fsm: null }, [filterStateKey]: { fsm: null },
}); });
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'fsm' implicitly has an 'any' type. const skip: FsmActionFn<MyUndoableAction> = (fsm, transition) => ({
const skip = (fsm, transition) => ({ [filterActionKey]: "skip",
[actionKey]: "skip", [filterStateKey]: { fsm: transition.to !== "done" ? fsm : null },
[stateKey]: { fsm: transition.to !== "done" ? fsm : null },
}); });
const clear = () => ({ [actionKey]: "clear", [stateKey]: { fsm: null } }); const clear: FsmActionFn<MyUndoableAction> = () => ({
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'fsm' implicitly has an 'any' type. [filterActionKey]: "clear",
const save = (fsm, transition) => ({ [filterStateKey]: { fsm: null },
[actionKey]: "save", });
[stateKey]: { fsm: transition.to !== "done" ? fsm : null }, const save: FsmActionFn<MyUndoableAction> = (fsm, transition) => ({
[filterActionKey]: "save",
[filterStateKey]: { fsm: transition.to !== "done" ? fsm : null },
}); });
/* /*
@@ -156,11 +170,13 @@ StateMachine when it doesn't know what to do.
Signature: (fsm, event, from) => undoableAction Signature: (fsm, event, from) => undoableAction
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. const onFsmError: FsmErrorFn<MyUndoableAction> = (fsm, event, from) => {
const onFsmError = (fsm: any, event: any, from: any) => {
console.error(`FSM error [event: "${event}", state: "${from}"]`, fsm); console.error(`FSM error [event: "${event}", state: "${from}"]`, fsm);
// In production, try to recover gracefully if we have unexpected state // In production, try to recover gracefully if we have unexpected state
return clear(); return {
[filterActionKey]: "clear",
[filterStateKey]: { fsm: null },
};
}; };
/* /*
@@ -175,7 +191,11 @@ const fsmTransitions = createFsmTransitions(
save save
); );
/* State machine we clone whenever we need to run it */ /* State machine we clone whenever we need to run it */
const seedFsm = new StateMachine("init", fsmTransitions, onFsmError); const seedFsm = new StateMachine<MyUndoableAction>(
"init",
fsmTransitions,
onFsmError
);
/* /*
See undoable.js for description action filter interface description. See undoable.js for description action filter interface description.
@@ -185,53 +205,52 @@ Basic approach:
* only implement complex state machines where absolutely required (eg, * only implement complex state machines where absolutely required (eg,
multi-event selection and the like) multi-event selection and the like)
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. const actionFilter =
const actionFilter = (debug: any) => ( (debug: boolean) =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. (
state: any, state: UndoableState<MyFilterState>,
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. action: AnyAction,
action: any, prevFilterState: MyFilterState | undefined
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. ): UndoableAction<MyFilterState> => {
prevFilterState: any const actionType = action.type;
) => { prevFilterState = prevFilterState || { fsm: null };
const actionType = action.type; const filterState: MyFilterState = {
const filterState = { ...prevFilterState,
...prevFilterState, prevAction: action,
prevAction: action, };
}; if (skipOnActions.has(actionType)) {
if (skipOnActions.has(actionType)) { return { [filterActionKey]: "skip", [filterStateKey]: filterState };
return { [actionKey]: "skip", [stateKey]: filterState }; }
} if (
if ( debounceOnActions.has(actionType) &&
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'any' is not assignable to parame... Remove this comment to see the full error message prevFilterState.prevAction &&
debounceOnActions.has(actionType) && shallowObjectEq(action, prevFilterState.prevAction)
shallowObjectEq(action, prevFilterState.prevAction) ) {
) { return { [filterActionKey]: "skip", [filterStateKey]: filterState };
return { [actionKey]: "skip", [stateKey]: filterState }; }
} if (clearOnActions.has(actionType)) {
if (clearOnActions.has(actionType)) { return { [filterActionKey]: "clear", [filterStateKey]: filterState };
return { [actionKey]: "clear", [stateKey]: filterState }; }
} if (saveOnActions.has(actionType)) {
if (saveOnActions.has(actionType)) { return { [filterActionKey]: "save", [filterStateKey]: filterState };
return { [actionKey]: "save", [stateKey]: filterState }; }
}
/* /*
Else, something more complex OR unknown to us.... Else, something more complex OR unknown to us....
*/ */
if (seedFsm.events.has(actionType)) { if (seedFsm.events.has(actionType)) {
let { fsm } = filterState; let { fsm } = filterState;
if (!fsm) { if (!fsm) {
/* no active FSM, so create one in init state */ /* no active FSM, so create one in init state */
fsm = seedFsm.clone("init"); fsm = seedFsm.clone("init");
}
return fsm.next(action.type, { state, action });
} }
return fsm.next(action.type, { state, action });
}
/* else, we have no idea what this is - skip it */ /* else, we have no idea what this is - skip it */
if (debug) console.log("**** ACTION FILTER EVENT HANDLER MISS", actionType); if (debug) console.log("**** ACTION FILTER EVENT HANDLER MISS", actionType);
return { [actionKey]: "skip", [stateKey]: filterState }; return { [filterActionKey]: "skip", [filterStateKey]: filterState };
}; };
/* /*
return true if objA and objB are ===, OR if: return true if objA and objB are ===, OR if:
@@ -239,8 +258,10 @@ return true if objA and objB are ===, OR if:
- have same own properties - have same own properties
- all values are strict equal (===) - all values are strict equal (===)
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. function shallowObjectEq(
function shallowObjectEq(objA: any, objB: any) { objA: Record<string | number | symbol, unknown>,
objB: Record<string | number | symbol, unknown>
) {
if (objA === objB) return true; if (objA === objB) return true;
if (!objA || !objB) return false; if (!objA || !objB) return false;
if (!shallowArrayEq(Object.keys(objA), Object.keys(objB))) return false; if (!shallowArrayEq(Object.keys(objA), Object.keys(objB))) return false;
@@ -252,8 +273,7 @@ function shallowObjectEq(objA: any, objB: any) {
return true if arrA and arrB contain the same strict-equal values, return true if arrA and arrB contain the same strict-equal values,
in the same order. in the same order.
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. function shallowArrayEq(arrA: unknown[], arrB: unknown[]) {
function shallowArrayEq(arrA: any, arrB: any) {
if (arrA.length !== arrB.length) return false; if (arrA.length !== arrB.length) return false;
for (let i = 0, l = arrA.length; i < l; i += 1) { for (let i = 0, l = arrA.length; i < l; i += 1) {
if (arrA[i] !== arrB[i]) return false; if (arrA[i] !== arrB[i]) return false;
@@ -268,7 +288,7 @@ Set to true or 1 for base logging, high number for more verbosity (currently onl
or 2). or 2).
*/ */
const debug = false; const debug = false;
const undoableConfig = { const undoableConfig: UndoableConfig<MyFilterState> = {
debug, debug,
historyLimit: 50, // maximum history size historyLimit: 50, // maximum history size
actionFilter: actionFilter(debug), actionFilter: actionFilter(debug),
@@ -307,7 +327,7 @@ if (debug) {
); );
if (trivialOverlapWithFsm.size > 0) { if (trivialOverlapWithFsm.size > 0) {
console.error( console.error(
"Undoable misconfiguration - trivival action filter blocking FSM filter", "Undoable misconfiguration - trivial action filter blocking FSM filter",
[...trivialOverlapWithFsm] [...trivialOverlapWithFsm]
); );
} }
+24 -26
View File
@@ -18,22 +18,16 @@ b) compound actions that should be collapsed into a single history change.
*/ */
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS. import { StateMachine, FsmTransition, FsmActionFn } from "../util/statemachine";
const createFsmTransitions = (
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS. const createFsmTransitions = <ActionReturnType>(
stashPending: any, stashPending: FsmActionFn<ActionReturnType>,
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS. cancelPending: FsmActionFn<ActionReturnType>,
cancelPending: any, applyPending: FsmActionFn<ActionReturnType>,
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS. skip: FsmActionFn<ActionReturnType>,
applyPending: any, _clear: FsmActionFn<ActionReturnType>,
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS. save: FsmActionFn<ActionReturnType>
skip: any, ): FsmTransition<ActionReturnType>[] => [
// @ts-expect-error ts-migrate(6133) FIXME: 'clear' is declared but its value is never read.
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
clear: any,
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
save: any
) => [
/* graph selection brushing */ /* graph selection brushing */
{ {
event: "graph brush start", event: "graph brush start",
@@ -52,12 +46,14 @@ const createFsmTransitions = (
from: "graph brush in progress", from: "graph brush in progress",
to: "done", to: "done",
/* if current selection is all, cancelPending. Else, applyPending */ /* if current selection is all, cancelPending. Else, applyPending */
// @ts-expect-error ts-migrate(6133) FIXME: 'fsm' is declared but its value is never read. action: (
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. fsm: StateMachine<ActionReturnType>,
action: (fsm: any, transition: any, data: any) => transition: FsmTransition<ActionReturnType>,
data: any // eslint-disable-line @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS. Requires state typing.
) =>
data.state.graphSelection.selection.mode === "all" data.state.graphSelection.selection.mode === "all"
? cancelPending() ? cancelPending(fsm, transition, data)
: applyPending(), : applyPending(fsm, transition, data),
}, },
{ {
event: "graph brush end", event: "graph brush end",
@@ -84,12 +80,14 @@ const createFsmTransitions = (
from: "graph lasso in progress", from: "graph lasso in progress",
to: "done", to: "done",
/* if current selection is all, cancelPending. Else, applyPending */ /* if current selection is all, cancelPending. Else, applyPending */
// @ts-expect-error ts-migrate(6133) FIXME: 'fsm' is declared but its value is never read. action: (
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. fsm: StateMachine<ActionReturnType>,
action: (fsm: any, transition: any, data: any) => transition: FsmTransition<ActionReturnType>,
data: any // eslint-disable-line @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS. Requires state typing.
) =>
data.state.graphSelection.selection.mode === "all" data.state.graphSelection.selection.mode === "all"
? cancelPending() ? cancelPending(fsm, transition, data)
: applyPending(), : applyPending(fsm, transition, data),
}, },
{ {
event: "graph lasso end", event: "graph lasso end",
+56 -41
View File
@@ -13,18 +13,17 @@ Where:
to: state_name_transitioning_to, to: state_name_transitioning_to,
from: state_name_transitioning_from, from: state_name_transitioning_from,
event: value_that_will_cause_transition, event: value_that_will_cause_transition,
action: optional_callback_upon_transition action: callback_upon_transition
} }
The transition will be provided to the action callback, so other data The transition will be provided to the action callback, so other data
may be stored in the transition object for use by the action callback. may be stored in the transition object for use by the action callback.
* onErrorCallback - a callback function called if the FSM receives an event * onErrorCallback - a callback function called if the FSM receives an event
for which it has no defined transition. for which it has no defined transition.
Interface: Interface:
* states - property containing the state names. A Set(), contianing the * states - property containing the state names. A Set(), containing the
union of to: and from: values. union of to: and from: values.
* events - property containing all of the accepted event values. Set(). * events - property containing all of the accepted event values. Set().
* graph - a Map of Maps, organized as graph[eventValue][fromStateValue] * graph - a Map of Maps, organized as graph[eventValue][fromStateValue]
* clone() - clone the entire statemachine. * clone() - clone the entire statemachine.
* next(eventValue) - drive the FSM to the next state. If the event * next(eventValue) - drive the FSM to the next state. If the event
@@ -40,47 +39,69 @@ Example:
const fsm = new StateMachine("A", transitions, () => { throw new Error("oops") }); const fsm = new StateMachine("A", transitions, () => { throw new Error("oops") });
fsm.next("yo"); // returns 42 fsm.next("yo"); // returns 42
*/ */
export default class StateMachine {
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
events: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. export type FsmState = number | string;
graph: any; export type FsmEvent = string; // by convention, we assume Events are redux action types, aka strings
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. export type FsmActionFn<ActionReturnType> = (
onError: any; fsm: StateMachine<ActionReturnType>,
transition: FsmTransition<ActionReturnType>,
data: unknown
) => ActionReturnType;
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. export interface FsmTransition<ActionReturnType> {
state: any; from: FsmState;
to: FsmState;
event: FsmEvent;
action: FsmActionFn<ActionReturnType>;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. export type FsmErrorFn<ActionReturnType> = (
states: any; fsm: StateMachine<ActionReturnType>,
event: FsmEvent,
state: FsmState
) => ActionReturnType;
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS. export class StateMachine<ActionReturnType> {
constructor(initState: any, transitions: any, onError: any) { events: Set<FsmEvent>;
this.onError = onError || (() => undefined);
graph: Map<FsmEvent, Map<FsmState, FsmTransition<ActionReturnType>>>;
onError: FsmErrorFn<ActionReturnType>;
state: FsmState;
states: Set<FsmState>;
constructor(
initState: FsmState,
transitions: FsmTransition<ActionReturnType>[],
onError: FsmErrorFn<ActionReturnType>
) {
this.onError = onError;
this.state = initState; this.state = initState;
// all states // all states
this.states = new Set( this.states = new Set(
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. transitions.reduce(
transitions.reduce((names: any, tsn: any) => { (names: Array<FsmState>, tsn: FsmTransition<ActionReturnType>) => {
names.push(tsn.from); names.push(tsn.from);
names.push(tsn.to); names.push(tsn.to);
return names; return names;
}, []) },
[]
)
); );
// all transition names (aka events) // all transition names (aka events)
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. this.events = new Set(
this.events = new Set(transitions.map((tsn: any) => tsn.event)); transitions.map((tsn: FsmTransition<ActionReturnType>) => tsn.event)
);
// the transition graph. // the transition graph.
// graph[event][from] -> transition // graph[event][from] -> transition
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. this.graph = transitions.reduce((graph, tsn) => {
this.graph = transitions.reduce((graph: any, tsn: any) => {
const { event, from } = tsn; const { event, from } = tsn;
if (!graph.has(event)) graph.set(event, new Map()); if (!graph.has(event)) graph.set(event, new Map());
const tsnMap = graph.get(event); const tsnMap = graph.get(event);
@@ -89,29 +110,23 @@ export default class StateMachine {
}, new Map()); }, new Map());
} }
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS. clone(initState: FsmState): StateMachine<ActionReturnType> {
clone(initState: any) { const fsm = new StateMachine<ActionReturnType>(initState, [], this.onError);
// @ts-expect-error ts-migrate(2554) FIXME: Expected 3 arguments, but got 2.
const fsm = new StateMachine(initState, []);
fsm.onError = this.onError;
fsm.states = this.states; fsm.states = this.states;
fsm.events = this.events; fsm.events = this.events;
fsm.graph = this.graph; fsm.graph = this.graph;
return fsm; return fsm;
} }
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS. next(event: FsmEvent, data: unknown): ActionReturnType {
next(event: any, data: any) {
const { graph, state } = this; const { graph, state } = this;
const tsnMap = graph.get(event); const tsnMap = graph.get(event);
if (!tsnMap) return this.onError(this, event, state, undefined); if (!tsnMap) return this.onError(this, event, state);
const transition = tsnMap.get(state); const transition = tsnMap.get(state);
if (!transition) return this.onError(this, event, state, undefined); if (!transition) return this.onError(this, event, state);
this.state = transition.to; this.state = transition.to;
return transition.action return transition.action(this, transition, data);
? transition.action(this, transition, data)
: undefined;
} }
} }