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
+6 -25
View File
@@ -5,7 +5,6 @@ import type { ExtensionState, ConnectionState, TabState, TabInfo } from './types
import type { CDPEvent, Protocol } from 'playwriter/src/cdp-types' import type { CDPEvent, Protocol } from 'playwriter/src/cdp-types'
import type { ExtensionCommandMessage, ExtensionResponseMessage } from 'playwriter/src/protocol' import type { ExtensionCommandMessage, ExtensionResponseMessage } from 'playwriter/src/protocol'
import { import {
initRecording,
getActiveRecordings, getActiveRecordings,
handleStartRecording, handleStartRecording,
handleStopRecording, handleStopRecording,
@@ -412,9 +411,9 @@ class ConnectionManager {
} }
} }
const connectionManager = new ConnectionManager() export const connectionManager = new ConnectionManager()
const store = createStore<ExtensionState>(() => ({ export const store = createStore<ExtensionState>(() => ({
tabs: new Map(), tabs: new Map(),
connectionState: 'idle', connectionState: 'idle',
currentTabId: undefined, currentTabId: undefined,
@@ -477,7 +476,7 @@ function sendLog(level: string, args: any[]) {
}) })
} }
const logger = { export const logger = {
log: (...args: any[]) => { log: (...args: any[]) => {
console.log(...args) console.log(...args)
sendLog('log', args) sendLog('log', args)
@@ -518,7 +517,7 @@ self.addEventListener('unhandledrejection', (event) => {
}) })
let messageCount = 0 let messageCount = 0
function sendMessage(message: any): void { export function sendMessage(message: any): void {
if (connectionManager.ws?.readyState === WebSocket.OPEN) { if (connectionManager.ws?.readyState === WebSocket.OPEN) {
try { try {
connectionManager.ws.send(JSON.stringify(message)) connectionManager.ws.send(JSON.stringify(message))
@@ -602,7 +601,7 @@ async function syncTabGroup(): Promise<void> {
} }
} }
function getTabBySessionId(sessionId: string): { tabId: number; tab: TabInfo } | undefined { export function getTabBySessionId(sessionId: string): { tabId: number; tab: TabInfo } | undefined {
for (const [tabId, tab] of store.getState().tabs) { for (const [tabId, tab] of store.getState().tabs) {
if (tab.sessionId === sessionId) { if (tab.sessionId === sessionId) {
return { tabId, tab } return { tabId, tab }
@@ -620,6 +619,7 @@ function getTabByTargetId(targetId: string): { tabId: number; tab: TabInfo } | u
return undefined return undefined
} }
<<<<<<< HEAD
function emitChildDetachesForTab(tabId: number): void { function emitChildDetachesForTab(tabId: number): void {
const childEntries = Array.from(childSessions.entries()) const childEntries = Array.from(childSessions.entries())
.filter(([_, parentTab]) => parentTab.tabId === tabId) .filter(([_, parentTab]) => parentTab.tabId === tabId)
@@ -640,25 +640,6 @@ function emitChildDetachesForTab(tabId: number): void {
}) })
} }
// Initialize recording module with required dependencies
initRecording({
getTabBySessionId,
getTabs: () => store.getState().tabs,
updateTabRecordingState: (tabId: number, isRecording: boolean) => {
store.setState((state) => {
const newTabs = new Map(state.tabs)
const existing = newTabs.get(tabId)
if (existing) {
newTabs.set(tabId, { ...existing, isRecording })
}
return { tabs: newTabs }
})
},
sendMessage,
isWebSocketOpen: () => connectionManager.ws?.readyState === WebSocket.OPEN,
logger,
})
async function handleCommand(msg: ExtensionCommandMessage): Promise<any> { async function handleCommand(msg: ExtensionCommandMessage): Promise<any> {
if (msg.method !== 'forwardCDPCommand') return if (msg.method !== 'forwardCDPCommand') return
+32 -68
View File
@@ -3,7 +3,7 @@
* Uses chrome.tabCapture to record tabs via an offscreen document. * Uses chrome.tabCapture to record tabs via an offscreen document.
*/ */
import type { RecordingInfo, TabInfo } from './types' import type { RecordingInfo } from './types'
import type { import type {
StartRecordingParams, StartRecordingParams,
StopRecordingParams, StopRecordingParams,
@@ -19,37 +19,14 @@ import type {
OffscreenStopRecordingResult, OffscreenStopRecordingResult,
OffscreenIsRecordingResult, OffscreenIsRecordingResult,
} from './offscreen-types' } from './offscreen-types'
import { store, connectionManager, logger, sendMessage, getTabBySessionId } from './background'
// Dependencies injected from background.ts
interface RecordingDeps {
getTabBySessionId: (sessionId: string) => { tabId: number; tab: TabInfo } | undefined
getTabs: () => Map<number, TabInfo>
updateTabRecordingState: (tabId: number, isRecording: boolean) => void
sendMessage: (message: unknown) => void
isWebSocketOpen: () => boolean
logger: {
debug: (...args: unknown[]) => void
error: (...args: unknown[]) => void
}
}
// Active recordings - kept outside store since MediaRecorder/MediaStream can't be serialized // Active recordings - kept outside store since MediaRecorder/MediaStream can't be serialized
const activeRecordings: Map<number, RecordingInfo> = new Map() const activeRecordings: Map<number, RecordingInfo> = new Map()
// Module-level dependencies (set via initRecording)
let deps: RecordingDeps | null = null
// Offscreen document management // Offscreen document management
let offscreenDocumentCreating: Promise<void> | null = null let offscreenDocumentCreating: Promise<void> | null = null
/**
* Initialize the recording module with dependencies from background.ts.
* Must be called before using any recording functions.
*/
export function initRecording(dependencies: RecordingDeps): void {
deps = dependencies
}
/** /**
* Get the active recordings map (for cleanup on tab disconnect). * Get the active recordings map (for cleanup on tab disconnect).
*/ */
@@ -87,13 +64,9 @@ async function ensureOffscreenDocument(): Promise<void> {
} }
function resolveTabIdFromSessionId(sessionId?: string): number | undefined { function resolveTabIdFromSessionId(sessionId?: string): number | undefined {
if (!deps) {
throw new Error('Recording module not initialized')
}
if (!sessionId) { if (!sessionId) {
// Return the first connected tab // Return the first connected tab
for (const [tabId, tab] of deps.getTabs()) { for (const [tabId, tab] of store.getState().tabs) {
if (tab.state === 'connected') { if (tab.state === 'connected') {
return tabId return tabId
} }
@@ -101,15 +74,22 @@ function resolveTabIdFromSessionId(sessionId?: string): number | undefined {
return undefined return undefined
} }
const found = deps.getTabBySessionId(sessionId) const found = getTabBySessionId(sessionId)
return found?.tabId return found?.tabId
} }
export async function handleStartRecording(params: StartRecordingParams): Promise<StartRecordingResult> { function updateTabRecordingState(tabId: number, isRecording: boolean): void {
if (!deps) { store.setState((state) => {
throw new Error('Recording module not initialized') const newTabs = new Map(state.tabs)
} const existing = newTabs.get(tabId)
if (existing) {
newTabs.set(tabId, { ...existing, isRecording })
}
return { tabs: newTabs }
})
}
export async function handleStartRecording(params: StartRecordingParams): Promise<StartRecordingResult> {
const tabId = resolveTabIdFromSessionId(params.sessionId) const tabId = resolveTabIdFromSessionId(params.sessionId)
if (!tabId) { if (!tabId) {
return { success: false, error: 'No connected tab found for recording. Click the Playwriter extension icon on the tab you want to record.' } return { success: false, error: 'No connected tab found for recording. Click the Playwriter extension icon on the tab you want to record.' }
@@ -119,12 +99,12 @@ export async function handleStartRecording(params: StartRecordingParams): Promis
return { success: false, error: 'Recording already in progress for this tab' } return { success: false, error: 'Recording already in progress for this tab' }
} }
const tabInfo = deps.getTabs().get(tabId) const tabInfo = store.getState().tabs.get(tabId)
if (!tabInfo || tabInfo.state !== 'connected') { if (!tabInfo || tabInfo.state !== 'connected') {
return { success: false, error: 'Tab is not connected' } return { success: false, error: 'Tab is not connected' }
} }
deps.logger.debug('Starting recording for tab:', tabId, 'params:', params) logger.debug('Starting recording for tab:', tabId, 'params:', params)
try { try {
// Ensure offscreen document exists // Ensure offscreen document exists
@@ -150,7 +130,7 @@ export async function handleStartRecording(params: StartRecordingParams): Promis
}) })
}) })
deps.logger.debug('Got stream ID for tab:', tabId, 'streamId:', streamId.substring(0, 20) + '...') logger.debug('Got stream ID for tab:', tabId, 'streamId:', streamId.substring(0, 20) + '...')
// Send message to offscreen document to start recording // Send message to offscreen document to start recording
const result = await chrome.runtime.sendMessage({ const result = await chrome.runtime.sendMessage({
@@ -173,22 +153,18 @@ export async function handleStartRecording(params: StartRecordingParams): Promis
activeRecordings.set(tabId, { tabId, startedAt }) activeRecordings.set(tabId, { tabId, startedAt })
// Update tab state // Update tab state
deps.updateTabRecordingState(tabId, true) updateTabRecordingState(tabId, true)
deps.logger.debug('Recording started for tab:', tabId, 'mimeType:', result.mimeType) logger.debug('Recording started for tab:', tabId, 'mimeType:', result.mimeType)
return { success: true, tabId, startedAt } return { success: true, tabId, startedAt }
} catch (error: unknown) { } catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error) const errorMessage = error instanceof Error ? error.message : String(error)
deps.logger.error('Failed to start recording:', error) logger.error('Failed to start recording:', error)
return { success: false, error: errorMessage } return { success: false, error: errorMessage }
} }
} }
export async function handleStopRecording(params: StopRecordingParams): Promise<ExtensionStopRecordingResult> { export async function handleStopRecording(params: StopRecordingParams): Promise<ExtensionStopRecordingResult> {
if (!deps) {
throw new Error('Recording module not initialized')
}
const tabId = resolveTabIdFromSessionId(params.sessionId) const tabId = resolveTabIdFromSessionId(params.sessionId)
if (!tabId) { if (!tabId) {
return { success: false, error: 'No connected tab found' } return { success: false, error: 'No connected tab found' }
@@ -199,7 +175,7 @@ export async function handleStopRecording(params: StopRecordingParams): Promise<
return { success: false, error: 'No active recording for this tab' } return { success: false, error: 'No active recording for this tab' }
} }
deps.logger.debug('Stopping recording for tab:', tabId) logger.debug('Stopping recording for tab:', tabId)
try { try {
// Send message to offscreen document to stop recording - include tabId for concurrent support // Send message to offscreen document to stop recording - include tabId for concurrent support
@@ -216,22 +192,18 @@ export async function handleStopRecording(params: StopRecordingParams): Promise<
// Clean up // Clean up
activeRecordings.delete(tabId) activeRecordings.delete(tabId)
deps.updateTabRecordingState(tabId, false) updateTabRecordingState(tabId, false)
deps.logger.debug('Recording stopped for tab:', tabId, 'duration:', duration) logger.debug('Recording stopped for tab:', tabId, 'duration:', duration)
return { success: true, tabId, duration } return { success: true, tabId, duration }
} catch (error: unknown) { } catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error) const errorMessage = error instanceof Error ? error.message : String(error)
deps.logger.error('Failed to stop recording:', error) logger.error('Failed to stop recording:', error)
return { success: false, error: errorMessage } return { success: false, error: errorMessage }
} }
} }
export async function handleIsRecording(params: IsRecordingParams): Promise<IsRecordingResult> { export async function handleIsRecording(params: IsRecordingParams): Promise<IsRecordingResult> {
if (!deps) {
throw new Error('Recording module not initialized')
}
const tabId = resolveTabIdFromSessionId(params.sessionId) const tabId = resolveTabIdFromSessionId(params.sessionId)
if (!tabId) { if (!tabId) {
return { isRecording: false } return { isRecording: false }
@@ -261,10 +233,6 @@ export async function handleIsRecording(params: IsRecordingParams): Promise<IsRe
} }
export async function handleCancelRecording(params: CancelRecordingParams): Promise<CancelRecordingResult> { export async function handleCancelRecording(params: CancelRecordingParams): Promise<CancelRecordingResult> {
if (!deps) {
throw new Error('Recording module not initialized')
}
const tabId = resolveTabIdFromSessionId(params.sessionId) const tabId = resolveTabIdFromSessionId(params.sessionId)
if (!tabId) { if (!tabId) {
return { success: false, error: 'No connected tab found' } return { success: false, error: 'No connected tab found' }
@@ -275,7 +243,7 @@ export async function handleCancelRecording(params: CancelRecordingParams): Prom
return { success: true } // Already not recording return { success: true } // Already not recording
} }
deps.logger.debug('Cancelling recording for tab:', tabId) logger.debug('Cancelling recording for tab:', tabId)
try { try {
// Send message to offscreen document to cancel recording - include tabId for concurrent support // Send message to offscreen document to cancel recording - include tabId for concurrent support
@@ -285,11 +253,11 @@ export async function handleCancelRecording(params: CancelRecordingParams): Prom
}) })
activeRecordings.delete(tabId) activeRecordings.delete(tabId)
deps.updateTabRecordingState(tabId, false) updateTabRecordingState(tabId, false)
// Send cancel marker // Send cancel marker
if (deps.isWebSocketOpen()) { if (connectionManager.ws?.readyState === WebSocket.OPEN) {
deps.sendMessage({ sendMessage({
method: 'recordingCancelled', method: 'recordingCancelled',
params: { tabId }, params: { tabId },
}) })
@@ -298,7 +266,7 @@ export async function handleCancelRecording(params: CancelRecordingParams): Prom
return { success: true } return { success: true }
} catch (error: unknown) { } catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error) const errorMessage = error instanceof Error ? error.message : String(error)
deps.logger.error('Failed to cancel recording:', error) logger.error('Failed to cancel recording:', error)
return { success: false, error: errorMessage } return { success: false, error: errorMessage }
} }
} }
@@ -307,18 +275,14 @@ export async function handleCancelRecording(params: CancelRecordingParams): Prom
* Clean up recordings when tab is disconnected. * Clean up recordings when tab is disconnected.
*/ */
export async function cleanupRecordingForTab(tabId: number): Promise<void> { export async function cleanupRecordingForTab(tabId: number): Promise<void> {
if (!deps) {
return
}
const recording = activeRecordings.get(tabId) const recording = activeRecordings.get(tabId)
if (recording) { if (recording) {
deps.logger.debug('Cleaning up recording for disconnected tab:', tabId) logger.debug('Cleaning up recording for disconnected tab:', tabId)
try { try {
// Tell offscreen document to cancel recording - include tabId for concurrent support // Tell offscreen document to cancel recording - include tabId for concurrent support
await chrome.runtime.sendMessage({ action: 'cancelRecording', tabId }) await chrome.runtime.sendMessage({ action: 'cancelRecording', tabId })
} catch (e) { } catch (e) {
deps.logger.debug('Error cleaning up recording:', e) logger.debug('Error cleaning up recording:', e)
} }
activeRecordings.delete(tabId) activeRecordings.delete(tabId)
} }
+3 -3
View File
@@ -264,11 +264,11 @@ export async function startPlayWriterCDPRelayServer({
} }
// Recording relay for screen recording functionality // Recording relay for screen recording functionality
const recordingRelay = new RecordingRelay({ const recordingRelay = new RecordingRelay(
sendToExtension, sendToExtension,
isExtensionConnected: () => extensionWs !== null, () => extensionWs !== null,
logger, logger,
}) )
// Auto-create initial tab when PLAYWRITER_AUTO_ENABLE is set and no targets exist. // 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. // 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 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 { export class RecordingRelay {
private activeRecordings = new Map<number, ActiveRecording>() private activeRecordings = new Map<number, ActiveRecording>()
// Track which tabId just sent recordingData metadata - used to route the next binary chunk // Track which tabId just sent recordingData metadata - used to route the next binary chunk
private lastRecordingMetadataTabId: number | null = null 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) { constructor(
this.deps = deps 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. * 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 const tabId = this.lastRecordingMetadataTabId
this.lastRecordingMetadataTabId = null this.lastRecordingMetadataTabId = null
@@ -60,41 +58,35 @@ export class RecordingRelay {
const recording = this.activeRecordings.get(tabId) const recording = this.activeRecordings.get(tabId)
if (recording) { if (recording) {
recording.chunks.push(buffer) recording.chunks.push(buffer)
this.deps.logger?.log(pc.blue(`Received recording chunk for tab ${tabId}: ${buffer.length} bytes (total chunks: ${recording.chunks.length})`)) this.logger?.log(pc.blue(`Received recording chunk for tab ${tabId}: ${buffer.length} bytes (total chunks: ${recording.chunks.length})`))
return true
} else { } 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 { } 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. * Handle recordingData message from extension.
* This is sent before binary chunks to identify which recording they belong to.
*/ */
handleRecordingData(message: RecordingDataMessage): void { handleRecordingData(message: RecordingDataMessage): void {
const { tabId, final } = message.params const { tabId, final } = message.params
const recording = this.activeRecordings.get(tabId) const recording = this.activeRecordings.get(tabId)
// Track which tab sent this metadata for routing the next binary chunk
if (!final) { if (!final) {
this.lastRecordingMetadataTabId = tabId this.lastRecordingMetadataTabId = tabId
} }
if (recording && final) { if (recording && final) {
// This is the final marker - write all chunks to file
try { try {
const totalSize = recording.chunks.reduce((sum, chunk) => sum + chunk.length, 0) const totalSize = recording.chunks.reduce((sum, chunk) => sum + chunk.length, 0)
const combined = Buffer.concat(recording.chunks) const combined = Buffer.concat(recording.chunks)
fs.writeFileSync(recording.outputPath, combined) fs.writeFileSync(recording.outputPath, combined)
const duration = Date.now() - recording.startedAt 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) { if (recording.resolveStop) {
recording.resolveStop({ recording.resolveStop({
success: true, success: true,
@@ -106,7 +98,7 @@ export class RecordingRelay {
} }
} catch (error: unknown) { } catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error) 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) { if (recording.resolveStop) {
recording.resolveStop({ success: false, error: errorMessage }) recording.resolveStop({ success: false, error: errorMessage })
} }
@@ -114,7 +106,6 @@ export class RecordingRelay {
this.activeRecordings.delete(tabId) 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 { tabId } = message.params
const recording = this.activeRecordings.get(tabId) const recording = this.activeRecordings.get(tabId)
if (recording) { 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) { if (recording.resolveStop) {
recording.resolveStop({ success: false, error: 'Recording was cancelled' }) 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> { async startRecording(params: StartRecordingParams & { outputPath: string }): Promise<StartRecordingResult> {
const { outputPath, ...recordingParams } = params const { outputPath, ...recordingParams } = params
@@ -142,18 +130,17 @@ export class RecordingRelay {
return { success: false, error: 'outputPath is required' } return { success: false, error: 'outputPath is required' }
} }
if (!this.deps.isExtensionConnected()) { if (!this.isExtensionConnected()) {
return { success: false, error: 'Extension not connected' } return { success: false, error: 'Extension not connected' }
} }
// Ensure output directory exists
const dir = path.dirname(outputPath) const dir = path.dirname(outputPath)
if (!fs.existsSync(dir)) { if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true }) fs.mkdirSync(dir, { recursive: true })
} }
try { try {
const result = await this.deps.sendToExtension({ const result = await this.sendToExtension({
method: 'startRecording', method: 'startRecording',
params: recordingParams, params: recordingParams,
timeout: 10000, timeout: 10000,
@@ -164,7 +151,6 @@ export class RecordingRelay {
} }
if (result.success) { if (result.success) {
// Track this recording - store sessionId for lookup when stopping
this.activeRecordings.set(result.tabId, { this.activeRecordings.set(result.tabId, {
tabId: result.tabId, tabId: result.tabId,
sessionId: recordingParams.sessionId, sessionId: recordingParams.sessionId,
@@ -172,27 +158,22 @@ export class RecordingRelay {
chunks: [], chunks: [],
startedAt: result.startedAt, 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 return result
} catch (error: unknown) { } catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error) 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 } return { success: false, error: errorMessage }
} }
} }
/**
* Stop recording and save to file.
*/
async stopRecording(params: StopRecordingParams): Promise<StopRecordingResult> { async stopRecording(params: StopRecordingParams): Promise<StopRecordingResult> {
if (!this.deps.isExtensionConnected()) { if (!this.isExtensionConnected()) {
return { success: false, error: 'Extension not connected' } 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 => { const findRecording = (): ActiveRecording | undefined => {
if (params.sessionId) { if (params.sessionId) {
for (const recording of this.activeRecordings.values()) { for (const recording of this.activeRecordings.values()) {
@@ -200,10 +181,8 @@ export class RecordingRelay {
return recording return recording
} }
} }
// SessionId provided but no matching recording found
return undefined return undefined
} }
// No sessionId - return first recording (backward compat for single-recording case)
return this.activeRecordings.values().next().value return this.activeRecordings.values().next().value
} }
@@ -216,7 +195,6 @@ export class RecordingRelay {
return { success: false, error: errorMsg } return { success: false, error: errorMsg }
} }
// Set up promise to wait for final chunk
let timeoutId: ReturnType<typeof setTimeout> let timeoutId: ReturnType<typeof setTimeout>
const finalPromise = new Promise<StopRecordingResult>((resolve) => { const finalPromise = new Promise<StopRecordingResult>((resolve) => {
const wrappedResolve = (result: StopRecordingResult) => { const wrappedResolve = (result: StopRecordingResult) => {
@@ -224,7 +202,6 @@ export class RecordingRelay {
resolve(result) resolve(result)
} }
recording.resolveStop = wrappedResolve recording.resolveStop = wrappedResolve
// Timeout after 30 seconds
timeoutId = setTimeout(() => { timeoutId = setTimeout(() => {
if (recording.resolveStop) { if (recording.resolveStop) {
recording.resolveStop = undefined recording.resolveStop = undefined
@@ -234,8 +211,7 @@ export class RecordingRelay {
}) })
try { try {
// Tell extension to stop recording const result = await this.sendToExtension({
const result = await this.deps.sendToExtension({
method: 'stopRecording', method: 'stopRecording',
params, params,
timeout: 10000, timeout: 10000,
@@ -247,55 +223,44 @@ export class RecordingRelay {
return result return result
} }
// Wait for final chunk to arrive
return await finalPromise return await finalPromise
} catch (error: unknown) { } catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error) 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 } return { success: false, error: errorMessage }
} }
} }
/**
* Check if recording is active.
*/
async isRecording(params: IsRecordingParams): Promise<IsRecordingResult> { async isRecording(params: IsRecordingParams): Promise<IsRecordingResult> {
if (!this.deps.isExtensionConnected()) { if (!this.isExtensionConnected()) {
return { isRecording: false } return { isRecording: false }
} }
try { try {
return await this.deps.sendToExtension({ return await this.sendToExtension({
method: 'isRecording', method: 'isRecording',
params, params,
timeout: 5000, timeout: 5000,
}) as IsRecordingResult }) as IsRecordingResult
} catch (error: unknown) { } catch {
const errorMessage = error instanceof Error ? error.message : String(error)
this.deps.logger?.error('Recording status error:', error)
return { isRecording: false } return { isRecording: false }
} }
} }
/**
* Cancel recording without saving.
*/
async cancelRecording(params: CancelRecordingParams): Promise<CancelRecordingResult> { async cancelRecording(params: CancelRecordingParams): Promise<CancelRecordingResult> {
if (!this.deps.isExtensionConnected()) { if (!this.isExtensionConnected()) {
return { success: false, error: 'Extension not connected' } return { success: false, error: 'Extension not connected' }
} }
try { try {
// Note: Recording cleanup is handled by the 'recordingCancelled' event handler return await this.sendToExtension({
// which is triggered by the extension after cancellation.
return await this.deps.sendToExtension({
method: 'cancelRecording', method: 'cancelRecording',
params, params,
timeout: 5000, timeout: 5000,
}) as CancelRecordingResult }) as CancelRecordingResult
} catch (error: unknown) { } catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error) 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 } return { success: false, error: errorMessage }
} }
} }