fix thumbnails

This commit is contained in:
KingKDot
2026-05-18 09:55:05 -05:00
parent 4129fc2164
commit 95f7db5dd5
8 changed files with 192 additions and 23 deletions
@@ -253,9 +253,10 @@ export async function handleClientRoutes(
if (!canUserAccessClient(user.userId, user.role, clientId)) {
return new Response("Forbidden: Client access denied", { status: 403 });
}
const { generateThumbnail, markThumbnailRequested, getThumbnailVersion } = await import("../../thumbnails");
const { requestThumbnailRegen, markThumbnailRequested, getThumbnailVersion, waitForThumbnail, clearThumbnailRequest } = await import("../../thumbnails");
markThumbnailRequested(clientId);
const target = clientManager.getClient(clientId);
const beforeVersion = getThumbnailVersion(clientId);
if (target?.online) {
const commandId = uuidv4();
target.ws.send(
@@ -268,10 +269,16 @@ export async function handleClientRoutes(
);
metrics.recordCommand("screenshot");
}
const success = await generateThumbnail(clientId);
const fresh = target?.online ? await waitForThumbnail(clientId, 2500) : false;
if (fresh) {
await new Promise((r) => setTimeout(r, 300));
} else {
await requestThumbnailRegen(clientId);
}
clearThumbnailRequest(clientId);
const version = getThumbnailVersion(clientId);
return Response.json(
{ ok: true, updated: success, version },
{ ok: true, updated: version > beforeVersion, version },
{ headers: deps.CORS_HEADERS },
);
}
+84 -1
View File
@@ -128,7 +128,9 @@ export async function generateThumbnail(id: string): Promise<boolean> {
updatedAt: now,
});
evictThumbnailsIfFull();
latestFrames.delete(id);
if (latestFrames.get(id) === frameData) {
latestFrames.delete(id);
}
return true;
} catch (err) {
console.error(`[thumbnails] Failed to generate thumbnail for client ${id}:`, err);
@@ -136,10 +138,54 @@ export async function generateThumbnail(id: string): Promise<boolean> {
}
}
const thumbnailGenState = new Map<string, { inFlight: boolean; pending: boolean }>();
export async function requestThumbnailRegen(id: string): Promise<boolean> {
let state = thumbnailGenState.get(id);
if (!state) {
state = { inFlight: false, pending: false };
thumbnailGenState.set(id, state);
}
if (state.inFlight) {
state.pending = true;
return false;
}
state.inFlight = true;
let didGenerate = false;
try {
while (true) {
state.pending = false;
const ok = await generateThumbnail(id);
if (ok) didGenerate = true;
if (!state.pending) break;
}
} finally {
state.inFlight = false;
if (thumbnailGenState.get(id) === state && !state.pending) {
thumbnailGenState.delete(id);
}
}
return didGenerate;
}
export function markThumbnailRequested(id: string) {
thumbnailRequests.set(id, Date.now());
}
export function isThumbnailRequested(id: string, windowMs = 5000): boolean {
const ts = thumbnailRequests.get(id);
if (!ts) return false;
if (Date.now() - ts > windowMs) {
thumbnailRequests.delete(id);
return false;
}
return true;
}
export function clearThumbnailRequest(id: string) {
thumbnailRequests.delete(id);
}
export function consumeThumbnailRequest(id: string, windowMs = 5000): boolean {
const ts = thumbnailRequests.get(id);
if (!ts) return false;
@@ -150,3 +196,40 @@ export function consumeThumbnailRequest(id: string, windowMs = 5000): boolean {
thumbnailRequests.delete(id);
return true;
}
const thumbnailWaiters = new Map<string, Set<() => void>>();
export function notifyThumbnailGenerated(id: string) {
const waiters = thumbnailWaiters.get(id);
if (!waiters) return;
thumbnailWaiters.delete(id);
for (const cb of waiters) {
try { cb(); } catch {}
}
}
export function waitForThumbnail(id: string, timeoutMs = 2500): Promise<boolean> {
return new Promise((resolve) => {
let settled = false;
let callback: () => void;
const finish = (fresh: boolean) => {
if (settled) return;
settled = true;
clearTimeout(timer);
const set = thumbnailWaiters.get(id);
if (set) {
set.delete(callback);
if (set.size === 0) thumbnailWaiters.delete(id);
}
resolve(fresh);
};
callback = () => finish(true);
const timer = setTimeout(() => finish(false), timeoutMs);
let set = thumbnailWaiters.get(id);
if (!set) {
set = new Set();
thumbnailWaiters.set(id, set);
}
set.add(callback);
});
}
+7 -4
View File
@@ -9,10 +9,11 @@ async function getGeoip() {
}
import { ClientInfo } from "./types";
import {
consumeThumbnailRequest,
isThumbnailRequested,
hasThumbnail,
generateThumbnail,
setLatestFrame,
notifyThumbnailGenerated,
requestThumbnailRegen,
} from "./thumbnails";
import { upsertClientRow, type ClientDbRow } from "./db";
import { metrics } from "./metrics";
@@ -296,10 +297,12 @@ export function handleFrame(info: ClientInfo, payload: any) {
if (safeFormat) {
const now = Date.now();
const thumbnailRequested = consumeThumbnailRequest(info.id);
const thumbnailRequested = isThumbnailRequested(info.id);
if (thumbnailRequested || !hasThumbnail(info.id)) {
setLatestFrame(info.id, bytes, safeFormat);
void generateThumbnail(info.id);
void requestThumbnailRegen(info.id).then((ok) => {
if (ok) notifyThumbnailGenerated(info.id);
});
}
info.lastSeen = now;
info.online = true;