Advanced Topics
Architecture Guide
Advanced patterns for large-scale StateFlow applications, drawn from production systems like media players and real-time applications.
Multi-State Applications
Coordinated State Systems
Production applications often require 7+ interconnected states working together:
// Define related states that need to coordinate
const connectionState = defineState<{
url: string;
retryCount: number;
lastError?: Error;
}>()
.name("connection")
.variant("disconnected", true)
.variant("connecting")
.variant("connected")
.variant("failed")
.build();
const mediaState = defineState<{
duration: number;
position: number;
buffered: TimeRanges | null;
}>()
.name("media")
.variant("idle", true)
.variant("loading")
.variant("ready")
.variant("error")
.build();
const playbackState = defineState<{
volume: number;
playbackRate: number;
seeking: boolean;
}>()
.name("playback")
.variant("stopped", true)
.variant("playing")
.variant("paused")
.variant("buffering")
.build();
State Dependencies and Cross-State Logic
Implement state dependencies using the context parameter in flows:
// Application type with all states
interface MediaApplication {
connection: Infer<typeof connectionState>;
media: Infer<typeof mediaState>;
playback: Infer<typeof playbackState>;
}
// Flow that depends on other states
defineFlow(playbackState.stopped, {
play: (state, signal, context: MediaApplication) => {
// Check if we can play based on other states
if (stateVar(context.connection) !== 'connected') {
return Result.reject('Cannot play while disconnected');
}
if (stateVar(context.media) !== 'ready') {
return Result.reject('Media not ready for playback');
}
return playbackState.playing({
...state,
seeking: false
});
}
});
// Cross-state reactions
defineFlow(connectionState.connecting, {
connectionFailed: (state, signal, context: MediaApplication) => {
// When connection fails, reset dependent states
return connectionState.failed({
...state,
lastError: signal.error,
retryCount: state.retryCount + 1
});
}
});
Signal Organization at Scale
Hierarchical Signal Structure
For applications with 50+ signals, organize them by domain and purpose:
// Production example from video player
const signals = {
// Connection management
connection: {
connect: defineSignal<{ url: string; options?: ConnectionOptions }>("connect"),
disconnect: defineSignal("disconnect"),
retry: defineSignal("retry"),
timeout: defineSignal<{ after: number }>("timeout"),
},
// Media loading and preparation
media: {
load: defineSignal<{ source: MediaSource }>("loadMedia"),
metadata: defineSignal<{ duration: number; tracks: Track[] }>("mediaMetadata"),
error: defineSignal<{ code: number; message: string }>("mediaError"),
buffering: defineSignal<{ buffered: TimeRanges }>("buffering"),
},
// Playback control
playback: {
play: defineSignal("play"),
pause: defineSignal("pause"),
stop: defineSignal("stop"),
seek: defineSignal<{ position: number; precise?: boolean }>("seek"),
setVolume: defineSignal<{ level: number }>("setVolume"),
setRate: defineSignal<{ rate: number }>("setRate"),
},
// User interface
ui: {
showControls: defineSignal("showControls"),
hideControls: defineSignal("hideControls"),
toggleFullscreen: defineSignal("toggleFullscreen"),
showError: defineSignal<{ message: string; recoverable: boolean }>("showError"),
},
// Internal system signals
internal: {
tick: defineSignal<{ currentTime: number }>("tick"),
qualityChange: defineSignal<{ level: QualityLevel }>("qualityChange"),
cleanup: defineSignal("cleanup"),
}
};
Signal Naming Conventions
Establish consistent naming patterns:
// Pattern: domain.verb[Object] for user actions
signals.playback.seekToPosition({ position: 30 })
signals.connection.connectToUrl({ url: "..." })
signals.ui.showErrorMessage({ message: "..." })
// Pattern: domain.on[Event] for external events
signals.media.onLoadComplete({ duration: 180 })
signals.connection.onDisconnected()
signals.playback.onTimeUpdate({ position: 45 })
// Pattern: internal.[system] for internal operations
signals.internal.cleanup()
signals.internal.syncStates()
signals.internal.validateConfiguration()
Advanced State Patterns
State Machines with Complex Transitions
Use nested state logic for complex state machines:
const downloadState = defineState<{
url: string;
progress: number;
speed: number; // bytes/second
resumeData?: ResumeData;
}>()
.name("download")
.variant("idle", true)
.variant("preparing")
.variant("downloading")
.variant("paused")
.variant("completed")
.variant("failed")
.variant("cancelled")
.build();
// Complex state machine logic
defineFlow(downloadState.downloading, {
pause: (state) => {
// Can pause from downloading
return downloadState.paused({
...state,
resumeData: captureResumeData(state)
});
},
progress: (state, signal) => {
// Update progress while downloading
if (signal.progress >= 100) {
return downloadState.completed(state);
}
return {
...state,
progress: signal.progress,
speed: calculateSpeed(state, signal)
};
},
networkError: (state, signal) => {
// Handle network interruptions
if (signal.error.recoverable && state.resumeData) {
return downloadState.paused({
...state,
resumeData: signal.error.resumeData
});
}
return downloadState.failed({
...state,
error: signal.error
});
}
});
State Composition Patterns
Break complex states into composable pieces:
// Instead of one large state
interface MonolithicPlayerState {
// 50+ properties covering all aspects
connectionUrl: string;
connectionStatus: string;
mediaUrl: string;
mediaDuration: number;
playbackVolume: number;
playbackPosition: number;
uiControlsVisible: boolean;
uiFullscreen: boolean;
// ... many more
}
// Prefer composition of focused states
interface ComposedPlayerApplication {
connection: ConnectionState;
media: MediaState;
playback: PlaybackState;
ui: UIState;
quality: QualityState;
audio: AudioState;
video: VideoState;
}
// Each state handles its own domain
const audioState = defineState<{
volume: number;
muted: boolean;
tracks: AudioTrack[];
selectedTrack?: string;
}>()
.name("audio")
.variant("unavailable", true)
.variant("available")
.variant("processing")
.build();
Resource Management
Lifecycle Management with State Handlers
Use StateFlow's handler system for resource management:
interface VideoPlayerApp {
connection: Infer<typeof connectionState>;
media: Infer<typeof mediaState>;
playback: Infer<typeof playbackState>;
resources: {
webSocket?: WebSocket;
mediaElement?: HTMLVideoElement;
bufferController?: BufferController;
timers: Map<string, NodeJS.Timer>;
};
}
applyFlow(app, [connectionState, mediaState, playbackState], (sm) => {
// Resource acquisition
sm.addEnterHandler(connectionState.connecting, async (state, context) => {
return Result.transition(async () => {
try {
const ws = new WebSocket(state.url);
// Set up event handlers
ws.onopen = () => dispatch(context, signals.connection.connected()).done();
ws.onerror = (error) => dispatch(context, signals.connection.failed({ error })).done();
context.resources.webSocket = ws;
// Wait for connection with timeout
await waitForConnection(ws, 5000);
return Result.ok();
} catch (error) {
return Result.error(error);
}
}, 5000);
});
// Resource cleanup
sm.addExitHandler(connectionState.connected, (state, context) => {
if (context.resources.webSocket) {
context.resources.webSocket.close();
context.resources.webSocket = undefined;
}
return Result.ok();
});
// Automatic cleanup on state changes
sm.addExitHandler(playbackState.playing, (state, context) => {
// Clear any playback timers
context.resources.timers.forEach((timer) => clearInterval(timer));
context.resources.timers.clear();
return Result.ok();
});
// Error recovery
sm.addRollbackHandler(mediaState.loading, (state, context) => {
// Clean up partially loaded resources
if (context.resources.bufferController) {
context.resources.bufferController.abort();
context.resources.bufferController = undefined;
}
return Result.ok();
});
});
Memory Management and Cleanup
// Automatic disposal pattern
class VideoPlayerApplication implements Disposable {
private disposables: Array<{ [Symbol.dispose](): void }> = [];
constructor() {
// Set up observers that auto-cleanup
this.disposables.push(
observe(this, [connectionState.failed], (state) => {
this.handleConnectionFailure(state);
}),
observe(this, [mediaState.error], (state) => {
this.handleMediaError(state);
})
);
}
[Symbol.dispose]() {
// Clean up all observers
this.disposables.forEach(disposable => disposable[Symbol.dispose]());
// Clean up resources
this.resources.webSocket?.close();
this.resources.timers.forEach(timer => clearInterval(timer));
}
}
// Usage with automatic cleanup
function createPlayer(): VideoPlayerApplication {
using player = new VideoPlayerApplication();
// Player automatically disposed when scope exits
return player;
}
Integration Patterns
External System Integration
Connect StateFlow to external APIs and services:
// Service integration through handlers
interface ExternalServices {
analyticsService: AnalyticsService;
authService: AuthService;
mediaService: MediaService;
}
applyFlow(app, states, (sm) => {
// Analytics integration
sm.addEnterHandler(playbackState.playing, (state, context) => {
context.services.analyticsService.trackEvent('playback_started', {
mediaId: context.media.id,
position: state.position,
timestamp: Date.now()
});
return Result.ok();
});
// Authentication integration
sm.addEnterHandler(connectionState.connecting, async (state, context) => {
return Result.transition(async () => {
try {
const token = await context.services.authService.getValidToken();
// Update connection with auth token
const authenticatedUrl = addAuthToken(state.url, token);
return connectionState.connecting({
...state,
url: authenticatedUrl
});
} catch (error) {
return Result.error(error);
}
}, 3000);
});
});
Event System Integration
Bridge StateFlow with external event systems:
// Two-way integration with DOM events
class DOMIntegration {
constructor(private app: MediaApplication, private element: HTMLVideoElement) {
this.setupDOMListeners();
this.setupStateFlowObservers();
}
private setupDOMListeners() {
// DOM events -> StateFlow signals
this.element.addEventListener('play', () => {
dispatch(this.app, signals.playback.play()).done();
});
this.element.addEventListener('pause', () => {
dispatch(this.app, signals.playback.pause()).done();
});
this.element.addEventListener('timeupdate', () => {
dispatch(this.app, signals.internal.tick({
currentTime: this.element.currentTime
})).done();
});
this.element.addEventListener('error', (event) => {
dispatch(this.app, signals.media.error({
code: this.element.error?.code || 0,
message: this.element.error?.message || 'Unknown error'
})).done();
});
}
private setupStateFlowObservers() {
// StateFlow states -> DOM updates
observe(this.app, [playbackState.playing], (state) => {
if (this.element.paused) {
this.element.play().catch(error => {
dispatch(this.app, signals.playback.error({ error })).done();
});
}
});
observe(this.app, [playbackState.paused], (state) => {
if (!this.element.paused) {
this.element.pause();
}
});
observe(this.app, [playbackState.seeking], (state) => {
if (Math.abs(this.element.currentTime - state.position) > 0.5) {
this.element.currentTime = state.position;
}
});
}
}
Performance Optimization
Efficient Observer Patterns
Optimize observer performance for large applications:
// Batch observer updates
class BatchedObserver {
private pendingUpdates = new Map<string, StateInstance>();
private updateScheduled = false;
observe<T>(app: Application, states: StateVariant<T>[], handler: (state: StateInstance<T>) => void) {
return observe(app, states, (state) => {
// Batch updates instead of immediate handling
const stateKey = `${getName(state)}.${stateVar(state)}`;
this.pendingUpdates.set(stateKey, state);
if (!this.updateScheduled) {
this.updateScheduled = true;
queueMicrotask(() => this.flushUpdates(handler));
}
});
}
private flushUpdates<T>(handler: (state: StateInstance<T>) => void) {
for (const [key, state] of this.pendingUpdates) {
handler(state as StateInstance<T>);
}
this.pendingUpdates.clear();
this.updateScheduled = false;
}
}
// Selective observation with custom comparisons
observe(
app,
[mediaState.ready],
(state) => expensiveUIUpdate(state),
(prev, curr) => {
// Only update UI if meaningful properties changed
return prev.duration !== curr.duration ||
prev.tracks.length !== curr.tracks.length ||
prev.quality !== curr.quality;
}
);
State Validation and Runtime Checks
Add runtime validation for production applications:
// Schema validation with Zod
import { z } from 'zod';
const MediaPropsSchema = z.object({
url: z.string().url(),
duration: z.number().min(0),
position: z.number().min(0),
bitrate: z.number().positive().optional(),
tracks: z.array(z.object({
id: z.string(),
type: z.enum(['audio', 'video', 'subtitle']),
language: z.string().optional()
}))
});
const mediaState = defineState<z.infer<typeof MediaPropsSchema>>()
.name("media")
.variant("loading", true)
.variant("ready")
.variant("error")
.parser(obj => {
// Runtime validation
const result = MediaPropsSchema.safeParse(obj);
if (!result.success) {
throw new StateFlowError(`Invalid media state: ${result.error.message}`);
}
return result.data;
})
.stringRepr(s => `${s.url} (${Math.round(s.position)}/${Math.round(s.duration)}s)`)
.build();
Error Handling and Recovery
Graceful Degradation Patterns
Implement fallback states for robust applications:
const qualityState = defineState<{
current: QualityLevel;
available: QualityLevel[];
auto: boolean;
fallbackReason?: string;
}>()
.name("quality")
.variant("auto", true) // Automatic quality selection
.variant("manual") // User selected quality
.variant("degraded") // Fallback due to network/performance
.variant("unavailable") // No quality options available
.build();
defineFlow(qualityState.auto, {
networkDegraded: (state, signal) => {
// Automatic fallback to lower quality
const fallbackQuality = findBestQuality(
state.available,
signal.bandwidth,
signal.dropRate
);
if (!fallbackQuality) {
return qualityState.unavailable({
...state,
fallbackReason: 'No suitable quality for current network conditions'
});
}
return qualityState.degraded({
...state,
current: fallbackQuality,
fallbackReason: `Downgraded due to ${signal.reason}`
});
},
networkImproved: (state, signal) => {
// Try to upgrade quality when conditions improve
const betterQuality = findBetterQuality(
state.available,
state.current,
signal.bandwidth
);
if (betterQuality && betterQuality.bitrate > state.current.bitrate) {
return {
...state,
current: betterQuality,
fallbackReason: undefined
};
}
return Result.ignore('No better quality available');
}
});
These architectural patterns enable building complex, maintainable StateFlow applications that can scale with your needs while maintaining the core benefits of type safety, immutability, and explicit error handling.