Skip to main content

Builder Pattern

The StateMachine library uses the Builder Pattern to provide a fluent, chainable API for constructing state machines. This approach makes the code more readable and helps prevent configuration errors.

Basic Builder Usage

The builder pattern allows you to chain method calls to configure your state machine:

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

const machine = StateMachine.builder()
.initialState('IDLE')
.state('IDLE')
.state('RUNNING')
.state('COMPLETE')
.transition('IDLE', 'RUNNING', 'start')
.transition('RUNNING', 'COMPLETE', 'finish')
.build(); // Creates the actual StateMachine instance

Builder Methods

State Definition

// Define states explicitly
.state('STATE_NAME')

// States are automatically added from transitions
.transition('FROM_STATE', 'TO_STATE', 'EVENT') // Both states auto-added

Initial State

// Set the starting state (required)
.initialState('IDLE')

Transitions

// Basic transition
.transition('FROM_STATE', 'TO_STATE', 'EVENT')

// The last defined transition can have guards and actions added
.transition('IDLE', 'RUNNING', 'start')
.guard((context) => context.isReady)
.action((context) => console.log('Starting...'))

Guards and Actions

Guards and actions are applied to the last defined transition:

const machine = StateMachine.builder()
.initialState('IDLE')

// First transition with guard and action
.transition('IDLE', 'RUNNING', 'start')
.guard((context) => context.hasPermission)
.action((context) => (context.startTime = Date.now()))

// Second transition with different guard and action
.transition('RUNNING', 'COMPLETE', 'finish')
.guard((context) => context.isValid)
.action((context) => (context.endTime = Date.now()))

.build();

State Entry and Exit Actions

const machine = StateMachine.builder()
.initialState('IDLE')
.state('IDLE')
.state('PROCESSING')
.state('COMPLETE')

// Actions when entering/exiting states
.onStateEntry('PROCESSING', (context) => {
console.log('Started processing');
context.processingStart = Date.now();
})

.onStateExit('PROCESSING', (context) => {
const duration = Date.now() - context.processingStart;
console.log(`Processing took ${duration}ms`);
})

.transition('IDLE', 'PROCESSING', 'start')
.transition('PROCESSING', 'COMPLETE', 'finish')
.build();

Method Chaining Rules

Order Independence

Most builder methods can be called in any order:

// These are equivalent
const machine1 = StateMachine.builder()
.initialState('A')
.state('A')
.state('B')
.transition('A', 'B', 'go')
.build();

const machine2 = StateMachine.builder()
.state('B')
.transition('A', 'B', 'go')
.state('A')
.initialState('A')
.build();

Guard and Action Placement

Guards and actions must come immediately after the transition they apply to:

// ✅ Correct - guard applies to the transition above it
.transition('A', 'B', 'event1')
.guard((context) => context.isValid)

.transition('B', 'C', 'event2')
.action((context) => console.log('Moving to C'))

// ❌ Incorrect - guard has no transition to apply to
.guard((context) => context.isValid) // Error: no preceding transition
.transition('A', 'B', 'event1')

Multiple Guards and Actions

You can add multiple guards and actions to the same transition:

.transition('A', 'B', 'proceed')
.guard((context) => context.hasPermission) // First guard
.guard((context) => context.isValid) // Second guard (AND logic)
.action((context) => console.log('First')) // First action
.action((context) => console.log('Second')) // Second action

Advanced Builder Patterns

Conditional Building

Build different state machines based on conditions:

function createMachine(hasAdvancedFeatures) {
const builder = StateMachine.builder()
.initialState('START')
.state('START')
.state('BASIC')
.transition('START', 'BASIC', 'begin');

if (hasAdvancedFeatures) {
builder
.state('ADVANCED')
.transition('BASIC', 'ADVANCED', 'upgrade')
.transition('ADVANCED', 'BASIC', 'downgrade');
}

return builder.build();
}

Builder Composition

Break complex machines into smaller, composable parts:

function addAuthenticationStates(builder) {
return builder
.state('LOGGED_OUT')
.state('LOGGING_IN')
.state('LOGGED_IN')
.transition('LOGGED_OUT', 'LOGGING_IN', 'login')
.transition('LOGGING_IN', 'LOGGED_IN', 'success')
.transition('LOGGING_IN', 'LOGGED_OUT', 'failure');
}

