Milestone v1 (v2.0.0): Mokosh — Session Capture #1
@@ -14,6 +14,8 @@ const VIDEO_MIME = 'video/webm;codecs=vp9'; // D-20 strict — no fallbac
|
|||||||
const VIDEO_BITRATE = 400_000; // CON-video-codec
|
const VIDEO_BITRATE = 400_000; // CON-video-codec
|
||||||
const TIMESLICE_MS = 2_000; // SPEC §4.1
|
const TIMESLICE_MS = 2_000; // SPEC §4.1
|
||||||
const PORT_NAME = 'video-keepalive'; // Plan 04 owns the ping loop
|
const PORT_NAME = 'video-keepalive'; // Plan 04 owns the ping loop
|
||||||
|
const PORT_PING_MS = 25_000; // < 30 s SW idle threshold
|
||||||
|
const PORT_RECONNECT_MS = 290_000; // pre-empt the ~5 min port cap (Pitfall 4)
|
||||||
|
|
||||||
const logger = new OffscreenLogger('Recorder');
|
const logger = new OffscreenLogger('Recorder');
|
||||||
|
|
||||||
@@ -137,20 +139,79 @@ function stopRecording(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Bootstrap (Plan 04 wires the full port + handshake) ────────────────
|
// ─── Bootstrap: handshake + port lifecycle (D-17, RESEARCH.md Patterns 4 & 5) ──
|
||||||
// Plan 03 commits this minimal bootstrap so that Plan 02's ring-buffer +
|
// T-1-04 sender-id check: defense-in-depth on the offscreen side. The SW-side
|
||||||
// codec tests can mock `chrome.runtime` without crashing on module import.
|
// `onConnect` listener (Plan 05) MUST also validate
|
||||||
// Plan 04 elaborates the OFFSCREEN_READY handshake and the port reconnect
|
// `port.sender?.id === chrome.runtime.id` to reject port-hijack attempts
|
||||||
// loop without rewriting this module's exports.
|
// from other extensions. See threat register T-1-04 in 01-04-PLAN.md.
|
||||||
|
|
||||||
function isFromOwnExtension(sender: chrome.runtime.MessageSender | undefined): boolean {
|
function isFromOwnExtension(sender: chrome.runtime.MessageSender | undefined): boolean {
|
||||||
return sender?.id === chrome.runtime.id;
|
return sender?.id === chrome.runtime.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Бутстрап (Plan 03 stub; Plan 04 wires the full handshake + reconnect).
|
// Stable handles for the ping interval and the pre-emptive reconnect timer,
|
||||||
|
// so we can clear them on disconnect / re-init.
|
||||||
|
let pingIntervalId: ReturnType<typeof setInterval> | null = null;
|
||||||
|
let preemptiveReconnectId: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
function teardownPortTimers(): void {
|
||||||
|
if (pingIntervalId !== null) {
|
||||||
|
clearInterval(pingIntervalId);
|
||||||
|
pingIntervalId = null;
|
||||||
|
}
|
||||||
|
if (preemptiveReconnectId !== null) {
|
||||||
|
clearTimeout(preemptiveReconnectId);
|
||||||
|
preemptiveReconnectId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPortMessage(message: unknown): void {
|
||||||
|
// Defense-in-depth: explicit shape check before destructuring (T-1-04)
|
||||||
|
if (typeof message !== 'object' || message === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const type = (message as { type?: unknown }).type;
|
||||||
|
if (type === 'REQUEST_BUFFER') {
|
||||||
|
if (keepalivePort !== null) {
|
||||||
|
keepalivePort.postMessage({ type: 'BUFFER', chunks: getBuffer() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Any unknown port message type is silently dropped (T-1-04 defense-in-depth).
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectPort(): void {
|
||||||
|
teardownPortTimers();
|
||||||
|
try {
|
||||||
|
keepalivePort = chrome.runtime.connect({ name: PORT_NAME });
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('port connect failed:', err);
|
||||||
|
keepalivePort = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
keepalivePort.onMessage.addListener(onPortMessage);
|
||||||
|
keepalivePort.onDisconnect.addListener(() => {
|
||||||
|
logger.warn('port disconnected — reconnecting');
|
||||||
|
teardownPortTimers();
|
||||||
|
keepalivePort = null;
|
||||||
|
// Synchronous reconnect — tests/offscreen/port.test.ts pins this
|
||||||
|
connectPort();
|
||||||
|
});
|
||||||
|
pingIntervalId = setInterval(() => {
|
||||||
|
keepalivePort?.postMessage({ type: 'PING' });
|
||||||
|
}, PORT_PING_MS);
|
||||||
|
preemptiveReconnectId = setTimeout(() => {
|
||||||
|
logger.log('pre-emptive port reconnect (290 s cap)');
|
||||||
|
keepalivePort?.disconnect();
|
||||||
|
// onDisconnect handler above triggers a fresh connectPort() call
|
||||||
|
}, PORT_RECONNECT_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Бутстрап (handshake + port lifecycle).
|
||||||
// Guarded so the pure ring-buffer + codec-check tests can import the module
|
// Guarded so the pure ring-buffer + codec-check tests can import the module
|
||||||
// without providing a chrome stub. Production runs always have chrome.runtime
|
// without providing a full chrome stub. Production always has chrome.runtime
|
||||||
// available, so the guard is a no-op there.
|
// fully populated, so the guard is a no-op there. Order matters per Pattern 4:
|
||||||
|
// onMessage listener registration MUST happen BEFORE OFFSCREEN_READY is sent,
|
||||||
|
// so the SW can safely send START_RECORDING the moment it sees the ready signal.
|
||||||
function bootstrap(): void {
|
function bootstrap(): void {
|
||||||
if (typeof chrome === 'undefined' || !chrome.runtime) {
|
if (typeof chrome === 'undefined' || !chrome.runtime) {
|
||||||
return;
|
return;
|
||||||
@@ -175,9 +236,8 @@ function bootstrap(): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Plan 04 will replace this stub with the full reconnect + ping loop.
|
|
||||||
if (typeof chrome.runtime.connect === 'function') {
|
if (typeof chrome.runtime.connect === 'function') {
|
||||||
keepalivePort = chrome.runtime.connect({ name: PORT_NAME });
|
connectPort();
|
||||||
}
|
}
|
||||||
if (typeof chrome.runtime.sendMessage === 'function') {
|
if (typeof chrome.runtime.sendMessage === 'function') {
|
||||||
chrome.runtime.sendMessage({ type: 'OFFSCREEN_READY' });
|
chrome.runtime.sendMessage({ type: 'OFFSCREEN_READY' });
|
||||||
@@ -186,10 +246,6 @@ function bootstrap(): void {
|
|||||||
|
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
|
||||||
// Touch the keepalive var so noUnusedLocals doesn't complain — Plan 04
|
|
||||||
// uses it. Once Plan 04 lands, this line is removed in its refactor pass.
|
|
||||||
void keepalivePort;
|
|
||||||
|
|
||||||
// ─── D-13 fallback: restart-segments skeleton (pre-staged per CONTEXT.md) ──
|
// ─── D-13 fallback: restart-segments skeleton (pre-staged per CONTEXT.md) ──
|
||||||
// Activated only if the Phase 07 ffprobe gate fails on the simpler
|
// Activated only if the Phase 07 ffprobe gate fails on the simpler
|
||||||
// continuous-recorder + age-trim approach. See RESEARCH.md Pattern 3.
|
// continuous-recorder + age-trim approach. See RESEARCH.md Pattern 3.
|
||||||
|
|||||||
Reference in New Issue
Block a user