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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@bernierllc/state-machine

v1.2.0

Published

Workflow finite state machine for content lifecycle with transition validation and event emission

Readme

@bernierllc/state-machine

Workflow finite state machine for content lifecycle with transition validation and event emission.

Installation

npm install @bernierllc/state-machine

Features

  • Finite State Machine (FSM) - Define states and allowed transitions
  • Transition Guards - Validate transitions with custom guard functions
  • Transition Callbacks - Execute code before/after state changes
  • Event Emission - Subscribe to state change events
  • State History - Track complete history of state transitions
  • TypeScript Support - Full type safety with strict mode
  • Async Support - Guards and callbacks support async operations
  • Zero Dependencies - Uses only @bernierllc/event-emitter and @bernierllc/logger

Usage

Basic State Machine

import { StateMachine } from '@bernierllc/state-machine';

// Create a simple workflow
const workflow = new StateMachine({
  initial: 'draft',
  states: ['draft', 'review', 'approved', 'published'],
  transitions: {
    draft: ['review'],
    review: ['draft', 'approved'],
    approved: ['published', 'review'],
    published: [],
  },
});

// Check current state
console.log(workflow.currentState); // 'draft'

// Execute transitions
const result = await workflow.transition('review');
console.log(result.success); // true
console.log(workflow.currentState); // 'review'

// Check allowed transitions
console.log(workflow.getAllowedTransitions()); // ['draft', 'approved']

Using Factory Functions

import { createWorkflowStateMachine } from '@bernierllc/state-machine';

// Create pre-configured content workflow
const workflow = createWorkflowStateMachine();
// States: draft -> review -> approved -> published

Transition Guards

Guards validate whether a transition should be allowed:

const sm = new StateMachine({
  initial: 'draft',
  states: ['draft', 'review', 'published'],
  transitions: {
    draft: ['review'],
    review: ['published'],
    published: [],
  },
});

// Add guard to check if user has permission
sm.addTransitionConfig('review', 'published', {
  guards: [
    (context) => {
      // Only allow if data contains approval
      return context.data?.approved === true;
    },
  ],
});

// This will fail (guard returns false)
await sm.transition('review');
await sm.transition('published'); // Blocked by guard

// This will succeed
await sm.transition('published', { approved: true }); // Allowed

Async Guards

Guards can be async for database checks, API calls, etc:

sm.addTransitionConfig('draft', 'review', {
  guards: [
    async (context) => {
      // Check database for user permissions
      const user = await database.getUser(context.data.userId);
      return user.role === 'reviewer';
    },
  ],
});

Multiple Guards

All guards must pass for transition to be allowed:

sm.addTransitionConfig('review', 'published', {
  guards: [
    (ctx) => ctx.data.approved === true,
    (ctx) => ctx.data.reviewer !== null,
    (ctx) => ctx.data.checks.length === 0, // No outstanding checks
  ],
});

Transition Callbacks

Execute code before and after transitions:

sm.addTransitionConfig('draft', 'review', {
  before: [
    (context) => {
      console.log(`Starting review for ${context.data.documentId}`);
      // Send notification to reviewer
      notificationService.notify(context.data.reviewerId);
    },
  ],
  after: [
    (context) => {
      console.log(`Review started at ${context.timestamp}`);
      // Log to analytics
      analytics.track('review_started', {
        document: context.data.documentId,
      });
    },
  ],
});

Event Subscription

Subscribe to state machine events:

// Listen for any state change
sm.on('state:changed', (context) => {
  console.log(`State changed: ${context.from} -> ${context.to}`);
  console.log('Data:', context.data);
  console.log('Timestamp:', context.timestamp);
});

// Listen for before transition
sm.on('transition:before', (context) => {
  console.log(`About to transition: ${context.from} -> ${context.to}`);
});

// Listen for after transition
sm.on('transition:after', (context) => {
  console.log(`Completed transition: ${context.from} -> ${context.to}`);
});

// Listen for failed transitions
sm.on('transition:failed', (context) => {
  console.log(`Failed: ${context.from} -> ${context.to}`);
  console.log('Error:', context.error);
});

State History

Track complete history of state changes:

// Get full history
console.log(workflow.history);
// [
//   { state: 'draft', timestamp: Date, data: undefined },
//   { state: 'review', timestamp: Date, data: { reviewerId: 123 } },
//   { state: 'published', timestamp: Date, data: { publishedBy: 456 } }
// ]

// Get state at specific index
const firstState = workflow.getStateAt(0);
console.log(firstState?.state); // 'draft'

// Limit history size
const sm = new StateMachine({
  initial: 'draft',
  states: ['draft', 'published'],
  transitions: { draft: ['published'], published: [] },
  maxHistoryLength: 100, // Keep only last 100 entries
});

Check Transition Validity

Check if a transition is allowed without executing it:

const canTransition = await workflow.canTransition('published');
if (canTransition) {
  await workflow.transition('published');
} else {
  console.log('Transition not allowed');
}

Reset State Machine

Reset to initial state and clear history:

workflow.reset();
console.log(workflow.currentState); // Back to 'draft'
console.log(workflow.history.length); // 1 (only initial state)

API Reference

StateMachine Class

Constructor

new StateMachine<S extends string = string, T = any>(config: StateMachineConfig<S, T>)

Properties

  • currentState: S - Current state (readonly)
  • history: StateHistoryEntry[] - State history (readonly)

