State Management

Signal System

StateFlow's signal system provides the exclusive pathway for state modifications, ensuring that every state change request receives explicit feedback. This architecture eliminates silent failures and provides comprehensive tracking of all state transitions.

Signal Architecture

The signal system operates on a fundamental principle that distinguishes StateFlow from traditional state management approaches. Rather than allowing direct state mutations, all state changes must flow through signals, which act as validated commands with guaranteed handling and explicit results.

Signal as Commands

Signals represent intent to change state rather than direct mutations. This distinction is crucial for maintaining system integrity. When you dispatch a signal, you are requesting a state change, not demanding it. The system evaluates this request against current state and business rules before deciding how to proceed.

// Traditional approach - direct mutation with no feedback
state.volume = 0.8; // What if this fails? What if it's invalid?

// StateFlow approach - signal with guaranteed feedback
const result = dispatch(app, signals.setVolume({ level: 0.8 }));
if (result.kind === ResultKind.Rejected) {
  console.error(`Volume change rejected: ${result.message}`);
}

Type-Safe Signal Parameters

Signals leverage TypeScript's type system to ensure parameter correctness at compile time. This prevents a entire class of runtime errors related to incorrect signal data.

const authSignals = {
  login: defineSignal<{
    username: string;
    password: string;
    rememberMe?: boolean;
  }>("login"),
  
  logout: defineSignal("logout"),
  
  updateProfile: defineSignal<{
    displayName?: string;
    email?: string;
    avatarUrl?: string;
  }>("updateProfile")
};

// Type checking prevents errors
dispatch(app, authSignals.login({ 
  username: "user@example.com"
  // Error: Property 'password' is missing
}));

Guaranteed Handling

Every signal dispatched through StateFlow receives explicit handling, even if that handling is to ignore the signal. This guarantee eliminates the uncertainty of whether a state change request was processed.

Explicit Results

StateFlow's Result type system ensures that every signal dispatch provides clear feedback about what occurred:

function performAction() {
  const result = dispatch(app, signals.play());
  
  // Exhaustive handling ensures no case is missed
  switch (result.kind) {
    case ResultKind.OK:
      updateUI("Playing");
      break;
      
    case ResultKind.Ignored:
      // Signal was not applicable to current state
      console.log("Already playing or signal not handled");
      break;
      
    case ResultKind.Rejected:
      showError(`Cannot play: ${result.message}`);
      break;
      
    case ResultKind.Error:
      logError(result.error);
      showError("An unexpected error occurred");
      break;
      
    case ResultKind.InTransition:
      showLoading();
      handleAsyncResult(result);
      break;
  }
}

Signal Routing

StateFlow automatically routes signals to the appropriate handlers based on current state. This routing is deterministic and type-safe:

defineFlow(mediaState.paused, {
  play: (state) => mediaState.playing(state),
  stop: (state) => mediaState.stopped({ ...state, position: 0 })
  // 'pause' signal is not defined - will be Ignored
});

defineFlow(mediaState.playing, {
  pause: (state) => mediaState.paused(state),
  stop: (state) => mediaState.stopped({ ...state, position: 0 })
  // 'play' signal is not defined - will be Ignored
});

// Signal routing based on current state
dispatch(app, signals.play());   // Handled if paused, ignored if playing
dispatch(app, signals.pause());  // Handled if playing, ignored if paused

Comprehensive Feedback

The signal system provides detailed feedback mechanisms that support debugging, monitoring, and error recovery.

Result Metadata

Results carry contextual information about signal processing:

const result = dispatch(app, signals.connect())
  .withSignal(signals.connect())
  .withHandlerName("handleConnect")
  .withStateUpdating("connection.disconnected->connection.connecting");

// Results can be inspected for debugging
if (result.kind === ResultKind.Error) {
  console.error("Signal:", result.signal);
  console.error("Handler:", result.handler);
  console.error("State transition:", result.stateUpdate);
  console.error("Error:", result.error);
  console.error("Stack:", result.stacktrace);
}

Result Merging

When multiple states handle a signal, their results are merged according to strict priority rules:

// Consider an application with multiple states
const app = {
  auth: { /* ... */ },
  data: { /* ... */ },
  ui: { /* ... */ }
};

// When a signal affects multiple states
const result = dispatch(app, signals.logout());

