linkinator
Version:
Find broken links, missing images, etc in your HTML. Scurry around your site and find all those broken links.
111 lines (110 loc) • 3.57 kB
JavaScript
import { EventEmitter } from 'node:events';
export class Queue extends EventEmitter {
q = [];
activeFunctions = 0;
concurrency;
wakeup;
wakeupTime;
constructor(options) {
super();
this.concurrency = options.concurrency;
}
// biome-ignore lint/suspicious/noExplicitAny: this can actually be any
on(event, listener) {
return super.on(event, listener);
}
add(function_, options) {
const delay = options?.delay || 0;
const timeToRun = Date.now() + delay;
this.q.push({
fn: function_,
timeToRun,
});
this.scheduleWakeup();
}
async onIdle() {
if (this.activeFunctions === 0 && this.q.length === 0) {
return;
}
return new Promise((resolve) => {
this.once('done', () => {
resolve();
});
});
}
tick() {
// Check if we're complete
if (this.activeFunctions === 0 && this.q.length === 0) {
this.cancelWakeup();
this.emit('done');
return;
}
// Inspect each currently queued item once. Delayed items are moved to the
// back and handled by a referenced timer for the earliest due item.
const queuedItems = this.q.length;
for (let i = 0; i < queuedItems; i++) {
// Check if we have too many concurrent functions executing
if (this.activeFunctions >= this.concurrency) {
break;
}
// Grab the element at the front of the array
const item = this.q.shift();
if (item === undefined) {
throw new Error('unexpected undefined item in queue');
}
// Make sure this element is ready to execute - if not, to the back of the stack
if (item.timeToRun <= Date.now()) {
// This function is ready to go!
this.activeFunctions++;
item
.fn()
.catch(() => {
// Errors are handled within crawl() and stored in results.
// Silently catch here to prevent unhandled promise rejections.
})
.finally(() => {
this.activeFunctions--;
this.tick();
});
}
else {
this.q.push(item);
}
}
if (this.q.length === 0) {
this.cancelWakeup();
return;
}
this.scheduleWakeup();
}
cancelWakeup() {
if (this.wakeup !== undefined) {
clearTimeout(this.wakeup);
this.wakeup = undefined;
this.wakeupTime = undefined;
}
}
scheduleWakeup() {
if (this.activeFunctions >= this.concurrency || this.q.length === 0) {
return;
}
let nextRun = Number.POSITIVE_INFINITY;
for (const item of this.q) {
nextRun = Math.min(nextRun, item.timeToRun);
}
if (this.wakeup !== undefined &&
this.wakeupTime !== undefined &&
this.wakeupTime <= nextRun) {
return;
}
if (this.wakeup !== undefined) {
this.cancelWakeup();
}
this.wakeupTime = nextRun;
this.wakeup = setTimeout(() => {
this.wakeup = undefined;
this.wakeupTime = undefined;
this.tick();
}, Math.max(0, nextRun - Date.now()));
}
}