6.3 KiB
6.3 KiB
Old vs New Implementation Comparison
Old Implementation Issues
Class-Based Complexity
Old (cdpRelay.ts):
export class CDPRelayServer {
private _wsHost: string
private _cdpPath: string
private _extensionPath: string
private _wss: WebSocketServer
private _playwrightConnection: WebSocket | null = null
private _extensionConnection: ExtensionConnection | null = null
private _connectedTargets: Map<string, ConnectedTarget> = new Map()
private _extensionConnectionPromise!: ManualPromise<void>
constructor(server: http.Server) {
// Initialize all state
this._wss = new wsServer({ server })
this._wss.on('connection', (ws, request) => {
// Complex nested logic
})
}
private _handlePlaywrightConnection(ws: WebSocket): void { }
private _handleExtensionMessage(message: ExtensionEventMessage) { }
private async _handlePlaywrightMessage(message: CDPCommand): Promise<void> { }
private _sendToPlaywright(message: CDPResponse | CDPEvent): void { }
// ... many more private methods
}
class ExtensionConnection {
// Nested class with its own state
}
Issues:
- State scattered across private fields
- Methods mutate class state (
this._playwrightConnection = ws) - Nested classes make it hard to follow data flow
- Manual promise management (
ManualPromise)
New (Functional):**
export async function startRelayServer({ port = 9988 }) {
const targetsRegistry = createTargetsRegistry()
let playwrightWs: WSContext | null = null
let extensionWs: WSContext | null = null
let extensionConnection: ExtensionConnection | null = null
// All state is local variables
// All logic is inline or in pure functions
return {
cdpEndpoint,
extensionEndpoint,
close() { /* cleanup */ }
}
}
Benefits:
- All state in one place (function scope)
- Clear lifecycle (create → use → cleanup)
- No
this, easier to reason about - Returns cleanup function
Message Handling
Old:
// Spread across multiple private methods
private async _handlePlaywrightMessage(message: CDPCommand): Promise<void> {
const forwardToExtension = async (...) => { }
const handleCDPCommand = async (...) => {
switch (method) {
case 'Browser.getVersion': { }
case 'Target.setAutoAttach': { }
// ...
}
}
try {
const result = await handleCDPCommand(...)
this._sendToPlaywright({ id, sessionId, result })
} catch (e) {
this._sendToPlaywright({ error })
}
}
New:
// CDP routing extracted to dedicated module
const router = createCdpRouter({ targetsRegistry, extensionConnection })
const result: any = await router.route({ method, params, sessionId })
sendToPlaywright({ id, sessionId, result })
Benefits:
- Separation of concerns
- Router is testable in isolation
- Clear dependencies (passed as args)
WebSocket Setup
Old (Manual ws library):
this._wss = new wsServer({ server })
this._wss.on('connection', (ws: WebSocket, request: http.IncomingMessage) => {
const url = new URL(`http://localhost${request.url}`)
if (url.pathname === this._cdpPath) {
this._handlePlaywrightConnection(ws)
} else if (url.pathname === this._extensionPath) {
this._extensionConnection = new ExtensionConnection(ws)
}
})
New (Hono WebSocket):
const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app })
app.get('/cdp', upgradeWebSocket(() => ({
onOpen(_event, ws) { },
onMessage(event, ws) { },
onClose() { },
onError(event) { }
})))
app.get('/extension', upgradeWebSocket(() => ({
onOpen(_event, ws) { },
onMessage(event, ws) { },
onClose() { },
onError(event) { }
})))
Benefits:
- Declarative routing
- Built-in event handlers
- Framework handles upgrade protocol
- Cleaner separation between endpoints
Targets Registry
Old (Private Map):
class CDPRelayServer {
private _connectedTargets: Map<string, ConnectedTarget> = new Map()
// Scattered operations
this._connectedTargets.set(targetParams.sessionId, { ... })
this._connectedTargets.delete(detachParams.sessionId)
for (const target of this._connectedTargets.values()) { }
}
New (Functional Registry):
function createTargetsRegistry() {
const targets = new Map<string, ConnectedTarget>()
return {
add({ sessionId, targetId, targetInfo }) { },
remove(sessionId) { },
get(sessionId) { },
findByTargetId(targetId) { },
getAll() { },
clear() { }
}
}
const targetsRegistry = createTargetsRegistry()
targetsRegistry.add({ ... })
targetsRegistry.remove(sessionId)
Benefits:
- Encapsulated state
- Clear API
- Easy to test
- Can be reused
Logging
Old:
debugLogger(`New connection to ${url.pathname}`)
debugLogger('Rejecting second Playwright connection')
debugLogger('Playwright MCP connected')
debugLogger('Extension WebSocket closed:', reason, c === this._extensionConnection)
debugLogger(`← Playwright: ${message.method} (id=${message.id})`)
debugLogger('\x1b[36m← Playwright:\x1b[0m', `${message.method} (id=${message.id})`)
debugLogger('\x1b[33m← Extension:\x1b[0m', `Target.attachedToTarget ...`)
debugLogger('\x1b[32m→ Playwright:\x1b[0m', logMessage)
debugLogger('\x1b[31mError in the extension:\x1b[0m', e)
New:
console.log('Playwright connected')
console.log('Extension connected')
console.log('← Playwright:', `${message.method} (id=${message.id})`)
console.log('← Extension:', method)
console.error('Error handling CDP command:', e)
Benefits:
- Simple console.log
- Only important events
- No color codes
- No verbose state dumps
Code Metrics
| Metric | Old | New |
|---|---|---|
| Files | 4 | 2 (relay-server.ts + protocol.ts) |
| Main class LOC | ~417 | 321 (everything inlined) |
| Total LOC | ~600 | ~370 |
| Classes | 2 | 0 |
| Private methods | 10+ | 0 |
| Helper modules | 0 | 0 (all inlined) |
| Dependencies | playwright-core (ws) | hono, @hono/node-ws |
Migration Path
The new implementation is a drop-in replacement:
Old:
const httpServer = await startHttpServer({ port: 9988 })
const cdpRelayServer = new CDPRelayServer(httpServer)
cdpRelayServer.stop()
New:
const server = await startRelayServer({ port: 9988 })
server.close()