- src/shared/types.ts SessionMetadata: REPLACE `url: string` with
`urls: string[]`; ADD `schemaVersion: string` as the first field.
Total 8 fields. Field-emission order follows source-declaration order
(TypeScript object-literal insertion order; JSON.stringify emits in
insertion order per ECMA-262). Docstring cites D-P2-02 + D-P2-03 +
Plan 02-01 Task 3 planner-resolved 8th field decision + F2 empty-array
permission.
- src/background/index.ts:
* Import { initTabUrlTracker, snapshotOpenTabs, getTabUrlsSeen } from
'./tab-url-tracker'.
* Register initTabUrlTracker() at module top-level alongside
chrome.downloads.onChanged (Plan 02-02 precedent for D-P2-* feature
registration). Defensive try/catch matches the surrounding chrome.*
listener pattern; tracker module has its own initialized flag for
idempotency.
* createArchive: snapshotOpenTabs() before reading getTabUrlsSeen()
(DEC-011 Amendment 1 capability — captures tabs opened but never
activated). Empty urls[] emitted faithfully per F2 (no fake
extension-origin sentinel; logger.warn for diagnostic visibility on
whole-desktop-no-tab sessions).
* metadata literal: schemaVersion: '2' first, urls (not url), 8 fields
total. ECMA-262 insertion-order guarantee + JSON.stringify deliver
the canonical wire shape.
- Always-on charter preserved: createArchive does NOT call
clearTabUrlsSeen() — tracker continues accumulating across saves
(Plan 01-09 Amendment 3 invariant).
Verification:
- npx tsc --noEmit → clean.
- npm run build → clean (dist/assets/index.ts-8LkXuqac.js 378.82 kB,
~+2 kB vs pre-Task-2 baseline for the new tab-url-tracker module).
- npx vitest run → 171/171 GREEN (was 163 GREEN / 8 RED; +8 GREEN net).
- Tier-1 grep gate: 13/13 GREEN unchanged.
Closes 8 RED tests:
- tests/background/meta-json-urls-schema.test.ts Tests 1+2 (Tests 3+4+5
flipped in Task 1).
- tests/build/strict-meta-json-validation.test.ts Tests 1+3+8 (Tests 2,
4, 5, 6, 7 remain GREEN regression guards).
191 lines
8.3 KiB
TypeScript
191 lines
8.3 KiB
TypeScript
// Типы для обмена сообщениями между компонентами
|
|
|
|
export type MessageType =
|
|
| 'REQUEST_PERMISSIONS'
|
|
| 'PERMISSIONS_GRANTED'
|
|
| 'PERMISSIONS_DENIED'
|
|
| 'GET_VIDEO_BUFFER'
|
|
| 'VIDEO_BUFFER_RESPONSE'
|
|
| 'GET_RRWEB_EVENTS'
|
|
| 'RRWEB_EVENTS_RESPONSE'
|
|
| 'SAVE_ARCHIVE'
|
|
| 'ARCHIVE_SAVED'
|
|
| 'START_RECORDING'
|
|
| 'STOP_RECORDING'
|
|
| 'RECORDING_ERROR'
|
|
| 'OFFSCREEN_READY';
|
|
|
|
// IN-05 fix: Message<T = unknown> instead of `T = any`. Call sites that
|
|
// access `(msg as Message).data` MUST narrow before use — `unknown`
|
|
// forces that discipline, which `any` silently bypassed (every
|
|
// downstream access was implicitly `any` and skipped type checking).
|
|
// The Message inbox in src/background/index.ts and src/offscreen/recorder.ts
|
|
// already destructure via `msg.type` switch and never read `.data`
|
|
// directly, so the migration was safe (no widening needed).
|
|
export interface Message<T = unknown> {
|
|
type: MessageType;
|
|
data?: T;
|
|
tabId?: number;
|
|
}
|
|
|
|
// Типы сообщений в long-lived port (offscreen ↔ SW; D-17 / Plan 04 / D-13)
|
|
//
|
|
// Option C (debug session empty-archive-port-race) extends the surface:
|
|
// - 'PONG' completes the health-probe loop: offscreen pings, SW echoes,
|
|
// offscreen tracks last-pong time. Replaces the 290 s pre-emptive
|
|
// `setTimeout` reconnect (whose race window weaponised the upstream
|
|
// silent-skip in createArchive — see the bisect notes in the debug
|
|
// session for full lineage).
|
|
// - REQUEST_BUFFER + BUFFER carry an architectural `requestId` so the
|
|
// SW can match a response to the in-flight request that issued it.
|
|
// This retires the silent-cross-talk failure mode where a stale
|
|
// BUFFER from a prior request would route into a newer Promise.
|
|
//
|
|
// D-P2-01 Blob URL migration triplet (P0-6 fix, Phase 02 Plan 02-02):
|
|
// - CREATE_DOWNLOAD_URL + DOWNLOAD_URL + REVOKE_DOWNLOAD_URL.
|
|
// SW posts CREATE_DOWNLOAD_URL with the archive bytes as base64;
|
|
// offscreen mints URL.createObjectURL and responds with DOWNLOAD_URL;
|
|
// SW calls chrome.downloads.download(url); when chrome.downloads
|
|
// .onChanged reports 'complete' or 'interrupted', SW posts
|
|
// REVOKE_DOWNLOAD_URL so offscreen can free the URL. The base64
|
|
// wire-format reuses the D-12 precedent from src/shared/binary.ts —
|
|
// chrome.runtime.Port JSON-serializes payloads, so Blob → empty
|
|
// object; base64 round-trips cleanly. See
|
|
// .planning/phases/02-stabilize-export-pipeline/02-CONTEXT.md
|
|
// D-P2-01 for the full architectural rationale.
|
|
export type PortMessageType =
|
|
| 'PING'
|
|
| 'PONG'
|
|
| 'REQUEST_BUFFER'
|
|
| 'BUFFER'
|
|
| 'CREATE_DOWNLOAD_URL' // SW → offscreen: "here is a Blob as base64; mint a URL for it"
|
|
| 'DOWNLOAD_URL' // offscreen → SW: "here is the minted blob:URL"
|
|
| 'REVOKE_DOWNLOAD_URL'; // SW → offscreen: "you can revoke this URL now"
|
|
|
|
export interface PortMessage {
|
|
type: PortMessageType;
|
|
// Per-request correlation id (Option C). The SW generates a uuid for
|
|
// each REQUEST_BUFFER call and only resolves on BUFFER responses that
|
|
// echo the same id. PING/PONG do not carry a requestId — they are
|
|
// pure liveness signals.
|
|
// Also used by the D-P2-01 triplet: CREATE_DOWNLOAD_URL → DOWNLOAD_URL
|
|
// pair carries a per-mint requestId so concurrent mints (theoretically
|
|
// possible across two SAVE flows) cannot cross-talk. REVOKE_DOWNLOAD_URL
|
|
// is fire-and-forget — no requestId required.
|
|
requestId?: string;
|
|
// Wire-format (D-12 base64 transfer + D-13 segment lifecycle):
|
|
// segments travel as TransferredVideoSegment[] because
|
|
// chrome.runtime.Port JSON-serializes payloads across extension
|
|
// contexts and JSON.stringify(blob) === "{}" loses binary content.
|
|
// Each entry is one self-contained ~10 s WebM segment (EBML header
|
|
// + seed keyframe). The receive side reconstructs VideoSegment[]
|
|
// via src/shared/binary.ts.
|
|
segments?: TransferredVideoSegment[];
|
|
// D-P2-01 (P0-6 fix): archive bytes as base64 + MIME type for Blob
|
|
// reconstruction in the offscreen document on CREATE_DOWNLOAD_URL.
|
|
// Reuses src/shared/binary.ts blobToBase64/base64ToBlob helpers; same
|
|
// wire-format precedent as the BUFFER segment transfer.
|
|
dataBase64?: string;
|
|
mimeType?: string;
|
|
// D-P2-01: the minted blob:URL string on DOWNLOAD_URL (offscreen → SW)
|
|
// OR the URL to free on REVOKE_DOWNLOAD_URL (SW → offscreen). The
|
|
// string is a `blob:chrome-extension://<id>/<uuid>` URL minted by
|
|
// URL.createObjectURL in the offscreen document origin.
|
|
url?: string;
|
|
}
|
|
|
|
// In-memory segment shape used by mergeVideoSegments after the SW
|
|
// decodes the wire format. Под D-13 каждый сегмент — самодостаточный
|
|
// WebM-блок ≈ 10 секунд (свой EBML-заголовок и стартовый keyframe).
|
|
export interface VideoSegment {
|
|
data: Blob;
|
|
timestamp: number;
|
|
}
|
|
|
|
// Wire-format for video segments traveling across the offscreen↔SW
|
|
// chrome.runtime.Port boundary. Replaces the previous Blob payload,
|
|
// which failed because Blob is not JSON-serializable.
|
|
// See debug session d12-blob-port-transfer-fails (resolved) and the
|
|
// GREEN block of tests/offscreen/port-serialization.test.ts for the
|
|
// pinned shape. The header-pin flag from the D-09..D-11 era is gone
|
|
// — under D-13 every segment IS implicitly its own header.
|
|
export interface TransferredVideoSegment {
|
|
data: string; // base64-encoded blob bytes (no data: prefix)
|
|
type: string; // MIME type to apply when reconstructing the Blob
|
|
timestamp: number;
|
|
}
|
|
|
|
// Лог событий пользователя
|
|
// IN-05-companion: `meta` uses `unknown` values instead of `any`. The
|
|
// field carries free-form diagnostic data (HTTP status, stack frames,
|
|
// XHR method, etc.); none of the SW consumers READ from meta — it
|
|
// only flows from content script → archive JSON. `unknown` documents
|
|
// "treat me opaquely" honestly; `any` silently allowed any access.
|
|
export interface UserEvent {
|
|
timestamp: number;
|
|
type: 'click' | 'input' | 'navigation' | 'js_error' | 'network_error';
|
|
target: string;
|
|
value?: string;
|
|
url: string;
|
|
meta?: Record<string, unknown>;
|
|
}
|
|
|
|
// Метаданные сессии.
|
|
//
|
|
// Phase 2 Plan 02-03 — D-P2-02 + D-P2-03 schema-breaking amendment
|
|
// (2026-05-20; closes audit P1 #10):
|
|
//
|
|
// - `url: string` REPLACED by `urls: string[]`. Captures the operator's
|
|
// multi-tab context during the rolling 30 s recording window — not
|
|
// just the active-at-save tab. Fed by the new
|
|
// `src/background/tab-url-tracker.ts` module via chrome.tabs.onActivated
|
|
// + chrome.tabs.onUpdated listeners + a SAVE-time
|
|
// chrome.tabs.query({}) snapshot (DEC-011 Amendment 1 grants the
|
|
// `tabs` permission). Empty array IS permitted (F2 — whole-desktop-
|
|
// no-tab session).
|
|
//
|
|
// - `schemaVersion: string` ADDED as the 8th field. Value '2' marks the
|
|
// D-P2-02 url→urls cutover; future schema bumps increment. The 8th
|
|
// field name was planner-suggested in Plan 02-01 Task 3 and ratified
|
|
// here as the lockstep for tests/build/strict-meta-json-validation.test.ts
|
|
// EXPECTED_KEYS.
|
|
//
|
|
// Total field count: 8 (per D-P2-03 strict exact-shape rule).
|
|
//
|
|
// Field-emission order follows source-declaration order: TypeScript object
|
|
// literals preserve insertion order, and JSON.stringify emits in insertion
|
|
// order per ECMA-262 §9.1.11.1 — so the meta.json output mirrors this
|
|
// interface line-by-line.
|
|
export interface SessionMetadata {
|
|
schemaVersion: string;
|
|
timestamp: string;
|
|
urls: string[];
|
|
userAgent: string;
|
|
extensionVersion: string;
|
|
videoBufferSeconds: number;
|
|
logDurationMinutes: number;
|
|
totalEvents: number;
|
|
}
|
|
|
|
// Сообщения для popup
|
|
export interface PopupState {
|
|
isRecording: boolean;
|
|
hasPermissions: boolean;
|
|
status: 'idle' | 'saving' | 'done';
|
|
}
|
|
|
|
// Ответы от Service Worker
|
|
export interface VideoBufferResponse {
|
|
segments: VideoSegment[];
|
|
}
|
|
|
|
// IN-05-companion: `events: unknown[]` instead of `any[]`. The events
|
|
// array carries rrweb's `EventWithTime` shape which is too large to
|
|
// import here without pulling rrweb into the shared module; the
|
|
// consumers (background/index.ts) already type it as `unknown[]` and
|
|
// pass through to JSZip. `any[]` would have silently bypassed all
|
|
// downstream type-checking.
|
|
export interface RrwebEventsResponse {
|
|
events: unknown[];
|
|
}
|