UNPKG

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
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())); } }