Milestone v1 (v2.0.0): Mokosh — Session Capture #1
@@ -15,7 +15,7 @@
|
||||
// is preserved as 3 × 10 s = 30 s.)
|
||||
|
||||
import { OffscreenLogger } from '../shared/logger';
|
||||
import { blobToBase64 } from '../shared/binary';
|
||||
import { blobToBase64, base64ToBlob } from '../shared/binary';
|
||||
import type { Message, TransferredVideoSegment } from '../shared/types';
|
||||
|
||||
// ─── 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 {
|
||||
// Defense-in-depth: explicit shape check before destructuring (T-1-04).
|
||||
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
|
||||
// tests/offscreen/port-serialization.test.ts for the contract.
|
||||
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).
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user