fix thumbnails
This commit is contained in:
@@ -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 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user