// Results are merged with priority:
// 1. Error - any error stops processing
// 2. Rejected - validation failures take precedence  
// 3. InTransition - async operations are awaited
// 4. OK - successful handling
// 5. Ignored - lowest priority

// The final result reflects the highest priority outcome

Asynchronous Signal Handling

Signals can trigger asynchronous operations while maintaining feedback guarantees:

applyFlow(app, [dataState], (sm) => {
  sm.addEnterHandler(dataState.loading, (state) => {
    return Result.transition(async () => {
      try {
        const data = await fetchData(state.query);
        // Dispatch follow-up signal
        return dispatch(app, signals.dataLoaded({ data }));
      } catch (error) {
        return Result.error(error);
      }
    }, 10000); // 10 second timeout
  });
});

// Async feedback handling
async function loadData(query: string) {
  const result = dispatch(app, signals.startLoading({ query }));
  
  if (result.kind === ResultKind.InTransition) {
    showLoadingSpinner();
    const finalResult = await result.done();
    hideLoadingSpinner();
    
    if (finalResult.kind === ResultKind.OK) {
      displayData();
    } else {
      showError(finalResult);
    }
  }
}

Signal Validation

The signal system supports comprehensive validation at multiple levels to ensure system integrity.

Parameter Validation

Signal handlers can validate parameters before processing:

defineFlow(formState.editing, {
  submit: (state, signal) => {
    // Email validation
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!emailRegex.test(signal.email)) {
      return Result.reject("Invalid email format");
    }
    
    // Password strength validation
    if (signal.password.length < 8) {
      return Result.reject("Password must be at least 8 characters");
    }
    
    // Business rule validation
    if (state.submitCount >= 3) {
      return Result.reject("Too many submission attempts");
    }
    
    return formState.submitting({
      ...state,
      formData: signal,
      submitCount: state.submitCount + 1
    });
  }
});

Cross-State Validation

Signals can be validated against the broader application context:

defineFlow(orderState.draft, {
  submit: (state, signal, context) => {
    // Validate against user state
    if (!context.user.isVerified) {
      return Result.reject("Account must be verified to place orders");
    }
    
    // Validate against inventory state
    const available = context.inventory[state.productId];
    if (available < state.quantity) {
      return Result.reject(`Only ${available} items in stock`);
    }
    
    // Validate against payment state
    if (context.payment.method === null) {
      return Result.reject("Payment method required");
    }
    
    return orderState.processing(state);
  }
});

Signal Patterns

Common patterns emerge when working with StateFlow's signal system that promote clean, maintainable code.

Command and Query Separation

Signals should represent commands (state changes) rather than queries (state reads):

// ✅ Good - signals for state changes
const signals = {
  startRecording: defineSignal("startRecording"),
  stopRecording: defineSignal("stopRecording"),
  setQuality: defineSignal<{ quality: 'high' | 'medium' | 'low' }>("setQuality")
};

// ❌ Avoid - signals for queries
const badSignals = {
  getRecordingStatus: defineSignal("getRecordingStatus"), // Use direct state access
  isRecording: defineSignal("isRecording") // Use state observation
};

Signal Composition

Complex operations can be composed from simpler signals:

async function saveAndClose(data: FormData) {
  // Compose multiple signals for complex operations
  const saveResult = await dispatch(app, signals.save({ data })).done();
  
  if (saveResult.kind === ResultKind.OK) {
    const closeResult = dispatch(app, signals.close());
    return closeResult;
  }
  
  return saveResult;
}

Error Recovery Signals

Design signals that support error recovery flows:

const recoverySignals = {
  retry: defineSignal<{ attemptNumber: number }>("retry"),
  fallback: defineSignal<{ useCache: boolean }>("fallback"),
  reset: defineSignal("reset")
};

defineFlow(apiState.error, {
  retry: (state, signal) => {
    if (signal.attemptNumber > 3) {
      return Result.reject("Maximum retry attempts exceeded");
    }
    return apiState.fetching({ ...state, attempt: signal.attemptNumber });
  },
  
  fallback: (state, signal) => {
    if (signal.useCache && state.cachedData) {
      return apiState.ready({ ...state, data: state.cachedData });
    }
    return Result.reject("No cached data available");
  },
  
  reset: () => apiState.idle(initialApiState)
});

Through these mechanisms, StateFlow's signal system provides a robust foundation for state management that guarantees handling, provides comprehensive feedback, and maintains system integrity throughout the application lifecycle.

Previous
State consistency