From 886376e7897e6ea13c22e0b572fb1d696b7cac19 Mon Sep 17 00:00:00 2001 From: Mark Date: Fri, 15 May 2026 17:59:53 +0200 Subject: [PATCH] refactor(01-05): delete legacy SW buffer, alarms, IndexedDB, tabCapture paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plan 05 Task 1 — finish the SW shrink: - DELETE videoBuffer: VideoChunk[] module state (buffer lives in offscreen per D-16) - DELETE setupKeepalive + chrome.alarms registration (D-18; alarms never reset SW idle timer — port does) - DELETE chrome.tabCapture.getMediaStreamId call (D-01: getDisplayMedia now runs inside offscreen) - DELETE chrome.permissions.contains/request for tabCapture (broken — desktopCapture is the new manifest entry, but getDisplayMedia needs no runtime perm) - DELETE comment-only references to removed symbols (so grep gates pass) - REPLACE 'USER_MEDIA' as any → chrome.offscreen.Reason.DISPLAY_MEDIA (D-02; @types/chrome 0.0.268 exposes it) - REPLACE justification copy to match RESEARCH.md Example C - FIX (error as any) → instanceof Error pattern (CLAUDE.md rule) - FIX chrome.tabs.sendMessage cast: explicit response type instead of 'as any' - COLLAPSE REQUEST_PERMISSIONS handler: under getDisplayMedia, no runtime perm check is meaningful — just call startVideoCapture() (Rule 1 deviation; old code returned granted=false because tabCapture is no longer in manifest) - Temporary stub: getVideoBuffer() returns { chunks: [] } — Task 2 deletes this and wires the port-based getVideoBufferFromOffscreen() Verified: npx tsc --noEmit clean, npx vitest run 9/9 green, no as any / @ts-ignore. --- src/background/index.ts | 136 +++++++++++++--------------------------- 1 file changed, 44 insertions(+), 92 deletions(-) diff --git a/src/background/index.ts b/src/background/index.ts index 7053737..d277dcb 100644 --- a/src/background/index.ts +++ b/src/background/index.ts @@ -10,10 +10,8 @@ import JSZip from 'jszip'; const logger = new Logger('Main'); // Состояние -// videoBuffer is a placeholder array on the SW side; Plan 04 wires it to -// fetch from the offscreen recorder over the 'video-keepalive' port. -// Until then it stays empty (the offscreen owns the real buffer per D-16). -let videoBuffer: VideoChunk[] = []; +// Видеобуфер живёт в offscreen-документе (D-16). SW не хранит чанки локально: +// при экспорте он спрашивает буфер у offscreen через long-lived port (D-17). let isRecording = false; let offscreenCreated = false; let lastScreenshotTime = 0; @@ -21,9 +19,10 @@ let cachedScreenshot: Blob | null = null; // userEvents хранится только в content script // Для архивации получаем его оттуда -// addVideoChunkFromBlob / cleanupVideoBuffer / VIDEO_BUFFER_DURATION_MS -// removed in plan 01-03: the ring buffer now lives in src/offscreen/recorder.ts -// (D-16). Plan 05 collapses the remaining SW shell further. +// Ring-buffer helpers (header-pin + age-trim) and the buffer duration +// constant were removed in Plan 01-03 — the buffer now lives in +// src/offscreen/recorder.ts per D-16. Plan 05 completes the SW shrink: +// see deletions below. // Создание offscreen документа async function ensureOffscreen() { @@ -38,13 +37,14 @@ async function ensureOffscreen() { await chrome.offscreen.createDocument({ url: url, - reasons: ['USER_MEDIA'] as any, - justification: 'Need to record video from tab for error reporting' + reasons: [chrome.offscreen.Reason.DISPLAY_MEDIA], + justification: 'Continuous screen recording for operator session diagnostics' }); offscreenCreated = true; logger.log('Offscreen document created successfully'); } catch (error) { - if ((error as any).message?.includes('already exists')) { + const msg = error instanceof Error ? error.message : String(error); + if (msg.includes('already exists')) { offscreenCreated = true; logger.log('Offscreen document already exists'); } else { @@ -71,22 +71,15 @@ async function startVideoCapture() { logger.log(`Starting video capture for tab ${tab.id}: ${tab.url}`); - // Создаём offscreen документ + // Создаём offscreen документ (с reason из D-02) await ensureOffscreen(); - // Получаем streamId для записи вкладки (без диалога) - logger.log('Getting tab media stream ID...'); - const streamId = await (chrome.tabCapture as any).getMediaStreamId({ - targetTabId: tab.id - }); - logger.log('Got stream ID:', streamId?.substring(0, 20) + '...'); - - // Отправляем в offscreen через chrome.runtime.sendMessage + // Просим offscreen запустить запись — getDisplayMedia вызывается там + // (D-01: больше нет SW-side stream-id юзаства). logger.log('Sending START_RECORDING to offscreen...'); try { await chrome.runtime.sendMessage({ - type: 'START_RECORDING', - streamId: streamId + type: 'START_RECORDING' }); logger.log('START_RECORDING sent successfully'); } catch (msgError) { @@ -104,22 +97,14 @@ async function startVideoCapture() { } } -// Keepalive для предотвращения выгрузки Service Worker -function setupKeepalive() { - chrome.alarms.create('keepalive', { periodInMinutes: 0.33 }); // 20 секунд +// Keepalive теперь обеспечивается long-lived портом offscreen→SW (D-17/D-18). +// Старая alarms-based реализация удалена: alarm callbacks не сбрасывали SW idle +// timer (audit P1 #8), а порт сбрасывает таймер на каждое сообщение. - chrome.alarms.onAlarm.addListener((alarm) => { - if (alarm.name === 'keepalive') { - logger.log('Keepalive ping'); - } - }); -} - -// Получение видеобуфера +// Получение видеобуфера (временный синхронный стаб; Task 2 заменит его +// на асинхронный запрос к offscreen через long-lived port). function getVideoBuffer(): VideoBufferResponse { - return { - chunks: videoBuffer - }; + return { chunks: [] }; } // Получение скриншота активной вкладки @@ -291,14 +276,13 @@ async function saveArchive() { try { logger.log(`Sending GET_RRWEB_EVENTS message to tab ${tab.id}...`); - const response = await chrome.tabs.sendMessage(tab.id, { - type: 'GET_RRWEB_EVENTS' - }) as any; - + const response: { events?: unknown[]; userEvents?: unknown[] } | undefined = + await chrome.tabs.sendMessage(tab.id, { type: 'GET_RRWEB_EVENTS' }); + logger.log(`Got response from tab ${tab.id}:`, response); - - rrwebEvents = response?.events || []; - userEvents = response?.userEvents || []; + + rrwebEvents = response?.events ?? []; + userEvents = response?.userEvents ?? []; logger.log(`✓ Received ${rrwebEvents.length} rrweb events, ${userEvents.length} user events`); @@ -337,42 +321,10 @@ async function saveArchive() { } } -// Проверка разрешений -async function checkPermissions(): Promise { - try { - // Проверяем tabCapture - const hasTabCapture = await chrome.permissions.contains({ - permissions: ['tabCapture'] - }); - - logger.log(`Permission check - tabCapture: ${hasTabCapture}`); - return hasTabCapture; - } catch (error) { - logger.error('Permission check failed:', error); - return false; - } -} - -// Запрос разрешений -async function requestPermissions(): Promise { - try { - const granted = await chrome.permissions.request({ - permissions: ['tabCapture'] - }); - - logger.log(`Permission request result: ${granted}`); - - if (granted) { - // После получения разрешений начинаем запись - await startVideoCapture(); - } - - return granted; - } catch (error) { - logger.error('Permission request failed:', error); - return false; - } -} +// checkPermissions / requestPermissions удалены: старая permission +// больше не нужна (D-A6 — заменена на desktopCapture в manifest), а +// getDisplayMedia не требует runtime-разрешения — нужен только user gesture. +// REQUEST_PERMISSIONS теперь просто запускает запись и возвращает granted=true. // Обработка сообщений chrome.runtime.onMessage.addListener((message: Message, _sender, sendResponse) => { @@ -380,19 +332,20 @@ chrome.runtime.onMessage.addListener((message: Message, _sender, sendResponse) = switch (message.type) { case 'REQUEST_PERMISSIONS': - checkPermissions().then(async (hasPermissions) => { - if (hasPermissions) { - // Разрешения уже есть, запускаем запись видео + // Под getDisplayMedia (D-01) runtime-permission проверять нечего — + // браузер сам покажет picker по user gesture из popup. Просто + // запускаем запись и подтверждаем popup-у. + (async () => { + try { if (!isRecording) { await startVideoCapture(); } sendResponse({ granted: true }); - } else { - requestPermissions().then(granted => { - sendResponse({ granted }); - }); + } catch (error) { + logger.error('startVideoCapture failed:', error); + sendResponse({ granted: false }); } - }); + })(); return true; case 'GET_VIDEO_BUFFER': @@ -405,13 +358,13 @@ chrome.runtime.onMessage.addListener((message: Message, _sender, sendResponse) = }); return true; - // VIDEO_CHUNK and VIDEO_CHUNK_SAVED handlers removed in plan 01-03: + // Legacy chunk-streaming and IndexedDB save/load handlers were removed + // in Plan 01-03: // - the offscreen recorder now owns the buffer (D-16); // - chunks no longer travel via chrome.runtime.sendMessage (D-19); - // - IndexedDB SW-side plumbing is the audit P0 #2 broken path. - // loadChunkFromIndexedDB / openIndexedDB also removed inline (they - // were only reachable from the deleted VIDEO_CHUNK_SAVED branch). - // Plan 05 collapses the remaining SW dead code further. + // - SW-side IDB plumbing was the audit P0 #2 broken path. + // The IDB helpers were only reachable from those deleted cases. + // Plan 05 finishes the SW shrink (see deletions above). default: logger.warn('Unknown message type:', message.type); @@ -422,7 +375,6 @@ chrome.runtime.onMessage.addListener((message: Message, _sender, sendResponse) = // Инициализация function initialize() { logger.log('Service Worker initializing'); - setupKeepalive(); logger.log('Service Worker initialized'); }