npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@axi-engine/states

v0.1.1

Published

A minimal, type-safe state machine for the Axi Engine, designed for managing game logic and component states with a simple API.

Readme

StateMachine

A lightweight, type-safe, and easy-to-use state machine designed for managing game logic and component states within the Axi Engine.

This implementation prioritizes simplicity, a clean API, and robust error handling for common use cases. It is perfect for managing character states, UI flows, game session lifecycles, and more. It has minimal dependencies, relying only on the Emitter utility from @axi-engine/utils for its event system.

For highly complex scenarios involving hierarchical (nested) states, parallel states, or state history, I recommend considering more powerful, dedicated libraries like XState.

My goal is to provide a solid, built-in tool that covers 80% of typical game development needs without adding external dependencies or unnecessary complexity.

Features

  • Simple and Clean API: Registering states and transitioning between them is straightforward.
  • Type-Safe: Leverages TypeScript generics to provide type safety for state names and event payloads.
  • Lifecycle Hooks: States can have onEnter and onExit handlers for setup and cleanup logic.
  • Transition Guards: You can define which states are allowed to transition to a new state, preventing logical errors.
  • Observable State: A public onChange emitter allows you to subscribe to all state transitions for debugging, logging, or reacting to changes.
  • Minimal Dependencies: Relies only on a tiny, self-contained Emitter utility.

Getting Started

First, define the possible states, typically using an enum for type safety.

enum GameState {
  MainMenu,
  Loading,
  Playing,
  Paused,
  GameOver,
}

Then, create an instance of the StateMachine. The machine starts in an undefined state.

import { StateMachine } from './@axi-engine/states';

const gameState = new StateMachine<GameState>();

API and Usage

1. Registering States

You can register a state with a simple handler function. This function will be treated as the onEnter hook.

// Simple registration
gameState.register(GameState.MainMenu, () => {
  console.log('Welcome to the Main Menu!');
  showMainMenu();
});

gameState.register(GameState.GameOver, () => {
  console.log('Game Over!');
  showGameOverScreen();
});

2. Changing States

Use the call method to transition to a new state. The method is asynchronous to handle any async logic within state handlers. The first call will formally start the machine.

// Start the machine by calling the initial state
await gameState.call(GameState.MainMenu);

// ... later in the game
await gameState.call(GameState.GameOver);

3. Using Payloads

You can pass data during a state transition. The payload type can be defined in the StateMachine generic.

// State machine that accepts a string payload for the 'Loading' state
const sm = new StateMachine<GameState, string>();

sm.register(GameState.Loading, async (levelId: string) => {
  console.log(`Loading level: ${levelId}...`);
  await loadLevelAssets(levelId);
});

await sm.call(GameState.Loading, 'level-2');

4. Advanced Registration

For more control, you can register a state with a configuration object. This allows you to define onEnter, onExit hooks, and transition guards.

onEnter and onExit

  • onEnter: Called when the machine enters the state.
  • onExit: Called when the machine leaves the state. This is perfect for cleanup.
gameState.register(GameState.Playing, {
  onEnter: () => {
    console.log('Starting game...');
    gameMusic.play();
    player.enableControls();
  },
  onExit: () => {
    console.log('Exiting gameplay...');
    gameMusic.stop();
    player.disableControls();
  },
});

allowedFrom (Transition Guards)

Specify an array of states from which a transition to this state is permitted. An attempt to transition from any other state will throw an error.

gameState.register(GameState.Paused, {
  // You can only pause the game if you are currently playing.
  allowedFrom: [GameState.Playing],
  onEnter: () => {
    console.log('Game paused.');
    showPauseMenu();
  },
});

// This will work:
await gameState.call(GameState.Playing);
await gameState.call(GameState.Paused);

// This will throw an error:
await gameState.call(GameState.MainMenu);
await gameState.call(GameState.Paused); // Error: Transition from MainMenu to Paused is not allowed.

5. Subscribing to Changes

The public onChange property is an Emitter. You can use its subscribe method to be notified of any state change. The method returns a function to unsubscribe.

const unsubscribe = gameState.onChange.subscribe((from, to, payload) => {
  const fromState = from !== undefined ? GameState[from] : 'Start';
  console.log(`State changed from ${fromState} to ${GameState[to]}`, { payload });
});

await gameState.call(GameState.MainMenu);
// Console output: State changed from Start to MainMenu

// To stop listening later:
unsubscribe();

Full Example

import { StateMachine } from '@axi-engine/states';

enum GameState {
  MainMenu,
  Playing,
  Paused,
  GameOver,
}

// --- Setup ---
const game = new StateMachine<GameState>();

game.onChange.subscribe((from, to) => {
  const fromState = from !== undefined ? GameState[from] : 'Start';
  console.log(`[SYSTEM] Transition: ${fromState} -> ${GameState[to]}`);
});

game.register(GameState.MainMenu, {
  onEnter: () => console.log('Showing Main Menu.'),
});

game.register(GameState.Playing, {
  allowedFrom: [GameState.MainMenu, GameState.Paused],
  onEnter: () => console.log('Game has started! Player controls enabled.'),
  onExit: () => console.log('Player controls disabled.'),
});

game.register(GameState.Paused, {
  allowedFrom: [GameState.Playing],
  onEnter: () => console.log('Game is paused.'),
});

game.register(GameState.GameOver, {
  allowedFrom: [GameState.Playing],
  onEnter: () => console.log('You lose!'),
});

// --- Simulation ---
async function runGame() {
  await game.call(GameState.MainMenu);
  // [SYSTEM] Transition: Start -> MainMenu
  // Showing Main Menu.

  await game.call(GameState.Playing);
  // [SYSTEM] Transition: MainMenu -> Playing
  // Game has started! Player controls enabled.

  await game.call(GameState.Paused);
  // [SYSTEM] Transition: Playing -> Paused
  // Player controls disabled.
  // Game is paused.

  try {
    // This transition will fail because of the 'allowedFrom' guard
    await game.call(GameState.MainMenu);
  } catch (e) {
    console.error(e.message); // Error: Transition from Paused to MainMenu is not allowed.
  }
}

runGame();