Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Strengthen fsm types #205

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,35 +31,70 @@ export type Action<StatesT> = StatesT | ActionFn<StatesT>;

// State handlers are objects that map events to actions
// or lifecycle functions to handlers
export type StateHandler<StatesT extends string, EventsT extends string> = {
export type StateHandler<
StatesT extends string,
EventsT extends string,
EventsMapT extends { [K in string]: unknown[] },
> = {
[e in EventsT]?: Action<StatesT>;
} & {
[k in FSMLifecycle]?: FSMLifecycleFn<StatesT, EventsT>;
[e in keyof EventsMapT]?: (...args: EventsMapT[e]) => StatesT | void;
} & {
[k in FSMLifecycle]?: <E extends null | Event | keyof EventsMapT>(
meta: E extends null
? {
from: StatesT | null;
to: StatesT;
event: null;
}
: E extends Event
? {
from: StatesT | null;
to: StatesT;
event: E;
}
: E extends keyof EventsMapT
? {
from: StatesT | null;
to: StatesT;
event: E;
args: EventsMapT[E];
}
: never
) => void;
};

export type Transition<StatesT extends string, EventsT extends string> = {
[s in StatesT]: StateHandler<StatesT, EventsT>;
export type Transition<
StatesT extends string,
EventsT extends string,
EventsMapT extends { [K in string]: unknown[] },
> = {
[s in StatesT]?: StateHandler<StatesT, EventsT, EventsMapT>;
} & {
// '*' is a special fallback handler state.
// If no handler is found on the current state and the '*' state exists,
// the handler from the '*' state will be used.
// We can't put the '*' in the same object, has to be an intersection or
// the typescript compiler will complain about mapped types not being
// able to declare properties or methods
"*"?: StateHandler<StatesT, EventsT>;
"*"?: StateHandler<StatesT, EventsT, EventsMapT>;
};

/**
* Defines a strongly-typed finite state machine.
*
* @see {@link https://runed.dev/docs/utilities/finite-state-machine}
*/
export class FiniteStateMachine<StatesT extends string, EventsT extends string> {
export class FiniteStateMachine<
StatesT extends string,
EventsT extends string,
EventsMapT extends { [K in string]: unknown[] },
> {
#current: StatesT = $state()!;
readonly states: Transition<StatesT, EventsT>;
#timeout: Partial<Record<EventsT, NodeJS.Timeout>> = {};
readonly states: Transition<StatesT, EventsT, EventsMapT>;
#timeout: Partial<Record<EventsT | keyof EventsMapT, NodeJS.Timeout>> = {};

constructor(initial: StatesT, states: Transition<StatesT, EventsT>) {
constructor(initial: StatesT, states: Transition<StatesT, EventsT, EventsMapT>) {
this.#current = initial;
this.states = states;

Expand All @@ -70,14 +105,14 @@ export class FiniteStateMachine<StatesT extends string, EventsT extends string>
this.#dispatch("_enter", { from: null, to: initial, event: null, args: [] });
}

#transition(newState: StatesT, event: EventsT, args: unknown[]) {
#transition(newState: StatesT, event: EventsT | keyof EventsMapT, args: unknown[]) {
const metadata = { from: this.#current, to: newState, event, args };
this.#dispatch("_exit", metadata);
this.#current = newState;
this.#dispatch("_enter", metadata);
}

#dispatch(event: EventsT | FSMLifecycle, ...args: unknown[]): StatesT | void {
#dispatch(event: EventsT | keyof EventsMapT | FSMLifecycle, ...args: unknown[]): StatesT | void {
const action = this.states[this.#current]?.[event] ?? this.states["*"]?.[event];
if (action instanceof Function) {
if (event === "_enter" || event === "_exit") {
Expand All @@ -97,7 +132,10 @@ export class FiniteStateMachine<StatesT extends string, EventsT extends string>
}

/** Triggers a new event and returns the new state. */
send(event: EventsT, ...args: unknown[]): StatesT {
send<E extends EventsT | keyof EventsMapT>(
event: E,
...args: E extends keyof EventsMapT ? EventsMapT[E] : never[]
): StatesT {
const newState = this.#dispatch(event, ...args);
if (newState && newState !== this.#current) {
this.#transition(newState as StatesT, event, args);
Expand All @@ -106,7 +144,11 @@ export class FiniteStateMachine<StatesT extends string, EventsT extends string>
}

/** Debounces the triggering of an event. */
async debounce(wait: number = 500, event: EventsT, ...args: unknown[]): Promise<StatesT> {
async debounce<E extends EventsT | keyof EventsMapT>(
wait: number = 500,
event: E,
...args: E extends keyof EventsMapT ? EventsMapT[E] : never[]
): Promise<StatesT> {
if (this.#timeout[event]) {
clearTimeout(this.#timeout[event]);
}
Expand Down