Files
SubMiner/src/core/services/overlay-content-measurement-service.ts

150 lines
3.9 KiB
TypeScript

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,
};
}