import debug from "debug"; const dbg = debug("upend:imageQueue"); class ImageQueue { concurrency: number; queue: { element: HTMLElement; id: string; callback: () => Promise; check?: () => boolean; }[] = []; active = 0; constructor(concurrency: number) { this.concurrency = concurrency; } public add( element: HTMLImageElement, id: string, callback: () => Promise, order?: () => number, check?: () => boolean ) { this.queue = this.queue.filter((e) => e.element !== element); this.queue.push({ element, id, callback, check }); this.update(); } private update() { this.queue.sort((a, b) => { const aBox = a.element.getBoundingClientRect(); const bBox = b.element.getBoundingClientRect(); const topDifference = aBox.top - bBox.top; if (topDifference !== 0) { return topDifference; } else { return aBox.left - bBox.left; } }); dbg( "Active: %d, Queue: %O", this.active, this.queue.map((e) => [e.element, e.id]) ); if (this.active >= this.concurrency) { return; } if (!this.queue.length) { return; } const nextIdx = this.queue.findIndex((e) => e.check()) || 0; const next = this.queue.splice(nextIdx, 1)[0]; dbg(`Getting ${next.id}...`); this.active += 1; next .callback() .then(() => { dbg(`Loaded ${next.id}`); }) .catch(() => { dbg(`Failed to load ${next.id}...`); }) .finally(() => { this.active -= 1; this.update(); }); } } const imageQueue = new ImageQueue(1); export function concurrentImage(element: HTMLImageElement, src: string) { const bbox = element.getBoundingClientRect(); let visible = bbox.top >= 0 && bbox.left >= 0 && bbox.bottom <= window.innerHeight && bbox.right <= window.innerWidth; const observer = new IntersectionObserver((entries) => { visible = entries.some((e) => e.isIntersecting); }); observer.observe(element); function queueSelf() { const loadSelf = () => { return new Promise((resolve, reject) => { if (element.src === src) { resolve(); return; } element.addEventListener("load", () => { resolve(); }); element.addEventListener("error", () => { reject(); }); element.src = src; }); }; imageQueue.add( element, src, loadSelf, () => element.getBoundingClientRect().top, () => visible ); } queueSelf(); return { update(_src: string) { queueSelf(); }, destroy() { observer.disconnect(); }, }; }