Update TASK-20.2 status to done

This commit is contained in:
2026-02-12 02:49:54 -08:00
parent dfb54630df
commit f345547963
11 changed files with 427 additions and 7 deletions

View File

@@ -69,6 +69,7 @@ export {
DEFAULT_MPV_SUBTITLE_RENDER_METRICS,
sanitizeMpvSubtitleRenderMetrics,
} from "./mpv-render-metrics-service";
export { createOverlayContentMeasurementStoreService } from "./overlay-content-measurement-service";
export { handleMpvCommandFromIpcService } from "./ipc-command-service";
export { createFieldGroupingOverlayRuntimeService } from "./field-grouping-overlay-service";
export { createNumericShortcutRuntimeService } from "./numeric-shortcut-service";

View File

@@ -28,6 +28,7 @@ export interface IpcServiceDeps {
getRuntimeOptions: () => unknown;
setRuntimeOption: (id: string, value: unknown) => unknown;
cycleRuntimeOption: (id: string, direction: 1 | -1) => unknown;
reportOverlayContentBounds: (payload: unknown) => void;
}
interface WindowLike {
@@ -75,6 +76,7 @@ export interface IpcDepsRuntimeOptions {
getRuntimeOptions: () => unknown;
setRuntimeOption: (id: string, value: unknown) => unknown;
cycleRuntimeOption: (id: string, direction: 1 | -1) => unknown;
reportOverlayContentBounds: (payload: unknown) => void;
}
export function createIpcDepsRuntimeService(
@@ -126,6 +128,7 @@ export function createIpcDepsRuntimeService(
getRuntimeOptions: options.getRuntimeOptions,
setRuntimeOption: options.setRuntimeOption,
cycleRuntimeOption: options.cycleRuntimeOption,
reportOverlayContentBounds: options.reportOverlayContentBounds,
};
}
@@ -253,4 +256,8 @@ export function registerIpcHandlersService(deps: IpcServiceDeps): void {
ipcMain.handle("runtime-options:cycle", (_event, id: string, direction: 1 | -1) => {
return deps.cycleRuntimeOption(id, direction);
});
ipcMain.on("overlay-content-bounds:report", (_event: IpcMainEvent, payload: unknown) => {
deps.reportOverlayContentBounds(payload);
});
}

View File

@@ -0,0 +1,87 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
createOverlayContentMeasurementStoreService,
sanitizeOverlayContentMeasurement,
} from "./overlay-content-measurement-service";
test("sanitizeOverlayContentMeasurement accepts valid payload with null rect", () => {
const measurement = sanitizeOverlayContentMeasurement(
{
layer: "visible",
measuredAtMs: 100,
viewport: { width: 1920, height: 1080 },
contentRect: null,
},
500,
);
assert.deepEqual(measurement, {
layer: "visible",
measuredAtMs: 100,
viewport: { width: 1920, height: 1080 },
contentRect: null,
});
});
test("sanitizeOverlayContentMeasurement rejects invalid ranges", () => {
const measurement = sanitizeOverlayContentMeasurement(
{
layer: "invisible",
measuredAtMs: 100,
viewport: { width: 0, height: 1080 },
contentRect: { x: 0, y: 0, width: 100, height: 20 },
},
500,
);
assert.equal(measurement, null);
});
test("overlay measurement store keeps latest payload per layer", () => {
const store = createOverlayContentMeasurementStoreService({
now: () => 1000,
warn: () => {
// noop
},
});
const visible = store.report({
layer: "visible",
measuredAtMs: 900,
viewport: { width: 1280, height: 720 },
contentRect: { x: 50, y: 60, width: 400, height: 80 },
});
const invisible = store.report({
layer: "invisible",
measuredAtMs: 910,
viewport: { width: 1280, height: 720 },
contentRect: { x: 20, y: 30, width: 300, height: 40 },
});
assert.equal(visible?.layer, "visible");
assert.equal(invisible?.layer, "invisible");
assert.equal(store.getLatestByLayer("visible")?.contentRect?.width, 400);
assert.equal(store.getLatestByLayer("invisible")?.contentRect?.height, 40);
});
test("overlay measurement store rate-limits invalid payload warnings", () => {
let now = 1_000;
const warnings: string[] = [];
const store = createOverlayContentMeasurementStoreService({
now: () => now,
warn: (message) => {
warnings.push(message);
},
});
store.report({ layer: "visible" });
store.report({ layer: "visible" });
assert.equal(warnings.length, 0);
now = 11_000;
store.report({ layer: "visible" });
assert.equal(warnings.length, 1);
assert.match(warnings[0], /Dropped 3 invalid measurement payload/);
});