Methods

  • canTransition(to: S, data?: T): Promise<boolean> - Check if transition is allowed
  • transition(to: S, data?: T): Promise<TransitionResult> - Execute state transition
  • getAllowedTransitions(): S[] - Get allowed transitions from current state
  • on(event: string, handler: Function): void - Subscribe to events
  • reset(): void - Reset to initial state
  • getStateAt(index: number): StateHistoryEntry | undefined - Get state at index
  • addTransitionConfig(from: S, to: S, config: TransitionConfig<T>): void - Add transition configuration

Types

StateMachineConfig

interface StateMachineConfig<S extends string = string, T = any> {
  initial: S;
  states: S[];
  transitions: Record<S, S[]>;
  transitionConfigs?: Record<string, TransitionConfig<T>>;
  maxHistoryLength?: number;
}

TransitionResult

interface TransitionResult {
  success: boolean;
  error?: string;
  previousState?: string;
  newState?: string;
}

TransitionContext

interface TransitionContext<T = any> {
  from: string;
  to: string;
  data?: T;
  timestamp: Date;
}

TransitionConfig

interface TransitionConfig<T = any> {
  guards?: TransitionGuard<T>[];
  before?: BeforeTransitionCallback<T>[];
  after?: AfterTransitionCallback<T>[];
}

Factory Functions

  • createStateMachine<S, T>(config: StateMachineConfig<S, T>): StateMachine<S, T>
  • createWorkflowStateMachine(): StateMachine<'draft' | 'review' | 'approved' | 'published'>

Real-world Examples

Content Approval Workflow

import { StateMachine } from '@bernierllc/state-machine';

interface ApprovalData {
  contentId: string;
  reviewerId?: string;
  approverId?: string;
  comments?: string;
}

const approvalWorkflow = new StateMachine<
  'draft' | 'review' | 'approved' | 'published',
  ApprovalData
>({
  initial: 'draft',
  states: ['draft', 'review', 'approved', 'published'],
  transitions: {
    draft: ['review'],
    review: ['draft', 'approved'],
    approved: ['published', 'review'],
    published: [],
  },
});

// Require reviewer assignment
approvalWorkflow.addTransitionConfig('draft', 'review', {
  guards: [
    (ctx) => {
      if (!ctx.data?.reviewerId) {
        return false;
      }
      return true;
    },
  ],
  after: [
    async (ctx) => {
      // Notify reviewer
      await emailService.send({
        to: ctx.data.reviewerId,
        subject: 'New content for review',
        body: `Content ${ctx.data.contentId} needs your review`,
      });
    },
  ],
});

// Require approval before publishing
approvalWorkflow.addTransitionConfig('review', 'approved', {
  guards: [
    (ctx) => ctx.data?.approverId !== undefined,
  ],
});

// Usage
await approvalWorkflow.transition('review', {
  contentId: 'doc-123',
  reviewerId: 'user-456',
});

Deployment Pipeline

import { StateMachine } from '@bernierllc/state-machine';

interface DeploymentData {
  version: string;
  tests?: { passed: boolean; coverage: number };
  securityScan?: { passed: boolean };
}

const pipeline = new StateMachine<
  'development' | 'testing' | 'staging' | 'production',
  DeploymentData
>({
  initial: 'development',
  states: ['development', 'testing', 'staging', 'production'],
  transitions: {
    development: ['testing'],
    testing: ['development', 'staging'],
    staging: ['production', 'testing'],
    production: [],
  },
});

// Require tests to pass
pipeline.addTransitionConfig('testing', 'staging', {
  guards: [
    (ctx) => {
      const tests = ctx.data?.tests;
      return tests?.passed === true && tests.coverage >= 80;
    },
  ],
});

// Require security scan
pipeline.addTransitionConfig('staging', 'production', {
  guards: [
    (ctx) => ctx.data?.securityScan?.passed === true,
  ],
  before: [
    async (ctx) => {
      // Final backup before production
      await backupService.create(ctx.data.version);
    },
  ],
  after: [
    async (ctx) => {
      // Notify team
      await slackService.notify({
        channel: '#deployments',
        message: `Version ${ctx.data.version} deployed to production`,
      });
    },
  ],
});

Integration Status

Logger Integration

Status: Integrated

Justification: This package uses @bernierllc/logger for structured logging of state transitions. Logs include state changes, transition attempts, guard evaluations, and errors to help with debugging and monitoring workflow execution.

Pattern: Direct integration - logger is a required dependency for this package.

NeverHub Integration

Status: Optional (recommended for workflow orchestration)

Justification: While this package can function without NeverHub, it's highly recommended to integrate @bernierllc/neverhub-adapter for workflow orchestration and event publishing. State machines often need to coordinate with other services, and NeverHub provides a service mesh for publishing state change events that other services can subscribe to. However, the package is designed to work without NeverHub for simpler use cases.

Pattern: Optional integration - package works without NeverHub, but NeverHub enhances workflow orchestration capabilities.

Example Integration:

import { NeverHubAdapter } from '@bernierllc/neverhub-adapter';
import { StateMachine } from '@bernierllc/state-machine';

const neverhub = new NeverHubAdapter({ service: 'content-workflow' });
const workflow = new StateMachine({ /* config */ });

// Publish state changes to NeverHub
workflow.on('state:changed', async (context) => {
  await neverhub.publish('content.state.changed', {
    contentId: context.data?.contentId,
    from: context.from,
    to: context.to,
    timestamp: context.timestamp
  });
});

Docs-Suite Integration

Status: Ready

Format: TypeDoc-compatible JSDoc comments are included throughout the source code. All public APIs are documented with examples and type information.

License

Copyright (c) 2025 Bernier LLC. All rights reserved.

This file is licensed to the client under a limited-use license. The client may use and modify this code only within the scope of the project it was delivered for. Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.