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​