View File

@@ -0,0 +1,149 @@
import { OverlayContentMeasurement, OverlayContentRect, OverlayLayer } from "../../types";
const MAX_VIEWPORT = 10000;
const MAX_RECT_DIMENSION = 10000;
const MAX_RECT_OFFSET = 50000;
const MAX_FUTURE_TIMESTAMP_MS = 60_000;
const INVALID_LOG_THROTTLE_MS = 10_000;
type OverlayMeasurementStore = Record<OverlayLayer, OverlayContentMeasurement | null>;
export function sanitizeOverlayContentMeasurement(
payload: unknown,
nowMs: number,
): OverlayContentMeasurement | null {
if (!payload || typeof payload !== "object") return null;
const candidate = payload as {
layer?: unknown;
measuredAtMs?: unknown;
viewport?: { width?: unknown; height?: unknown };
contentRect?: { x?: unknown; y?: unknown; width?: unknown; height?: unknown } | null;
};
if (candidate.layer !== "visible" && candidate.layer !== "invisible") {
return null;
}
const viewportWidth = readFiniteInRange(candidate.viewport?.width, 1, MAX_VIEWPORT);
const viewportHeight = readFiniteInRange(candidate.viewport?.height, 1, MAX_VIEWPORT);
if (!Number.isFinite(viewportWidth) || !Number.isFinite(viewportHeight)) {
return null;
}
const measuredAtMs = readFiniteInRange(
candidate.measuredAtMs,
1,
nowMs + MAX_FUTURE_TIMESTAMP_MS,
);
if (!Number.isFinite(measuredAtMs)) {
return null;
}
const contentRect = sanitizeOverlayContentRect(candidate.contentRect);
if (candidate.contentRect !== null && !contentRect) {
return null;
}
return {
layer: candidate.layer,
measuredAtMs,
viewport: { width: viewportWidth, height: viewportHeight },
contentRect,
};
}
function sanitizeOverlayContentRect(
rect: unknown,
): OverlayContentRect | null {
if (rect === null || rect === undefined) {
return null;
}
if (!rect || typeof rect !== "object") {
return null;
}
const candidate = rect as {
x?: unknown;
y?: unknown;
width?: unknown;
height?: unknown;
};
const width = readFiniteInRange(candidate.width, 0, MAX_RECT_DIMENSION);
const height = readFiniteInRange(candidate.height, 0, MAX_RECT_DIMENSION);
const x = readFiniteInRange(candidate.x, -MAX_RECT_OFFSET, MAX_RECT_OFFSET);
const y = readFiniteInRange(candidate.y, -MAX_RECT_OFFSET, MAX_RECT_OFFSET);
if (
!Number.isFinite(width) ||
!Number.isFinite(height) ||
!Number.isFinite(x) ||
!Number.isFinite(y)
) {
return null;
}
return { x, y, width, height };
}
function readFiniteInRange(
value: unknown,
min: number,
max: number,
): number {
if (typeof value !== "number" || !Number.isFinite(value)) {
return Number.NaN;
}
if (value < min || value > max) {
return Number.NaN;
}
return value;
}
export function createOverlayContentMeasurementStoreService(options?: {
now?: () => number;
warn?: (message: string) => void;
}) {
const now = options?.now ?? (() => Date.now());
const warn = options?.warn ?? ((message: string) => console.warn(message));
const latestByLayer: OverlayMeasurementStore = {
visible: null,
invisible: null,
};
let droppedInvalid = 0;
let lastInvalidLogAtMs = 0;
function report(payload: unknown): OverlayContentMeasurement | null {
const nowMs = now();
const measurement = sanitizeOverlayContentMeasurement(payload, nowMs);
if (!measurement) {
droppedInvalid += 1;
if (
droppedInvalid > 0 &&
nowMs - lastInvalidLogAtMs >= INVALID_LOG_THROTTLE_MS
) {
warn(
`[overlay-content-bounds] Dropped ${droppedInvalid} invalid measurement payload(s) in the last ${INVALID_LOG_THROTTLE_MS}ms.`,
);
droppedInvalid = 0;
lastInvalidLogAtMs = nowMs;
}
return null;
}
latestByLayer[measurement.layer] = measurement;
return measurement;
}
function getLatestByLayer(layer: OverlayLayer): OverlayContentMeasurement | null {
return latestByLayer[layer];
}
return {
getLatestByLayer,
report,
};
}