State Management

State Consistency

StateFlow prevents state inconsistency through a combination of compile-time type safety and runtime immutability enforcement. This dual-layer protection makes it virtually impossible to create invalid or corrupted states in your application.

The Consistency Problem

Traditional state management systems suffer from several consistency issues that StateFlow eliminates by design.

Race Conditions

In mutable state systems, concurrent modifications can lead to race conditions where the final state depends on the order of operations rather than business logic. StateFlow prevents this through immutability and atomic state transitions. Every state change creates a new immutable snapshot, ensuring that concurrent operations cannot interfere with each other.

Partial Updates

Mutable systems often allow partial state updates that can leave the application in an inconsistent state if an error occurs mid-update. StateFlow requires complete state objects for every transition, making partial updates impossible. If a transition fails, the original state remains unchanged.

Type Violations

JavaScript's dynamic nature allows runtime type violations that can corrupt state. StateFlow leverages TypeScript's type system at compile time and enforces structure at runtime, creating a robust barrier against type-related inconsistencies.

Immutability Enforcement

StateFlow enforces immutability at multiple levels to ensure state consistency.

Runtime Freezing

Every state instance is frozen using Object.freeze() upon creation:

const state = playbackState.playing({ 
  position: 30, 
  duration: 180 
});

// Runtime error - state is frozen
state.position = 45; // TypeError: Cannot assign to read only property

// Even nested objects would be protected in a deep-freeze implementation

Type-Level Immutability

TypeScript's type system provides compile-time protection:

type PlaybackState = {
  readonly position: number;
  readonly duration: number;
};

// Compile error - cannot assign to readonly property
const updatePosition = (state: PlaybackState) => {
  state.position = 45; // Error: Cannot assign to 'position' because it is a read-only property
};

Immutable Transitions

State transitions always create new instances rather than modifying existing ones:

defineFlow(playbackState.playing, {
  seek: (state, signal) => ({
    ...state, // Spread existing state
    position: signal.position // Override specific field
  })
});

// Original state remains unchanged
const before = playbackState.playing({ position: 30, duration: 180 });
dispatch(app, signals.seek({ position: 60 }));
console.log(before.position); // Still 30 - original unchanged

Atomic State Transitions

StateFlow ensures that state transitions are atomic operations that either complete fully or fail without side effects.

Transaction Boundaries

Each dispatch operation forms a transaction boundary. Within this boundary, all state changes are collected and validated before being applied:

// Multiple states change atomically
const result = dispatch(app, signals.startPlayback());

if (result.kind === ResultKind.OK) {
  // All states transitioned successfully
  // playbackState: paused -> playing
  // bufferState: empty -> buffering
  // uiState: idle -> active
} else {
  // No states changed - all remain in original state
}

Rollback Mechanism

When a transition fails, StateFlow provides rollback handlers to ensure consistency:

applyFlow(app, [playbackState], (sm) => {
  sm.addEnterHandler(playbackState.playing, async (state) => {
    const result = await startMediaPlayback();
    if (!result.success) {
      return Result.reject("Playback initialization failed");
    }
    return Result.ok();
  });
  
  sm.addRollbackHandler(playbackState.playing, (state) => {
    // Clean up any partial initialization
    stopMediaResources();
    return Result.ok();
  });
});

Type Safety Through Inference

StateFlow leverages TypeScript's type inference to maintain consistency without verbose type annotations.

State Type Inference

The Infer utility type extracts precise types from state definitions:

const connectionState = defineState<{
  url: string;
  status: 'connecting' | 'connected' | 'error';
  errorCount: number;
}>()
  .name("connection")
  .variant("active")
  .build();

// Type is automatically inferred
type ConnectionProps = Infer<typeof connectionState>;
// { url: string; status: 'connecting' | 'connected' | 'error'; errorCount: number }

// Application structure is type-checked
interface App {
  connection: ConnectionProps;
}

Signal Type Safety

Signals maintain type safety for their parameters:

const signals = {
  updateConfig: defineSignal<{
    timeout: number;
    retryLimit: number;
  }>("updateConfig")
};

// Type error - missing required field
dispatch(app, signals.updateConfig({ timeout: 5000 }));
// Error: Property 'retryLimit' is missing

// Type error - wrong type
dispatch(app, signals.updateConfig({ 
  timeout: "5000", // Error: Type 'string' is not assignable to type 'number'
  retryLimit: 3 
}));

Validation at Transition Time

StateFlow allows validation logic within flow definitions to ensure business rule consistency.

Input Validation

Flow handlers can validate inputs and reject invalid transitions:

defineFlow(audioState.playing, {
  setVolume: (state, signal) => {
    if (signal.level < 0 || signal.level > 1) {
      return Result.reject("Volume must be between 0 and 1");
    }
    
    if (state.muted && signal.level > 0) {
      return Result.reject("Cannot set volume while muted");
    }
    
    return { ...state, volume: signal.level };
  }
});

State Invariants

Complex invariants can be enforced across multiple state properties:

defineFlow(gameState.playing, {
  movePlayer: (state, signal) => {
    const newPosition = {
      x: state.player.x + signal.deltaX,
      y: state.player.y + signal.deltaY
    };
    
    // Enforce boundary constraints
    if (newPosition.x < 0 || newPosition.x > state.boardSize ||
        newPosition.y < 0 || newPosition.y > state.boardSize) {
      return Result.reject("Move would place player outside board");
    }
    
    // Check collision with obstacles
    const collision = state.obstacles.some(
      obs => obs.x === newPosition.x && obs.y === newPosition.y
    );
    
    if (collision) {
      return Result.reject("Move blocked by obstacle");
    }
    
    return {
      ...state,
      player: newPosition,
      moveCount: state.moveCount + 1
    };
  }
});

Preventing Common Inconsistencies

StateFlow's design prevents several common state inconsistency patterns.

No Forgotten Updates

Traditional systems often require updating multiple related states, leading to inconsistencies when developers forget some updates. StateFlow's atomic transitions ensure all related states update together:

defineFlow(orderState.pending, {
  confirmOrder: (state) => {
    // All related states must be returned together
    return orderState.confirmed({
      ...state,
      confirmedAt: Date.now(),
      status: 'confirmed',
      // Compiler ensures all required fields are provided
    });
  }
});

No Stale Closures

JavaScript closures can capture stale state values. StateFlow's signal-based architecture ensures handlers always receive the current state:

// ❌ Traditional approach - prone to stale closure
let count = 0;
setTimeout(() => {
  count++; // Might use stale value
}, 1000);

// ✅ StateFlow approach - always current
defineFlow(counterState.active, {
  increment: (state) => ({ count: state.count + 1 })
  // 'state' parameter is always the current state
});

No Circular Dependencies

StateFlow's unidirectional data flow prevents circular state dependencies:

// States can only be modified through signals
// This prevents State A from directly modifying State B
// which could then modify State A, creating a cycle

defineFlow(stateA.active, {
  update: (state, signal, context) => {
    // Can read other states from context
    const stateB = context.stateB;
    
    // But cannot directly modify them
    // Must return new state for A only
    return { ...state, value: stateB.value + 1 };
  }
});

Through these mechanisms, StateFlow provides strong guarantees about state consistency, making it suitable for applications where correctness is critical.

Previous
Installation