mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-18 21:22:28 +03:00
feat(video): add isolated drill-down cache
This commit is contained in:
140
src/app/api/modality-bridge/video/drilldown/route.ts
Normal file
140
src/app/api/modality-bridge/video/drilldown/route.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { createErrorResponse } from "@/lib/api/errorResponse";
|
||||
import {
|
||||
VIDEO_BRIDGE_BROKER_PATH,
|
||||
isVideoBridgeBrokerInternalRequest,
|
||||
} from "@/lib/guardrails/videoBridgeBrokerAuth";
|
||||
import {
|
||||
VideoDrilldownCache,
|
||||
type VideoDrilldownFrame,
|
||||
} from "@/lib/guardrails/videoBridgeDrilldown";
|
||||
import { resolveModelSyncInternalBaseUrl } from "@/shared/services/modelSyncScheduler";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
export const revalidate = 0;
|
||||
|
||||
export const VIDEO_BRIDGE_DRILLDOWN_PATH = "/api/modality-bridge/video/drilldown";
|
||||
const MAX_BODY_BYTES = 34 * 1024 * 1024;
|
||||
const drilldownCache = new VideoDrilldownCache({
|
||||
maxEntries: 64,
|
||||
ttlMs: 10 * 60 * 1000,
|
||||
});
|
||||
|
||||
function expectedPath(): string {
|
||||
const basePath = new URL(resolveModelSyncInternalBaseUrl()).pathname.replace(/\/$/, "");
|
||||
return `${basePath}${VIDEO_BRIDGE_DRILLDOWN_PATH}`;
|
||||
}
|
||||
|
||||
function invalid(message: string, status = 400): Response {
|
||||
return createErrorResponse({ status, message, type: "invalid_request" });
|
||||
}
|
||||
|
||||
function parseQuery(url: URL): {
|
||||
endSeconds?: number;
|
||||
frameCount?: number;
|
||||
sessionId: string;
|
||||
startSeconds?: number;
|
||||
videoRef: string;
|
||||
} | null {
|
||||
const allowed = new Set(["end", "frames", "sessionId", "start", "videoRef"]);
|
||||
if ([...url.searchParams.keys()].some((key) => !allowed.has(key))) return null;
|
||||
const sessionId = url.searchParams.get("sessionId")?.trim() ?? "";
|
||||
const videoRef = url.searchParams.get("videoRef")?.trim() ?? "";
|
||||
if (!sessionId || !videoRef) return null;
|
||||
const parseNumber = (name: string): number | undefined | null => {
|
||||
const value = url.searchParams.get(name);
|
||||
if (value === null) return undefined;
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
|
||||
};
|
||||
const startSeconds = parseNumber("start");
|
||||
const endSeconds = parseNumber("end");
|
||||
const rawFrameCount = url.searchParams.get("frames");
|
||||
const frameCount =
|
||||
rawFrameCount === null
|
||||
? undefined
|
||||
: /^\d{1,2}$/.test(rawFrameCount) && Number(rawFrameCount) >= 1 && Number(rawFrameCount) <= 16
|
||||
? Number(rawFrameCount)
|
||||
: null;
|
||||
if (startSeconds === null || endSeconds === null || frameCount === null) return null;
|
||||
return { endSeconds, frameCount, sessionId, startSeconds, videoRef };
|
||||
}
|
||||
|
||||
interface VideoDrilldownRouteDependencies {
|
||||
cache?: VideoDrilldownCache;
|
||||
}
|
||||
|
||||
export async function handleVideoDrilldownRequest(
|
||||
request: Request,
|
||||
dependencies: VideoDrilldownRouteDependencies = {}
|
||||
): Promise<Response> {
|
||||
const url = new URL(request.url);
|
||||
if (url.pathname !== expectedPath()) return invalid("Invalid Video Bridge drill-down path", 404);
|
||||
if (!isVideoBridgeBrokerInternalRequest(request, VIDEO_BRIDGE_BROKER_PATH)) {
|
||||
return invalid("This endpoint requires an authenticated internal loopback request", 403);
|
||||
}
|
||||
const cache = dependencies.cache ?? drilldownCache;
|
||||
if (request.method === "GET") {
|
||||
const query = parseQuery(url);
|
||||
if (!query) return invalid("Invalid Video Bridge drill-down query");
|
||||
const result = cache.get(query.sessionId, query.videoRef, query);
|
||||
return result
|
||||
? Response.json(result, { headers: { "Cache-Control": "no-store" } })
|
||||
: invalid("Video Bridge drill-down result was not found", 404);
|
||||
}
|
||||
if (request.method === "DELETE") {
|
||||
const sessionId = url.searchParams.get("sessionId")?.trim() ?? "";
|
||||
if (!sessionId || [...url.searchParams.keys()].some((key) => key !== "sessionId")) {
|
||||
return invalid("A sessionId is required");
|
||||
}
|
||||
return Response.json({ removed: cache.clearSession(sessionId) });
|
||||
}
|
||||
if (request.method !== "POST") return invalid("Invalid Video Bridge drill-down method", 405);
|
||||
if (request.headers.get("content-type")?.toLowerCase() !== "application/json") {
|
||||
return invalid("Video Bridge drill-down requires application/json");
|
||||
}
|
||||
const declaredLength = Number(request.headers.get("content-length"));
|
||||
if (Number.isFinite(declaredLength) && declaredLength > MAX_BODY_BYTES) {
|
||||
return invalid("Video Bridge drill-down payload is too large", 413);
|
||||
}
|
||||
let body: unknown;
|
||||
try {
|
||||
const bytes = await request.arrayBuffer();
|
||||
if (bytes.byteLength > MAX_BODY_BYTES)
|
||||
return invalid("Video Bridge drill-down payload is too large", 413);
|
||||
body = JSON.parse(Buffer.from(bytes).toString("utf8"));
|
||||
} catch {
|
||||
return invalid("Video Bridge drill-down payload is invalid");
|
||||
}
|
||||
if (!body || typeof body !== "object")
|
||||
return invalid("Video Bridge drill-down payload is invalid");
|
||||
const record = body as Record<string, unknown>;
|
||||
if (
|
||||
typeof record.sessionId !== "string" ||
|
||||
typeof record.videoRef !== "string" ||
|
||||
typeof record.durationSeconds !== "number" ||
|
||||
!Array.isArray(record.frames)
|
||||
) {
|
||||
return invalid("Video Bridge drill-down payload is invalid");
|
||||
}
|
||||
try {
|
||||
cache.put(record.sessionId, record.videoRef, {
|
||||
durationSeconds: record.durationSeconds,
|
||||
frames: record.frames as VideoDrilldownFrame[],
|
||||
});
|
||||
} catch {
|
||||
return invalid("Video Bridge drill-down payload is invalid");
|
||||
}
|
||||
return Response.json({ stored: true }, { status: 201, headers: { "Cache-Control": "no-store" } });
|
||||
}
|
||||
|
||||
export async function POST(request: Request): Promise<Response> {
|
||||
return handleVideoDrilldownRequest(request);
|
||||
}
|
||||
|
||||
export async function GET(request: Request): Promise<Response> {
|
||||
return handleVideoDrilldownRequest(request);
|
||||
}
|
||||
|
||||
export async function DELETE(request: Request): Promise<Response> {
|
||||
return handleVideoDrilldownRequest(request);
|
||||
}
|
||||
171
src/lib/guardrails/videoBridgeDrilldown.ts
Normal file
171
src/lib/guardrails/videoBridgeDrilldown.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
import { resolveVideoFocusWindow, type VideoFocusWindow } from "./videoBridgeRuntime";
|
||||
|
||||
export interface VideoDrilldownFrame {
|
||||
dataUri: string;
|
||||
timestampSeconds: number;
|
||||
}
|
||||
|
||||
export interface VideoDrilldownPutValue {
|
||||
durationSeconds: number;
|
||||
frames: readonly VideoDrilldownFrame[];
|
||||
}
|
||||
|
||||
export interface VideoDrilldownResult {
|
||||
durationSeconds: number;
|
||||
focusWindow?: VideoFocusWindow;
|
||||
frames: VideoDrilldownFrame[];
|
||||
}
|
||||
|
||||
export interface VideoDrilldownCacheOptions {
|
||||
maxEntries: number;
|
||||
now?: () => number;
|
||||
ttlMs: number;
|
||||
}
|
||||
|
||||
interface StoredDrilldown extends VideoDrilldownPutValue {
|
||||
expiresAt: number;
|
||||
sessionId: string;
|
||||
}
|
||||
|
||||
const MAX_FRAME_BYTES = 4 * 1024 * 1024;
|
||||
const MAX_TOTAL_BYTES = 32 * 1024 * 1024;
|
||||
const MAX_DURATION_SECONDS = 600;
|
||||
|
||||
function cacheKey(sessionId: string, videoRef: string): string {
|
||||
return createHash("sha256").update(`${sessionId}\0${videoRef}`).digest("hex");
|
||||
}
|
||||
|
||||
function validateFrames(value: VideoDrilldownPutValue): VideoDrilldownFrame[] {
|
||||
if (
|
||||
!Number.isFinite(value.durationSeconds) ||
|
||||
value.durationSeconds <= 0 ||
|
||||
value.durationSeconds > MAX_DURATION_SECONDS ||
|
||||
!Array.isArray(value.frames) ||
|
||||
value.frames.length < 1 ||
|
||||
value.frames.length > 16
|
||||
) {
|
||||
throw new Error("Invalid drill-down duration or frame count");
|
||||
}
|
||||
let totalBytes = 0;
|
||||
const frames = value.frames.map((frame) => {
|
||||
if (
|
||||
!frame ||
|
||||
!Number.isFinite(frame.timestampSeconds) ||
|
||||
frame.timestampSeconds < 0 ||
|
||||
frame.timestampSeconds > value.durationSeconds ||
|
||||
!/^data:image\/jpeg;base64,[A-Za-z0-9+/=]+$/i.test(frame.dataUri)
|
||||
) {
|
||||
throw new Error("Invalid drill-down JPEG frame");
|
||||
}
|
||||
const encoded = frame.dataUri.slice(frame.dataUri.indexOf(",") + 1);
|
||||
const bytes = Math.floor((encoded.length * 3) / 4);
|
||||
if (bytes < 1 || bytes > MAX_FRAME_BYTES)
|
||||
throw new Error("Drill-down frame byte limit exceeded");
|
||||
totalBytes += bytes;
|
||||
if (totalBytes > MAX_TOTAL_BYTES) throw new Error("Drill-down response byte limit exceeded");
|
||||
return { dataUri: frame.dataUri, timestampSeconds: frame.timestampSeconds };
|
||||
});
|
||||
return frames.sort((left, right) => left.timestampSeconds - right.timestampSeconds);
|
||||
}
|
||||
|
||||
export class VideoDrilldownCache {
|
||||
private readonly entries = new Map<string, StoredDrilldown>();
|
||||
private readonly now: () => number;
|
||||
|
||||
constructor(private readonly options: VideoDrilldownCacheOptions) {
|
||||
if (!Number.isFinite(options.ttlMs) || options.ttlMs <= 0) {
|
||||
throw new Error("Drill-down cache TTL must be positive");
|
||||
}
|
||||
if (!Number.isInteger(options.maxEntries) || options.maxEntries < 1) {
|
||||
throw new Error("Drill-down cache entry limit is invalid");
|
||||
}
|
||||
this.now = options.now ?? Date.now;
|
||||
}
|
||||
|
||||
put(sessionId: string, videoRef: string, value: VideoDrilldownPutValue): void {
|
||||
if (!sessionId || sessionId.length > 128 || !videoRef || videoRef.length > 4096) {
|
||||
throw new Error("Drill-down cache key is invalid");
|
||||
}
|
||||
const key = cacheKey(sessionId, videoRef);
|
||||
this.entries.delete(key);
|
||||
this.entries.set(key, {
|
||||
durationSeconds: value.durationSeconds,
|
||||
expiresAt: this.now() + this.options.ttlMs,
|
||||
frames: validateFrames(value),
|
||||
sessionId,
|
||||
});
|
||||
while (this.entries.size > this.options.maxEntries) {
|
||||
const oldest = this.entries.keys().next().value;
|
||||
if (oldest) this.entries.delete(oldest);
|
||||
}
|
||||
}
|
||||
|
||||
get(
|
||||
sessionId: string,
|
||||
videoRef: string,
|
||||
options: { endSeconds?: number; frameCount?: number; startSeconds?: number } = {}
|
||||
): VideoDrilldownResult | null {
|
||||
const key = cacheKey(sessionId, videoRef);
|
||||
const stored = this.entries.get(key);
|
||||
if (!stored) return null;
|
||||
if (stored.expiresAt <= this.now()) {
|
||||
this.entries.delete(key);
|
||||
return null;
|
||||
}
|
||||
this.entries.delete(key);
|
||||
this.entries.set(key, stored);
|
||||
const hasFocus = options.startSeconds !== undefined || options.endSeconds !== undefined;
|
||||
let focusWindow: VideoFocusWindow | null = null;
|
||||
try {
|
||||
focusWindow = hasFocus
|
||||
? resolveVideoFocusWindow(stored.durationSeconds, {
|
||||
endSeconds: options.endSeconds,
|
||||
startSeconds: options.startSeconds,
|
||||
})
|
||||
: null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const frameCount =
|
||||
options.frameCount === undefined
|
||||
? 16
|
||||
: Number.isInteger(options.frameCount) &&
|
||||
options.frameCount >= 1 &&
|
||||
options.frameCount <= 16
|
||||
? options.frameCount
|
||||
: null;
|
||||
if (frameCount === null) return null;
|
||||
const frames = stored.frames
|
||||
.filter(
|
||||
(frame) =>
|
||||
!focusWindow ||
|
||||
(frame.timestampSeconds >= focusWindow.startSeconds &&
|
||||
frame.timestampSeconds <= focusWindow.endSeconds)
|
||||
)
|
||||
.slice(0, frameCount)
|
||||
.map((frame) => ({ ...frame }));
|
||||
if (frames.length === 0) return null;
|
||||
return {
|
||||
durationSeconds: stored.durationSeconds,
|
||||
...(focusWindow ? { focusWindow } : {}),
|
||||
frames,
|
||||
};
|
||||
}
|
||||
|
||||
clearSession(sessionId: string): number {
|
||||
let removed = 0;
|
||||
for (const [key, entry] of this.entries.entries()) {
|
||||
if (entry.sessionId === sessionId) {
|
||||
this.entries.delete(key);
|
||||
removed += 1;
|
||||
}
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
clearAll(): void {
|
||||
this.entries.clear();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user