function addDataStates(builder) {
return builder
.state('LOADING')
.state('LOADED')
.state('ERROR')
.transition('LOGGED_IN', 'LOADING', 'fetchData')
.transition('LOADING', 'LOADED', 'success')
.transition('LOADING', 'ERROR', 'failure');
}

const machine = addDataStates(
addAuthenticationStates(StateMachine.builder().initialState('LOGGED_OUT'))
).build();

Configuration Objects

Use configuration objects for complex setups:

const config = {
initialState: 'IDLE',
states: ['IDLE', 'PROCESSING', 'COMPLETE', 'ERROR'],
transitions: [
{ from: 'IDLE', to: 'PROCESSING', event: 'start' },
{ from: 'PROCESSING', to: 'COMPLETE', event: 'success' },
{ from: 'PROCESSING', to: 'ERROR', event: 'failure' },
{ from: 'ERROR', to: 'IDLE', event: 'retry' },
],
};

function buildFromConfig(config) {
let builder = StateMachine.builder().initialState(config.initialState);

// Add states
config.states.forEach((state) => {
builder = builder.state(state);
});

// Add transitions
config.transitions.forEach(({ from, to, event }) => {
builder = builder.transition(from, to, event);
});

return builder.build();
}

const machine = buildFromConfig(config);

TypeScript Builder

The builder provides full TypeScript support with generic types:

interface MyContext {
userId: string;
data: any[];
}

type MyState = 'IDLE' | 'LOADING' | 'SUCCESS' | 'ERROR';
type MyEvent = 'start' | 'success' | 'failure' | 'retry';

const machine = StateMachine.builder<MyContext, MyState, MyEvent>()
.initialState('IDLE')
.state('IDLE')
.state('LOADING')
.state('SUCCESS')
.state('ERROR')

.transition('IDLE', 'LOADING', 'start')
.guard((context: MyContext) => context.userId.length > 0) // Typed context

.transition('LOADING', 'SUCCESS', 'success')
.action((context: MyContext) => {
context.data.push('result'); // Type-safe operations
})

.build();

Validation and Error Handling

The builder validates your configuration and provides helpful error messages:

// Missing initial state
try {
StateMachine.builder().state('A').transition('A', 'B', 'go').build(); // Throws: "Initial state must be set"
} catch (error) {
console.error(error.message);
}

// Guard without transition
try {
StateMachine.builder()
.initialState('A')
.guard((context) => true) // Error: no transition to apply guard to
.build();
} catch (error) {
console.error(error.message);
}

// Unreachable states warning
const machine = StateMachine.builder()
.initialState('A')
.state('A')
.state('B')
.state('C') // Warning: C is unreachable (no transitions to it)
.transition('A', 'B', 'go')
.build();

Best Practices

Use Descriptive Names

// ✅ Good - clear, descriptive names
.transition('PAYMENT_PENDING', 'PAYMENT_PROCESSING', 'processPayment')
.guard((context) => context.hasValidPaymentMethod)
.action((context) => context.logPaymentStart())

// ❌ Poor - unclear names
.transition('S1', 'S2', 'e1')
.guard((context) => context.x)
.action((context) => context.y())
const machine = StateMachine.builder()
.initialState('IDLE')

// Define all states first
.state('IDLE')
.state('LOADING')
.state('SUCCESS')
.state('ERROR')

// Then define transitions with their guards/actions
.transition('IDLE', 'LOADING', 'start')
.guard((context) => context.isReady)
.action((context) => context.startLoading())

.transition('LOADING', 'SUCCESS', 'complete')
.action((context) => context.handleSuccess())

.transition('LOADING', 'ERROR', 'fail')
.action((context) => context.handleError())

// Finally, state entry/exit actions
.onStateEntry('LOADING', (context) => context.showSpinner())
.onStateExit('LOADING', (context) => context.hideSpinner())

.build();

Validate Early

// Check configuration before building
const builder = StateMachine.builder().initialState('START');

// Add states conditionally with validation
const requiredStates = ['START', 'MIDDLE', 'END'];
requiredStates.forEach((state) => {
if (state) {
// Validate state name
builder.state(state);
}
});

const machine = builder.build();

Next Steps