Why Zustand?
The relay server manages complex state:- Multiple extension connections (each with its own tabs and targets)
- Multiple Playwright clients (MCP, CLI, programmatic API)
- WebSocket connections, pending CDP requests, timers
- Reconnection logic when extensions disconnect and reconnect
- Deterministic: State transitions must be predictable and testable
- Pure reducers: No I/O or side effects inside state transitions
- Centralized: Single source of truth for all relay state
- Immutable updates: Prevent bugs from accidental mutation
State Structure
Source:playwriter/src/relay-state.ts
ExtensionEntry
Each connected Chrome extension:ConnectedTarget
Each browser tab controlled by the extension:PlaywrightClient
Each Playwright connection (MCP, CLI, or programmatic):Pure State Transition Functions
All state updates use pure functions - no I/O, no side effects, just data in → data out:new Map(state.extensions) before mutation to preserve immutability.
State Transition Examples
Adding a Target
When extension attaches to a tab:Removing an Extension
When WebSocket closes:Derivation Helpers
Instead of storing derived data, Playwriter uses derivation functions that compute values on demand:- Simpler: No cache invalidation logic
- Correct: Always returns current state
- Fast enough: Linear scans over 5-10 extensions are negligible
Side Effects in Route Handlers
Rule: Pure state transitions have no I/O. Side effects (WebSocket sends, timers) happen in route handlers after state updates. Example: Extension connects:Reconnection Handling
When an extension reconnects (e.g., after relay server restart):- Extension connects with same
stableKeybut newid addExtension()creates a new entry without removing the old onefindExtensionByStableKey()returns the newest match (last in iteration order)- Old extension’s WebSocket
onClosefires later, callsremoveExtension() - In-flight CDP messages continue routing to the old connection until cleanup
Testing State Transitions
All state functions are pure and testable:Benefits of This Approach
Deterministic: State transitions are pure functions - same input always produces same output. Testable: No mocks needed - just call functions with data and assert on returned state. Debuggable: State changes are explicit function calls, easy to trace in logs. Maintainable: No hidden dependencies or global side effects - all logic is local to functions.Related
- Architecture - How relay server fits into overall system
- Sessions - Session isolation using this state structure