"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 { 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(); const unknownChecks = new Map(); let timer: ReturnType | 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]); }