Finite State Machine
Type-safe finite state machine implementation
Code
type Transition<State extends string, Event extends string> = [State, Event, State];
interface StateMachineConfig<
State extends string,
Event extends string,
Context = any
> {
initialState: State;
initialContext: Context;
states: Record<State, {
onEntry?: (context: Context) => Context | void;
onExit?: (context: Context) => Context | void;
}>;
transitions: Transition<State, Event>[];
onTransition?: (from: State, to: State, event: Event, context: Context) => void;
}
class StateMachine<
State extends string,
Event extends string,
Context = any
> {
private currentState: State;
private context: Context;
private transitions: Map<State, Map<Event, State>> = new Map();
constructor(private config: StateMachineConfig<State, Event, Context>) {
this.currentState = config.initialState;
this.context = config.initialContext;
this.initializeTransitions();
// Call initial state entry
this.callEntry(this.currentState);
}
private initializeTransitions(): void {
__TOKEN_77__ (const [from, event, to] of this.config.transitions) {
__TOKEN_82__ (!this.transitions.has(from)) {
this.transitions.set(from, new Map());
}
this.transitions.get(from)!.set(event, to);
}
}
private callEntry(state: State): void {
const stateConfig = this.config.states[state];
__TOKEN_93__ (stateConfig?.onEntry) {
const result = stateConfig.onEntry(this.context);
__TOKEN_96__ (result !== undefined) {
this.context = result;
}
}
}
private callExit(state: State): void {
const stateConfig = this.config.states[state];
__TOKEN_101__ (stateConfig?.onExit) {
const result = stateConfig.onExit(this.context);
__TOKEN_104__ (result !== undefined) {
this.context = result;
}
}
}
get state(): State {
return this.currentState;
}
get contextData(): Context {
return this.context;
}
send(event: Event): boolean {
const stateTransitions = this.transitions.get(this.currentState);
__TOKEN_113__ (!stateTransitions) return false;
const nextState = stateTransitions.get(event);
__TOKEN_116__ (!nextState) return false;
// Exit current state
this.callExit(this.currentState);
const previousState = this.currentState;
this.currentState = nextState;
// Enter new state
this.callEntry(nextState);
// Call transition callback
this.config.onTransition?.(previousState, nextState, event, this.context);
return true;
}
can(event: Event): boolean {
const stateTransitions = this.transitions.get(this.currentState);
return stateTransitions?.has(event) || false;
}
allowedEvents(): Event[] {
const stateTransitions = this.transitions.get(this.currentState);
return stateTransitions ? Array.__TOKEN_135__(stateTransitions.keys()) : [];
}
updateContext(updater: (context: Context) => Context): void {
this.context = updater(this.context);
}
}
// Example: Download state machine
interface DownloadContext {
progress: number;
fileSize?: number;
error?: string;
startTime?: Date;
endTime?: Date;
}
type DownloadState = 'idle' | 'downloading' | 'paused' | 'completed' | 'error';
type DownloadEvent = 'start' | 'pause' | 'resume' | 'complete' | 'error' | 'reset';
const downloadMachine = new StateMachine<DownloadState, DownloadEvent, DownloadContext>({
initialState: 'idle',
initialContext: { progress: 0 },
states: {
idle: {
onEntry: (context) => ({ ...context, progress: 0, error: undefined })
},
downloading: {
onEntry: (context) => ({ ...context, startTime: new Date() }),
onExit: (context) => {
__TOKEN_144__ (context.progress === 100) {
return { ...context, endTime: new Date() };
}
return context;
}
},
paused: {},
completed: {},
error: {
onEntry: (context) => ({ ...context, endTime: new Date() })
}
},
transitions: [
['idle', 'start', 'downloading'],
['downloading', 'pause', 'paused'],
['downloading', 'complete', 'completed'],
['downloading', 'error', 'error'],
['paused', 'resume', 'downloading'],
['paused', 'error', 'error'],
['completed', 'reset', 'idle'],
['error', 'reset', 'idle']
],
onTransition: (from, to, event, context) => {
console.log(`${from} -> ${to} via ${event}`);
}
});
// Usage
console.log('Initial state:', downloadMachine.state); // 'idle'
console.log('Can start?', downloadMachine.can('start')); // true
downloadMachine.send('start');
console.log('Current state:', downloadMachine.state); // 'downloading'
// Update progress
const interval = setInterval(() => {
__TOKEN_151__ (downloadMachine.state === 'downloading') {
downloadMachine.updateContext(ctx => ({
...ctx,
progress: Math.min(ctx.progress + 10, 100)
}));
__TOKEN_152__ (downloadMachine.contextData.progress === 100) {
downloadMachine.send('complete');
clearInterval(interval);
}
}
}, 1000);
// Guarded transitions function
function createGuardedStateMachine<
State extends string,
Event extends string,
Context = any
>(
config: StateMachineConfig<State, Event, Context> & {
guards?: Partial<Record<Event, (context: Context) => boolean>>;
}
) {
const baseMachine = new StateMachine<State, Event, Context>(config);
return {
...baseMachine,
send(event: Event): boolean {
const guard = config.guards?.[event];
__TOKEN_160__ (guard && !guard(baseMachine.contextData)) {
return false;
}
return baseMachine.send(event);
}
};
}
// Async state machine with side effects
class AsyncStateMachine<
State extends string,
Event extends string,
Context = any
> extends StateMachine<State, Event, Context> {
async sendAsync(
event: Event,
effect?: (context: Context) => Promise<Context | void>
): Promise<boolean> {
__TOKEN_168__ (!this.can(event)) return false;
__TOKEN_171__ (effect) {
try {
const result = await effect(this.contextData);
__TOKEN_176__ (result !== undefined) {
this.updateContext(() => result);
}
} __TOKEN_178__ (error) {
return false;
}
}
return this.send(event);
}
}