ai-chatbot/hooks/use-canvas-image-archives.ts

106 lines
3.5 KiB
TypeScript

"use client";
import { useEffect } from "react";
import { useCanvasStore } from "@/store/canvas";
import {
collectImageArchiveSources,
type ArchivedImageUrlMap,
} from "@/lib/storage/image-archive-urls";
function preload(url: string, signal: AbortSignal): Promise<boolean> {
return new Promise((resolve) => {
const image = new Image();
let done = false;
const finish = (loaded: boolean) => {
if (done) return;
done = true;
clearTimeout(timer);
signal.removeEventListener("abort", cancel);
image.onload = image.onerror = null;
resolve(loaded);
};
const cancel = () => finish(false);
const timer = setTimeout(cancel, 15_000);
signal.addEventListener("abort", cancel, { once: true });
image.onload = () => finish(true);
image.onerror = cancel;
if (signal.aborted) cancel();
else image.src = url;
});
}
export function useCanvasImageArchives(projectId: string) {
useEffect(() => {
const controller = new AbortController();
const checked = new Set<string>();
const unknownChecks = new Map<string, number>();
let timer: ReturnType<typeof setTimeout> | undefined;
let polling = false;
const poll = async () => {
if (polling || controller.signal.aborted || document.visibilityState === "hidden") return;
const state = useCanvasStore.getState();
if (state.projectId !== projectId) return;
const urls = collectImageArchiveSources(state.nodes)
.filter((url) => !checked.has(url))
.slice(0, 100);
if (!urls.length) return;
polling = true;
try {
const response = await fetch(
`/api/canvas/${encodeURIComponent(projectId)}/image-archives`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ urls }),
signal: controller.signal,
},
);
if (!response.ok) return;
const result = (await response.json()) as {
replacements: ArchivedImageUrlMap;
pending: string[];
};
const pending = new Set(result.pending);
const ready: ArchivedImageUrlMap = {};
await Promise.all(
urls.map(async (url) => {
const target = result.replacements[url];
if (target) {
if (await preload(target, controller.signal)) {
ready[url] = target;
checked.add(url);
checked.add(target);
}
} else if (!pending.has(url)) {
// A direct generation can reach the browser before its first autosave.
// Allow ownership records to catch up before classifying a URL as untracked.
const checks = (unknownChecks.get(url) || 0) + 1;
unknownChecks.set(url, checks);
if (checks >= 10) checked.add(url);
}
}),
);
if (
!controller.signal.aborted &&
useCanvasStore.getState().projectId === projectId &&
Object.keys(ready).length
) {
useCanvasStore.getState().replaceArchivedImageUrls(ready);
}
} catch {
// Temporary network errors keep provider previews and are retried on the next poll.
} finally {
polling = false;
}
};
const tick = () => {
void poll();
timer = setTimeout(tick, 3_000);
};
timer = setTimeout(tick, 1_000);
return () => {
controller.abort();
if (timer) clearTimeout(timer);
};
}, [projectId]);
}