TYPESCRIPT

Finite State Machine

Type-safe finite state machine implementation

TypeScriptState MachinePatterns

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);
  }
}