refactor: remove dependency injection, use direct imports

- Export store, connectionManager, logger, sendMessage, getTabBySessionId from background.ts
- recording.ts now imports directly instead of using initRecording()
- Simplify RecordingRelay constructor to take params directly instead of interface
This commit is contained in:
Tommy D. Rossi
2026-01-25 23:30:24 +01:00
parent 4a8caf19bd
commit 53450ee36f
4 changed files with 72 additions and 162 deletions
+3 -3
View File
@@ -264,11 +264,11 @@ export async function startPlayWriterCDPRelayServer({
}
// Recording relay for screen recording functionality
const recordingRelay = new RecordingRelay({
const recordingRelay = new RecordingRelay(
sendToExtension,
isExtensionConnected: () => extensionWs !== null,
() => extensionWs !== null,
logger,
})
)
// Auto-create initial tab when PLAYWRITER_AUTO_ENABLE is set and no targets exist.
// This allows Playwright to connect and immediately have a page to work with.
+31 -66
View File
@@ -29,30 +29,28 @@ export interface ActiveRecording {
resolveStop?: (result: StopRecordingResult) => void
}
interface RecordingRelayDeps {
sendToExtension: (params: { method: string; params?: unknown; timeout?: number }) => Promise<unknown>
isExtensionConnected: () => boolean
logger?: {
log(...args: unknown[]): void
error(...args: unknown[]): void
}
}
export class RecordingRelay {
private activeRecordings = new Map<number, ActiveRecording>()
// Track which tabId just sent recordingData metadata - used to route the next binary chunk
private lastRecordingMetadataTabId: number | null = null
private deps: RecordingRelayDeps
private sendToExtension: (params: { method: string; params?: unknown; timeout?: number }) => Promise<unknown>
private isExtensionConnected: () => boolean
private logger?: { log(...args: unknown[]): void; error(...args: unknown[]): void }
constructor(deps: RecordingRelayDeps) {
this.deps = deps
constructor(
sendToExtension: (params: { method: string; params?: unknown; timeout?: number }) => Promise<unknown>,
isExtensionConnected: () => boolean,
logger?: { log(...args: unknown[]): void; error(...args: unknown[]): void }
) {
this.sendToExtension = sendToExtension
this.isExtensionConnected = isExtensionConnected
this.logger = logger
}
/**
* Handle incoming binary data (recording chunks) from the extension.
* Returns true if the data was handled as a recording chunk.
*/
handleBinaryData(buffer: Buffer): boolean {
handleBinaryData(buffer: Buffer): void {
const tabId = this.lastRecordingMetadataTabId
this.lastRecordingMetadataTabId = null
@@ -60,41 +58,35 @@ export class RecordingRelay {
const recording = this.activeRecordings.get(tabId)
if (recording) {
recording.chunks.push(buffer)
this.deps.logger?.log(pc.blue(`Received recording chunk for tab ${tabId}: ${buffer.length} bytes (total chunks: ${recording.chunks.length})`))
return true
this.logger?.log(pc.blue(`Received recording chunk for tab ${tabId}: ${buffer.length} bytes (total chunks: ${recording.chunks.length})`))
} else {
this.deps.logger?.log(pc.yellow(`Received recording chunk for unknown tab ${tabId}, ignoring`))
this.logger?.log(pc.yellow(`Received recording chunk for unknown tab ${tabId}, ignoring`))
}
} else {
this.deps.logger?.log(pc.yellow('Received recording chunk without preceding metadata, ignoring'))
this.logger?.log(pc.yellow('Received recording chunk without preceding metadata, ignoring'))
}
return false
}
/**
* Handle recordingData message from extension.
* This is sent before binary chunks to identify which recording they belong to.
*/
handleRecordingData(message: RecordingDataMessage): void {
const { tabId, final } = message.params
const recording = this.activeRecordings.get(tabId)
// Track which tab sent this metadata for routing the next binary chunk
if (!final) {
this.lastRecordingMetadataTabId = tabId
}
if (recording && final) {
// This is the final marker - write all chunks to file
try {
const totalSize = recording.chunks.reduce((sum, chunk) => sum + chunk.length, 0)
const combined = Buffer.concat(recording.chunks)
fs.writeFileSync(recording.outputPath, combined)
const duration = Date.now() - recording.startedAt
this.deps.logger?.log(pc.green(`Recording saved: ${recording.outputPath} (${totalSize} bytes, ${duration}ms)`))
this.logger?.log(pc.green(`Recording saved: ${recording.outputPath} (${totalSize} bytes, ${duration}ms)`))
// Resolve the stop promise
if (recording.resolveStop) {
recording.resolveStop({
success: true,
@@ -106,7 +98,7 @@ export class RecordingRelay {
}
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error)
this.deps.logger?.error('Failed to write recording:', error)
this.logger?.error('Failed to write recording:', error)
if (recording.resolveStop) {
recording.resolveStop({ success: false, error: errorMessage })
}
@@ -114,7 +106,6 @@ export class RecordingRelay {
this.activeRecordings.delete(tabId)
}
// Non-final recordingData is just a marker that binary follows - handled in handleBinaryData
}
/**
@@ -124,7 +115,7 @@ export class RecordingRelay {
const { tabId } = message.params
const recording = this.activeRecordings.get(tabId)
if (recording) {
this.deps.logger?.log(pc.yellow(`Recording cancelled for tab ${tabId}`))
this.logger?.log(pc.yellow(`Recording cancelled for tab ${tabId}`))
if (recording.resolveStop) {
recording.resolveStop({ success: false, error: 'Recording was cancelled' })
}
@@ -132,9 +123,6 @@ export class RecordingRelay {
}
}
/**
* Start recording a tab.
*/
async startRecording(params: StartRecordingParams & { outputPath: string }): Promise<StartRecordingResult> {
const { outputPath, ...recordingParams } = params
@@ -142,18 +130,17 @@ export class RecordingRelay {
return { success: false, error: 'outputPath is required' }
}
if (!this.deps.isExtensionConnected()) {
if (!this.isExtensionConnected()) {
return { success: false, error: 'Extension not connected' }
}
// Ensure output directory exists
const dir = path.dirname(outputPath)
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true })
}
try {
const result = await this.deps.sendToExtension({
const result = await this.sendToExtension({
method: 'startRecording',
params: recordingParams,
timeout: 10000,
@@ -164,7 +151,6 @@ export class RecordingRelay {
}
if (result.success) {
// Track this recording - store sessionId for lookup when stopping
this.activeRecordings.set(result.tabId, {
tabId: result.tabId,
sessionId: recordingParams.sessionId,
@@ -172,27 +158,22 @@ export class RecordingRelay {
chunks: [],
startedAt: result.startedAt,
})
this.deps.logger?.log(pc.green(`Recording started for tab ${result.tabId} (sessionId: ${recordingParams.sessionId || 'none'}), output: ${outputPath}`))
this.logger?.log(pc.green(`Recording started for tab ${result.tabId} (sessionId: ${recordingParams.sessionId || 'none'}), output: ${outputPath}`))
}
return result
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error)
this.deps.logger?.error('Start recording error:', error)
this.logger?.error('Start recording error:', error)
return { success: false, error: errorMessage }
}
}
/**
* Stop recording and save to file.
*/
async stopRecording(params: StopRecordingParams): Promise<StopRecordingResult> {
if (!this.deps.isExtensionConnected()) {
if (!this.isExtensionConnected()) {
return { success: false, error: 'Extension not connected' }
}
// Find the active recording by sessionId for concurrent recording support
// If no sessionId provided, fall back to the first recording (backward compatibility)
const findRecording = (): ActiveRecording | undefined => {
if (params.sessionId) {
for (const recording of this.activeRecordings.values()) {
@@ -200,10 +181,8 @@ export class RecordingRelay {
return recording
}
}
// SessionId provided but no matching recording found
return undefined
}
// No sessionId - return first recording (backward compat for single-recording case)
return this.activeRecordings.values().next().value
}
@@ -216,7 +195,6 @@ export class RecordingRelay {
return { success: false, error: errorMsg }
}
// Set up promise to wait for final chunk
let timeoutId: ReturnType<typeof setTimeout>
const finalPromise = new Promise<StopRecordingResult>((resolve) => {
const wrappedResolve = (result: StopRecordingResult) => {
@@ -224,7 +202,6 @@ export class RecordingRelay {
resolve(result)
}
recording.resolveStop = wrappedResolve
// Timeout after 30 seconds
timeoutId = setTimeout(() => {
if (recording.resolveStop) {
recording.resolveStop = undefined
@@ -234,8 +211,7 @@ export class RecordingRelay {
})
try {
// Tell extension to stop recording
const result = await this.deps.sendToExtension({
const result = await this.sendToExtension({
method: 'stopRecording',
params,
timeout: 10000,
@@ -247,55 +223,44 @@ export class RecordingRelay {
return result
}
// Wait for final chunk to arrive
return await finalPromise
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error)
this.deps.logger?.error('Stop recording error:', error)
this.logger?.error('Stop recording error:', error)
return { success: false, error: errorMessage }
}
}
/**
* Check if recording is active.
*/
async isRecording(params: IsRecordingParams): Promise<IsRecordingResult> {
if (!this.deps.isExtensionConnected()) {
if (!this.isExtensionConnected()) {
return { isRecording: false }
}
try {
return await this.deps.sendToExtension({
return await this.sendToExtension({
method: 'isRecording',
params,
timeout: 5000,
}) as IsRecordingResult
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error)
this.deps.logger?.error('Recording status error:', error)
} catch {
return { isRecording: false }
}
}
/**
* Cancel recording without saving.
*/
async cancelRecording(params: CancelRecordingParams): Promise<CancelRecordingResult> {
if (!this.deps.isExtensionConnected()) {
if (!this.isExtensionConnected()) {
return { success: false, error: 'Extension not connected' }
}
try {
// Note: Recording cleanup is handled by the 'recordingCancelled' event handler
// which is triggered by the extension after cancellation.
return await this.deps.sendToExtension({
return await this.sendToExtension({
method: 'cancelRecording',
params,
timeout: 5000,
}) as CancelRecordingResult
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error)
this.deps.logger?.error('Cancel recording error:', error)
this.logger?.error('Cancel recording error:', error)
return { success: false, error: errorMessage }
}
}