Files
playwriter/extension-new/COMPARISON.md
Tommy D. Rossi 1ac644d4bd new code
2025-11-15 13:06:08 +01:00

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()