import debug from 'debug'; import { DEBUG } from '$lib/debug'; const dbg = debug('kestrel: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, 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; } }); while (this.active < this.concurrency && this.queue.length) { const nextIdx = this.queue.findIndex((e) => e.check && e.check()) || 0; const next = this.queue.splice(nextIdx, 1)[0]; dbg(`Getting ${next.id}...`); this.active += 1; next.element.classList.add('image-loading'); if (DEBUG.imageQueueHalt) { return; } next .callback() .then(() => { dbg(`Loaded ${next.id}`); }) .catch(() => { dbg(`Failed to load ${next.id}...`); }) .finally(() => { this.active -= 1; next.element.classList.remove('image-loading'); this.update(); }); } dbg( 'Active: %d, Queue: %O', this.active, this.queue.map((e) => [e.element, e.id]) ); } } const imageQueue = new ImageQueue(2); 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() { element.classList.add('image-queued'); const loadSelf = () => { element.classList.remove('image-queued'); 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, () => visible); } queueSelf(); return { update(_src: string) { queueSelf(); }, destroy() { observer.disconnect(); } }; }