Milestone v1 (v2.0.0): Mokosh — Session Capture #1

Merged
strategy155 merged 297 commits from gsd/phase-04-harden-clean-up-optional into main 2026-05-31 15:34:17 +00:00
Showing only changes of commit f0b95f4a83 - Show all commits

View File

@@ -15,7 +15,7 @@
// is preserved as 3 × 10 s = 30 s.) // is preserved as 3 × 10 s = 30 s.)
import { OffscreenLogger } from '../shared/logger'; import { OffscreenLogger } from '../shared/logger';
import { blobToBase64 } from '../shared/binary'; import { blobToBase64, base64ToBlob } from '../shared/binary';
import type { Message, TransferredVideoSegment } from '../shared/types'; import type { Message, TransferredVideoSegment } from '../shared/types';
// ─── Plan 01-11: gated test-hook dynamic import ─────────────────────── // ─── Plan 01-11: gated test-hook dynamic import ───────────────────────
@@ -611,6 +611,65 @@ function teardownPortTimers(): void {
} }
} }
// D-P2-01 Blob URL hygiene set: track minted URLs so we can warn (not
// error) on unexpected revoke ops. URL.revokeObjectURL on an unknown URL
// is a no-op per the WHATWG URL spec, so this is purely diagnostic. The
// Set lives at module scope because URL minting is stateless per-Blob
// and an instance-per-mint container would itself be the only consumer
// (the postMessage round-trip happens via requestId, not Set membership).
const mintedDownloadUrls = new Set<string>();
/**
* D-P2-01 (P0-6 fix) CREATE_DOWNLOAD_URL handler: decode the SW-supplied
* base64 archive bytes into a Blob, mint a `blob:chrome-extension://...`
* URL via URL.createObjectURL, and post the URL string back to the SW
* via the keepalivePort as a DOWNLOAD_URL message.
*
* Defense-in-depth: on any failure (empty base64, decode throw, mint
* throw, port disconnected mid-flight) the function responds with
* `{type:'DOWNLOAD_URL', requestId, url:''}` and lets the SW's outer
* timeout fire. Mirrors the encodeAndSendBuffer best-effort pattern —
* partial recovery > silent stall.
*
* @param requestId - Per-mint correlation id (echoed in the DOWNLOAD_URL
* response). The SW's pendingDownloadUrlResolvers Map
* is keyed by this id.
* @param dataBase64 - Base64-encoded archive bytes (no `data:` prefix).
* @param mimeType - MIME type to assign to the reconstructed Blob.
* Defaults to 'application/zip' at the call site.
*/
async function handleCreateDownloadUrl(
requestId: string,
dataBase64: string,
mimeType: string,
): Promise<void> {
if (keepalivePort === null) {
logger.warn('CREATE_DOWNLOAD_URL: port unavailable at handler entry — dropping');
return;
}
let url = '';
try {
const blob = base64ToBlob(dataBase64, mimeType);
if (blob.size === 0) {
logger.warn('CREATE_DOWNLOAD_URL: empty blob from base64 decode — responding with empty url');
} else {
url = URL.createObjectURL(blob);
mintedDownloadUrls.add(url);
logger.log(`Minted Blob URL: ${url.substring(0, 30)}... (size: ${blob.size} bytes)`);
}
} catch (err) {
logger.error('CREATE_DOWNLOAD_URL: base64ToBlob or createObjectURL threw:', err);
}
// Always respond, even on empty url — the SW's downloadArchive checks
// `url === ''` and throws a typed `blob-url-mint-failed` error, which
// routes through saveArchive's catch into the RECORDING_ERROR channel.
try {
keepalivePort.postMessage({ type: 'DOWNLOAD_URL', requestId, url });
} catch (err) {
logger.warn('DOWNLOAD_URL post failed (port may have disconnected):', err);
}
}
function onPortMessage(message: unknown): void { function onPortMessage(message: unknown): void {
// Defense-in-depth: explicit shape check before destructuring (T-1-04). // Defense-in-depth: explicit shape check before destructuring (T-1-04).
if (typeof message !== 'object' || message === null) { if (typeof message !== 'object' || message === null) {
@@ -646,6 +705,52 @@ function onPortMessage(message: unknown): void {
// and JSON.stringify(blob) === "{}". See src/shared/binary.ts and // and JSON.stringify(blob) === "{}". See src/shared/binary.ts and
// tests/offscreen/port-serialization.test.ts for the contract. // tests/offscreen/port-serialization.test.ts for the contract.
void encodeAndSendBuffer(requestId); void encodeAndSendBuffer(requestId);
return;
}
if (type === 'CREATE_DOWNLOAD_URL') {
// D-P2-01 (P0-6 fix): mint a Blob URL on behalf of the SW (which
// lacks URL.createObjectURL per DEC-006). Each mint gets its own
// requestId; concurrent mints are allowed because URL minting is
// stateless per-Blob — unlike encodeAndSendBuffer, which gates
// concurrent calls because they share the segment ring buffer state.
const requestId = (message as { requestId?: unknown }).requestId;
const dataBase64 = (message as { dataBase64?: unknown }).dataBase64;
const mimeType = (message as { mimeType?: unknown }).mimeType;
if (typeof requestId !== 'string' || requestId.length === 0) {
logger.warn('CREATE_DOWNLOAD_URL without requestId — dropping');
return;
}
if (typeof dataBase64 !== 'string') {
logger.warn('CREATE_DOWNLOAD_URL with non-string dataBase64 — dropping');
return;
}
const effectiveMime = typeof mimeType === 'string' ? mimeType : 'application/zip';
// Fire-and-forget IIFE so the listener stays sync-typed; the helper
// owns its own error reporting + DOWNLOAD_URL response post.
void handleCreateDownloadUrl(requestId, dataBase64, effectiveMime);
return;
}
if (type === 'REVOKE_DOWNLOAD_URL') {
// D-P2-01: free the URL once chrome.downloads.onChanged reports the
// download is complete or interrupted. URL.revokeObjectURL on an
// unknown URL is a no-op per WHATWG spec, so the mintedDownloadUrls
// Set is purely a diagnostic signal (unexpected revokes get a warn
// but still execute).
const url = (message as { url?: unknown }).url;
if (typeof url !== 'string' || url.length === 0) {
logger.warn('REVOKE_DOWNLOAD_URL without url — dropping');
return;
}
if (!mintedDownloadUrls.has(url)) {
logger.warn(`REVOKE_DOWNLOAD_URL for unminted url ${url.substring(0, 30)}... — proceeding anyway (WHATWG no-op)`);
}
try {
URL.revokeObjectURL(url);
mintedDownloadUrls.delete(url);
} catch (err) {
logger.warn('URL.revokeObjectURL threw:', err);
}
return;
} }
// Any unknown port message type is silently dropped (T-1-04 defense-in-depth). // Any unknown port message type is silently dropped (T-1-04 defense-in-depth).
} }