upend/webui/src/components/imageQueue.ts

121 lines
2.9 KiB
TypeScript

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<void>;
check?: () => boolean;
}[] = [];
active = 0;
constructor(concurrency: number) {
this.concurrency = concurrency;
}
public add(
element: HTMLImageElement,
id: string,
callback: () => Promise<void>,
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()) || 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<void>((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();
},
};
}