17. Mini project: an async task queue with concurrency limits and retry
class TaskQueue {
#concurrency; #running = 0; #queue = [];
constructor(concurrency = 3) { this.#concurrency = concurrency; }
add(taskFn, { retries = 2 } = {}) {
return new Promise((resolve, reject) => {
this.#queue.push({ taskFn, retries, resolve, reject });
this.#next();
});
}
async #next() {
if (this.#running >= this.#concurrency || this.#queue.length === 0) return;
this.#running++;
const item = this.#queue.shift();
try {
resolve_result: {
try {
const result = await item.taskFn();
item.resolve(result);
break resolve_result;
} catch (err) {
if (item.retries > 0) {
this.#queue.push({ ...item, retries: item.retries - 1 });
} else {
item.reject(err);
}
}
}
} finally {
this.#running--;
this.#next(); // pull the next queued item, whether we just succeeded or freed a slot
}
}
}
const queue = new TaskQueue(2); // only 2 fetches in flight at once
const urls = ["/api/a", "/api/b", "/api/c", "/api/d", "/api/e"];
const results = await Promise.all(
urls.map((url) => queue.add(() => fetch(url).then((r) => r.json())))
);
This is the exact shape of pattern real apps use to avoid saturating a browser's per-origin connection limit (historically 6 for HTTP/1.1, see §26) when firing many requests at once.