From 21409281b2794cbdf7fed30d31460375f3f6b2c5 Mon Sep 17 00:00:00 2001 From: Hackall <36754621+hackall360@users.noreply.github.com> Date: Sun, 14 Sep 2025 14:33:55 -0700 Subject: [PATCH] Add polling for placeholder images --- js/ui/imagePoller.js | 62 +++++++++++++++++++++++++++++++++++++ tests/site-image-poller.mjs | 31 +++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 js/ui/imagePoller.js create mode 100644 tests/site-image-poller.mjs diff --git a/js/ui/imagePoller.js b/js/ui/imagePoller.js new file mode 100644 index 0000000..e97a513 --- /dev/null +++ b/js/ui/imagePoller.js @@ -0,0 +1,62 @@ +import { image as polliImage } from '../polliLib/src/image.js'; + +let imageFn = polliImage; +let pollIntervalMs = 2000; +let timeoutMs = 20000; +let fallbackSrc = 'https://via.placeholder.com/512?text=Image+Unavailable'; + +const pending = new Map(); +let timer = null; + +export function trackPlaceholder(img, { prompt, model, width, height } = {}) { + if (!img) return; + pending.set(img, { prompt, model, width, height, start: Date.now() }); + if (!timer) { + timer = setInterval(() => { + checkPending().catch(() => {}); + }, pollIntervalMs); + } +} + +async function checkPending() { + for (const [img, info] of pending) { + if (Date.now() - info.start > timeoutMs) { + img.src = fallbackSrc; + img.classList?.remove?.('placeholder'); + pending.delete(img); + continue; + } + try { + const data = await imageFn(info.prompt, { model: info.model, width: info.width, height: info.height, json: true }); + if (data && data.url) { + img.src = data.url; + img.classList?.remove?.('placeholder'); + pending.delete(img); + } + } catch (err) { + img.src = fallbackSrc; + img.classList?.remove?.('placeholder'); + pending.delete(img); + } + } + if (pending.size === 0 && timer) { + clearInterval(timer); + timer = null; + } +} + +export function _setImageFn(fn) { + imageFn = fn; +} + +export function _configure({ intervalMs, timeout, fallback } = {}) { + if (intervalMs != null) pollIntervalMs = intervalMs; + if (timeout != null) timeoutMs = timeout; + if (fallback != null) fallbackSrc = fallback; + if (timer) { + clearInterval(timer); + timer = setInterval(() => { + checkPending().catch(() => {}); + }, pollIntervalMs); + } +} diff --git a/tests/site-image-poller.mjs b/tests/site-image-poller.mjs new file mode 100644 index 0000000..8409792 --- /dev/null +++ b/tests/site-image-poller.mjs @@ -0,0 +1,31 @@ +import assert from 'assert/strict'; +import { trackPlaceholder, _configure, _setImageFn } from '../js/ui/imagePoller.js'; + +let calls = 0; +_setImageFn(async () => { + calls++; + if (calls === 1) return {}; // still pending + return { url: 'https://example.com/final.png' }; +}); + +_configure({ intervalMs: 5, timeout: 100 }); + +const img = { + src: 'about:blank', + classList: { + set: new Set(['placeholder']), + remove(cls) { this.set.delete(cls); }, + contains(cls) { return this.set.has(cls); } + } +}; + +trackPlaceholder(img, { prompt: 'test', model: 'foo' }); + +// wait long enough for two polling cycles +await new Promise(r => setTimeout(r, 30)); +await new Promise(r => setTimeout(r, 30)); + +assert.equal(img.src, 'https://example.com/final.png'); +assert.equal(img.classList.contains('placeholder'), false); + +console.log('image-poller test passed');