Skip to content

State Machine ​

Most state management libraries let you put anything anywhere. @logosdx/state-machine takes the opposite approach — you define which states exist, which events are valid in each state, and how context changes on transition. The machine enforces the rules so your code doesn't have to.

Installation ​

bash
npm install @logosdx/state-machine
bash
yarn add @logosdx/state-machine
bash
pnpm add @logosdx/state-machine

CDN:

html
<script src="https://cdn.jsdelivr.net/npm/@logosdx/state-machine@latest/dist/browser/bundle.min.js"></script>
<script>
    const { StateMachine, StateHub } = LogosDx.StateMachine;
</script>

Quick Start ​

typescript
import { StateMachine } from '@logosdx/state-machine'

interface OrderContext {
    items: string[]
    error: string | null
}

interface OrderEvents {
    ADD_ITEM: { name: string }
    SUBMIT: void
    SUCCESS: { orderId: string }
    FAILURE: { message: string }
    RETRY: void
}

const order = new StateMachine<OrderContext, OrderEvents>({
    initial: 'draft',
    context: { items: [], error: null },
    transitions: {
        draft: {
            on: {
                ADD_ITEM: {
                    target: 'draft',
                    action: (ctx, data) => ({
                        ...ctx,
                        items: [...ctx.items, data.name],
                    }),
                },
                SUBMIT: 'submitting',
            },
        },
        submitting: {
            on: {
                SUCCESS: {
                    target: 'confirmed',
                    action: (ctx) => ({ ...ctx, error: null }),
                },
                FAILURE: {
                    target: 'error',
                    action: (ctx, data) => ({ ...ctx, error: data.message }),
                },
            },
        },
        error: {
            on: { RETRY: 'submitting' },
        },
        confirmed: { final: true },
    },
})

order.send('ADD_ITEM', { name: 'Widget' })
order.send('SUBMIT')
order.send('SUCCESS', { orderId: 'ORD-123' })

console.log(order.state)   // 'confirmed'
console.log(order.context) // { items: ['Widget'], error: null }

Core Concepts ​

A StateMachine has three things: a current state (a string), a context (typed data), and a transition map that defines which events are valid in each state and what happens when they fire.

  • States are strings like 'idle', 'loading', 'error'
  • Events are typed messages like 'FETCH' or 'ADD_ITEM: { id: string }'
  • Transitions connect states through events, optionally modifying context via actions and preventing invalid moves via guards
  • Observer powers the notification layer internally — you get on(), off(), regex matching, and cleanup functions for free

The Three Layers ​

Every interaction with the machine falls into one of these categories:

Actions ​

Pure functions that transform context during a transition. (ctx, data) => newCtx. No side effects. No async. Just data in, data out.

typescript
// String shorthand — just move, don't touch context
FETCH: 'loading'

// Object — move and modify context
INCREMENT: {
    target: 'idle',
    action: (ctx) => ({ ...ctx, count: ctx.count + 1 }),
}

// With guard — conditional transition
WITHDRAW: {
    target: 'idle',
    action: (ctx, data) => ({ ...ctx, balance: ctx.balance - data.amount }),
    guard: (ctx, data) => ctx.balance >= data.amount,
}

Invoke ​

Async operations that feed results back into the machine as new transitions. The machine owns the lifecycle — it starts the work, waits, and transitions itself when the promise settles. If the machine leaves the state before the promise resolves, the result is discarded.

typescript
loading: {
    invoke: {
        src: async (ctx) => {
            const res = await fetch('/api/users')
            return res.json()
        },
        onDone: {
            target: 'idle',
            action: (ctx, result) => ({ ...ctx, users: result }),
        },
        onError: {
            target: 'error',
            action: (ctx, err) => ({ ...ctx, error: err.message }),
        },
    },
}

Listeners ​

Fire-and-forget. React to transitions but never touch the machine. Logging, analytics, updating the DOM, notifying other machines. One-way, outward.

typescript
machine.on('error', ({ context }) => {
    errorService.report(context.error)
})

machine.on('*', ({ from, to }) => {
    analytics.track('state_change', { from, to })
})

The rule is simple: actions transform, invoke feeds back, listeners observe.

What's Next ​

  • API Reference — Full details on StateMachine, StateHub, persistence, types, and every method
  • Practical Guide — A complete checkout flow walkthrough showing how the three layers work together in a real application