diff --git a/client/__tests__/reducers/undoable.test.ts b/client/__tests__/reducers/undoable.test.ts index 86e787e5..d58913cc 100644 --- a/client/__tests__/reducers/undoable.test.ts +++ b/client/__tests__/reducers/undoable.test.ts @@ -1,10 +1,12 @@ +import { Reducer } from "redux"; import undoable from "../../src/reducers/undoable"; describe("create", () => { test("no keys", () => { - // @ts-expect-error ts-migrate(2554) FIXME: Expected 2-3 arguments, but got 1. - expect(() => undoable(() => {})).toThrow(); - expect(() => undoable(() => {}, null)).toThrow(); + expect(() => + undoable(() => {}, undefined as unknown as string[]) + ).toThrow(); + expect(() => undoable(() => {}, null as unknown as string[])).toThrow(); expect(() => undoable(() => {}, [])).toThrow(); expect(() => undoable(() => {}, [], {})).toThrow(); }); @@ -24,8 +26,7 @@ describe("create", () => { describe("undo", () => { test("expected state modifications", () => { const initialState = { a: 0, b: 1000 }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. - const reducer = (state: any) => ({ a: state.a + 1, b: state.b + 1 }); + const reducer: Reducer = (state) => ({ a: state.a + 1, b: state.b + 1 }); const undoableReducer = undoable(reducer, ["a"]); const s1 = undoableReducer(initialState, { type: "test" }); @@ -43,10 +44,8 @@ describe("undo", () => { describe("redo", () => { const initialState = { a: 0, b: 1000 }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. - const reducer = (state: any) => ({ a: state.a + 1, b: state.b + 1 }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. - let UR: any; + const reducer: Reducer = (state) => ({ a: state.a + 1, b: state.b + 1 }); + let UR: Reducer; beforeEach(() => { UR = undoable(reducer, ["a"]); diff --git a/client/src/reducers/undoable.ts b/client/src/reducers/undoable.ts index f9269989..225b958e 100644 --- a/client/src/reducers/undoable.ts +++ b/client/src/reducers/undoable.ts @@ -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. */ +import { Reducer, AnyAction } from "redux"; import fromEntries from "../util/fromEntries"; -const historyKeyPrefix = "@@undoable/"; -const pastKey = `${historyKeyPrefix}past`; -const futureKey = `${historyKeyPrefix}future`; -const filterStateKey = `${historyKeyPrefix}filterState`; -const filterActionKey = `${historyKeyPrefix}filterAction`; -const pendingKey = `${historyKeyPrefix}pending`; +export const pastKey = "@@undoable/past"; +export const futureKey = "@@undoable/future"; +export const filterStateKey = "@@undoable/filterState"; +export const filterActionKey = "@@undoable/filterAction"; +export const pendingKey = "@@undoable/pending"; 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. -const Undoable = (reducer: any, undoableKeys: any, options = {}) => { - // @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 UndoableFilterState { + [name: string]: unknown; +} + +export interface UndoableConfig { + debug?: boolean | number; + historyLimit?: number; + actionFilter?: ActionFilterFn; +} + +export interface UndoableAction { + [filterActionKey]: string; + [filterStateKey]?: FilterStateType; +} + +export type ActionFilterFn = ( + undoableState: UndoableState, + action: AnyAction, + filterState?: FilterStateType +) => UndoableAction; + +export interface UndoableState { + [pastKey]: [string, unknown][][]; + [futureKey]: [string, unknown][][]; + [pendingKey]: [string, unknown][] | null; + [filterStateKey]: FilterStateType | undefined; +} + +const Undoable = ( + reducer: Reducer, + undoableKeys: string[], + options: UndoableConfig = {} +): Reducer => { + const debug = options?.debug ?? false; let { historyLimit } = options; if (!historyLimit) historyLimit = defaultHistoryLimit; if (historyLimit > 0) historyLimit = -historyLimit; - const actionFilter = - // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. - (options as any).actionFilter || (() => ({ [filterActionKey]: "save" })); + const actionFilter: ActionFilterFn = + options?.actionFilter ?? (() => ({ [filterActionKey]: "save" })); if (!Array.isArray(undoableKeys) || undoableKeys.length === 0) 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 */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. - function undo(currentState: any) { + function undo( + currentState: UndoableState + ): UndoableState { const past = currentState[pastKey]; const future = currentState[futureKey]; if (past.length === 0) return currentState; @@ -89,7 +118,7 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => { undoableKeysSet.has(kv[0]) ); const newPast = [...past]; - const newState = newPast.pop(); + const newState = newPast.pop() || []; const newFuture = push(future, currentUndoableState); const nextState = { ...currentState, @@ -104,8 +133,9 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => { /* Replay future, previously undone. */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. - function redo(currentState: any) { + function redo( + currentState: UndoableState + ): UndoableState { const past = currentState[pastKey] || []; const future = currentState[futureKey] || []; if (future.length === 0) return currentState; @@ -113,7 +143,7 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => { undoableKeysSet.has(kv[0]) ); const newFuture = [...future]; - const newState = newFuture.pop(); + const newState = newFuture.pop() || []; const newPast = push(past, currentUndoableState); const nextState = { ...currentState, @@ -128,13 +158,14 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => { /* 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(currentState: any) { + function clear( + currentState: UndoableState + ): UndoableState { return { ...currentState, [pastKey]: [], [futureKey]: [], - [filterStateKey]: {}, + [filterStateKey]: undefined, [pendingKey]: null, }; } @@ -142,8 +173,11 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => { /* 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(currentState: any, action: any, filterState: any) { + function skip( + currentState: UndoableState, + action: AnyAction, + filterState: UndoableFilterState + ): UndoableState { const past = currentState[pastKey] || []; const future = currentState[futureKey] || []; const pending = currentState[pendingKey]; @@ -160,8 +194,11 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => { /* 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(currentState: any, action: any, filterState: any) { + function save( + currentState: UndoableState, + action: AnyAction, + filterState: UndoableFilterState + ): UndoableState { const past = currentState[pastKey] || []; const currentUndoableState = Object.entries(currentState).filter((kv) => 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. */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. - function stashPending(currentState: any) { + function stashPending( + currentState: UndoableState + ): UndoableState { const currentUndoableState = Object.entries(currentState).filter((kv) => undoableKeysSet.has(kv[0]) ); @@ -195,8 +233,9 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => { /* 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(currentState: any) { + function cancelPending( + currentState: UndoableState + ): UndoableState { return { ...currentState, [pendingKey]: null, @@ -206,10 +245,12 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => { /* 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(currentState: any) { - const past = currentState[pastKey] || []; + function applyPending( + currentState: UndoableState + ): UndoableState { + const past = currentState[pastKey]; const pendingState = currentState[pendingKey]; + if (pendingState === null) return currentState; const newPast = push(past, pendingState, historyLimit); const nextState = { ...currentState, @@ -221,14 +262,13 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => { } return ( - currentState = { + currentState: UndoableState = { [pastKey]: [], [futureKey]: [], - [filterStateKey]: {}, + [filterStateKey]: undefined, [pendingKey]: null, }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. - action: any + action: AnyAction ) => { if (debug > 1) console.log("---- ACTION", 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(arr: any, val: any, limit = undefined) { +function push(arr: T[], val: T, limit?: number) { /* functional array push, with a max length limit to the new array. Like Array.push, except it returns new array and discards as needed diff --git a/client/src/reducers/undoableConfig.ts b/client/src/reducers/undoableConfig.ts index ea961e4f..df0ef1f5 100644 --- a/client/src/reducers/undoableConfig.ts +++ b/client/src/reducers/undoableConfig.ts @@ -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"; -const actionKey = "@@undoable/filterAction"; -const stateKey = "@@undoable/filterState"; - /* these actions will not affect history */ -const skipOnActions = new Set([ +const skipOnActions = new Set([ "annoMatrix: init complete", "url changed", "initial data load start", @@ -58,12 +64,12 @@ const skipOnActions = new Set([ identical, repeated occurances of these action types will be debounced. Entire action must be identical (all keys). */ -const debounceOnActions = new Set([]); +const debounceOnActions = new Set([]); /* history will be cleared when these actions occur */ -const clearOnActions = new Set([ +const clearOnActions = new Set([ "initial data load complete", "initial data load error", ]); @@ -71,7 +77,7 @@ const clearOnActions = new Set([ /* An immediate history save will be done for these */ -const saveOnActions = new Set([ +const saveOnActions = new Set([ "categorical metadata filter select", "categorical metadata filter deselect", "categorical metadata filter all of these", @@ -119,35 +125,43 @@ StateMachine - processing complex action handling - see FSM graph for actual structure, in undoableFsm.js **/ +interface MyFilterState extends UndoableFilterState { + prevAction?: AnyAction; + fsm: StateMachine | null; +} +type MyUndoableAction = UndoableAction; + /* Default FSM actions. Used to side-effect transitions in the graph. See graph definition for the transitions that use each. Signature: (fsm, transition, reducerState, reducerAction) => undoableAction */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. -const stashPending = (fsm: any) => ({ - [actionKey]: "stashPending", - [stateKey]: { fsm }, +const stashPending: FsmActionFn = ( + fsm: StateMachine +) => ({ + [filterActionKey]: "stashPending", + [filterStateKey]: { fsm }, }); -const cancelPending = () => ({ - [actionKey]: "cancelPending", - [stateKey]: { fsm: null }, +const cancelPending: FsmActionFn = () => ({ + [filterActionKey]: "cancelPending", + [filterStateKey]: { fsm: null }, }); -const applyPending = () => ({ - [actionKey]: "applyPending", - [stateKey]: { fsm: null }, +const applyPending: FsmActionFn = () => ({ + [filterActionKey]: "applyPending", + [filterStateKey]: { fsm: null }, }); -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'fsm' implicitly has an 'any' type. -const skip = (fsm, transition) => ({ - [actionKey]: "skip", - [stateKey]: { fsm: transition.to !== "done" ? fsm : null }, +const skip: FsmActionFn = (fsm, transition) => ({ + [filterActionKey]: "skip", + [filterStateKey]: { fsm: transition.to !== "done" ? fsm : null }, }); -const clear = () => ({ [actionKey]: "clear", [stateKey]: { fsm: null } }); -// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'fsm' implicitly has an 'any' type. -const save = (fsm, transition) => ({ - [actionKey]: "save", - [stateKey]: { fsm: transition.to !== "done" ? fsm : null }, +const clear: FsmActionFn = () => ({ + [filterActionKey]: "clear", + [filterStateKey]: { fsm: null }, +}); +const save: FsmActionFn = (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 */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. -const onFsmError = (fsm: any, event: any, from: any) => { +const onFsmError: FsmErrorFn = (fsm, event, from) => { console.error(`FSM error [event: "${event}", state: "${from}"]`, fsm); // 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 ); /* State machine we clone whenever we need to run it */ -const seedFsm = new StateMachine("init", fsmTransitions, onFsmError); +const seedFsm = new StateMachine( + "init", + fsmTransitions, + onFsmError +); /* See undoable.js for description action filter interface description. @@ -185,53 +205,52 @@ Basic approach: * only implement complex state machines where absolutely required (eg, multi-event selection and the like) */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. -const actionFilter = (debug: any) => ( - // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. - state: any, - // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. - action: any, - // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. - prevFilterState: any -) => { - const actionType = action.type; - const filterState = { - ...prevFilterState, - prevAction: action, - }; - if (skipOnActions.has(actionType)) { - return { [actionKey]: "skip", [stateKey]: filterState }; - } - if ( - // @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 - debounceOnActions.has(actionType) && - shallowObjectEq(action, prevFilterState.prevAction) - ) { - return { [actionKey]: "skip", [stateKey]: filterState }; - } - if (clearOnActions.has(actionType)) { - return { [actionKey]: "clear", [stateKey]: filterState }; - } - if (saveOnActions.has(actionType)) { - return { [actionKey]: "save", [stateKey]: filterState }; - } +const actionFilter = + (debug: boolean) => + ( + state: UndoableState, + action: AnyAction, + prevFilterState: MyFilterState | undefined + ): UndoableAction => { + const actionType = action.type; + prevFilterState = prevFilterState || { fsm: null }; + const filterState: MyFilterState = { + ...prevFilterState, + prevAction: action, + }; + if (skipOnActions.has(actionType)) { + return { [filterActionKey]: "skip", [filterStateKey]: filterState }; + } + if ( + debounceOnActions.has(actionType) && + prevFilterState.prevAction && + shallowObjectEq(action, prevFilterState.prevAction) + ) { + return { [filterActionKey]: "skip", [filterStateKey]: filterState }; + } + if (clearOnActions.has(actionType)) { + return { [filterActionKey]: "clear", [filterStateKey]: filterState }; + } + if (saveOnActions.has(actionType)) { + return { [filterActionKey]: "save", [filterStateKey]: filterState }; + } - /* + /* Else, something more complex OR unknown to us.... */ - if (seedFsm.events.has(actionType)) { - let { fsm } = filterState; - if (!fsm) { - /* no active FSM, so create one in init state */ - fsm = seedFsm.clone("init"); + if (seedFsm.events.has(actionType)) { + let { fsm } = filterState; + if (!fsm) { + /* no active FSM, so create one in init state */ + 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 */ - if (debug) console.log("**** ACTION FILTER EVENT HANDLER MISS", actionType); - return { [actionKey]: "skip", [stateKey]: filterState }; -}; + /* else, we have no idea what this is - skip it */ + if (debug) console.log("**** ACTION FILTER EVENT HANDLER MISS", actionType); + return { [filterActionKey]: "skip", [filterStateKey]: filterState }; + }; /* 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 - all values are strict equal (===) */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. -function shallowObjectEq(objA: any, objB: any) { +function shallowObjectEq( + objA: Record, + objB: Record +) { if (objA === objB) return true; if (!objA || !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, in the same order. */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. -function shallowArrayEq(arrA: any, arrB: any) { +function shallowArrayEq(arrA: unknown[], arrB: unknown[]) { if (arrA.length !== arrB.length) return false; for (let i = 0, l = arrA.length; i < l; i += 1) { 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). */ const debug = false; -const undoableConfig = { +const undoableConfig: UndoableConfig = { debug, historyLimit: 50, // maximum history size actionFilter: actionFilter(debug), @@ -307,7 +327,7 @@ if (debug) { ); if (trivialOverlapWithFsm.size > 0) { console.error( - "Undoable misconfiguration - trivival action filter blocking FSM filter", + "Undoable misconfiguration - trivial action filter blocking FSM filter", [...trivialOverlapWithFsm] ); } diff --git a/client/src/reducers/undoableFsm.ts b/client/src/reducers/undoableFsm.ts index 642145fa..7d6b0a35 100644 --- a/client/src/reducers/undoableFsm.ts +++ b/client/src/reducers/undoableFsm.ts @@ -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. -const createFsmTransitions = ( - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS. - stashPending: any, - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS. - cancelPending: any, - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS. - applyPending: any, - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS. - skip: any, - // @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 -) => [ +import { StateMachine, FsmTransition, FsmActionFn } from "../util/statemachine"; + +const createFsmTransitions = ( + stashPending: FsmActionFn, + cancelPending: FsmActionFn, + applyPending: FsmActionFn, + skip: FsmActionFn, + _clear: FsmActionFn, + save: FsmActionFn +): FsmTransition[] => [ /* graph selection brushing */ { event: "graph brush start", @@ -52,12 +46,14 @@ const createFsmTransitions = ( from: "graph brush in progress", to: "done", /* if current selection is all, cancelPending. Else, applyPending */ - // @ts-expect-error ts-migrate(6133) FIXME: 'fsm' is declared but its value is never read. - // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. - action: (fsm: any, transition: any, data: any) => + action: ( + fsm: StateMachine, + transition: FsmTransition, + 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" - ? cancelPending() - : applyPending(), + ? cancelPending(fsm, transition, data) + : applyPending(fsm, transition, data), }, { event: "graph brush end", @@ -84,12 +80,14 @@ const createFsmTransitions = ( from: "graph lasso in progress", to: "done", /* if current selection is all, cancelPending. Else, applyPending */ - // @ts-expect-error ts-migrate(6133) FIXME: 'fsm' is declared but its value is never read. - // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. - action: (fsm: any, transition: any, data: any) => + action: ( + fsm: StateMachine, + transition: FsmTransition, + 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" - ? cancelPending() - : applyPending(), + ? cancelPending(fsm, transition, data) + : applyPending(fsm, transition, data), }, { event: "graph lasso end", diff --git a/client/src/util/statemachine/index.ts b/client/src/util/statemachine/index.ts index 05e7edd7..881bcfc6 100644 --- a/client/src/util/statemachine/index.ts +++ b/client/src/util/statemachine/index.ts @@ -13,18 +13,17 @@ Where: to: state_name_transitioning_to, from: state_name_transitioning_from, 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 may be stored in the transition object for use by the action callback. * onErrorCallback - a callback function called if the FSM receives an event for which it has no defined transition. - 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. - * 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] * clone() - clone the entire statemachine. * 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") }); 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. - graph: any; +export type FsmState = number | string; +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. - onError: any; +export type FsmActionFn = ( + fsm: StateMachine, + transition: FsmTransition, + data: unknown +) => ActionReturnType; - // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. - state: any; +export interface FsmTransition { + from: FsmState; + to: FsmState; + event: FsmEvent; + action: FsmActionFn; +} - // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. - states: any; +export type FsmErrorFn = ( + fsm: StateMachine, + 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. - constructor(initState: any, transitions: any, onError: any) { - this.onError = onError || (() => undefined); +export class StateMachine { + events: Set; + + graph: Map>>; + + onError: FsmErrorFn; + + state: FsmState; + + states: Set; + + constructor( + initState: FsmState, + transitions: FsmTransition[], + onError: FsmErrorFn + ) { + this.onError = onError; this.state = initState; // all states this.states = new Set( - // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. - transitions.reduce((names: any, tsn: any) => { - names.push(tsn.from); - names.push(tsn.to); - return names; - }, []) + transitions.reduce( + (names: Array, tsn: FsmTransition) => { + names.push(tsn.from); + names.push(tsn.to); + return names; + }, + [] + ) ); // 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(transitions.map((tsn: any) => tsn.event)); + this.events = new Set( + transitions.map((tsn: FsmTransition) => tsn.event) + ); // the transition graph. // 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: any, tsn: any) => { + this.graph = transitions.reduce((graph, tsn) => { const { event, from } = tsn; if (!graph.has(event)) graph.set(event, new Map()); const tsnMap = graph.get(event); @@ -89,29 +110,23 @@ export default class StateMachine { }, 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: any) { - // @ts-expect-error ts-migrate(2554) FIXME: Expected 3 arguments, but got 2. - const fsm = new StateMachine(initState, []); - fsm.onError = this.onError; + clone(initState: FsmState): StateMachine { + const fsm = new StateMachine(initState, [], this.onError); fsm.states = this.states; fsm.events = this.events; fsm.graph = this.graph; 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: any, data: any) { + next(event: FsmEvent, data: unknown): ActionReturnType { const { graph, state } = this; 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); - if (!transition) return this.onError(this, event, state, undefined); + if (!transition) return this.onError(this, event, state); this.state = transition.to; - return transition.action - ? transition.action(this, transition, data) - : undefined; + return transition.action(this, transition, data); } }