Skip to content

Commit 47f684e

Browse files
authored
Merge pull request #911 from luckyb13/priority-queue-js
Priority queue js
2 parents 4796793 + 3561486 commit 47f684e

File tree

3 files changed

+234
-0
lines changed

3 files changed

+234
-0
lines changed

javascript/pLimit.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// pLimit.js
2+
// Minimal promise concurrency limiter (like p-limit). Queue tasks, run at most N at once.
3+
4+
/**
5+
* @typedef {() => Promise<any>} Task
6+
*/
7+
8+
/**
9+
* Create a concurrency limiter.
10+
* @param {number} concurrency - number of tasks to run simultaneously (>=1)
11+
* @returns {{
12+
* (fn: (...args:any[])=>Promise<any>|any, ...args:any[]): Promise<any>,
13+
* activeCount: () => number,
14+
* pendingCount: () => number,
15+
* clearQueue: () => void
16+
* }}
17+
*/
18+
export function pLimit(concurrency = 4) {
19+
if (!Number.isInteger(concurrency) || concurrency < 1) {
20+
throw new Error("concurrency must be an integer >= 1");
21+
}
22+
23+
/** @type {Task[]} */
24+
const queue = [];
25+
let active = 0;
26+
27+
const next = () => {
28+
if (active >= concurrency) return;
29+
const task = queue.shift();
30+
if (!task) return;
31+
active++;
32+
task().finally(() => {
33+
active--;
34+
next();
35+
});
36+
};
37+
38+
const run = (fn, ...args) => new Promise((resolve, reject) => {
39+
const task = async () => {
40+
try {
41+
resolve(await fn(...args));
42+
} catch (e) {
43+
reject(e);
44+
}
45+
};
46+
queue.push(task);
47+
// Try to start tasks ASAP (microtask ensures consistent ordering)
48+
queue.length && queue.length <= concurrency ? Promise.resolve().then(next) : next();
49+
});
50+
51+
run.activeCount = () => active;
52+
run.pendingCount = () => queue.length;
53+
run.clearQueue = () => { queue.length = 0; };
54+
55+
return run;
56+
}
57+
58+
// // Example usage:
59+
// const limit = pLimit(2);
60+
// const wait = ms => new Promise(r => setTimeout(r, ms));
61+
// await Promise.all([1,2,3,4,5].map(n => limit(async () => { await wait(100*n); return n; })));

javascript/priorityQueue.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// priorityQueue.js
2+
// Binary-heap Priority Queue with custom comparator (default: min-heap).
3+
// API: push, pop, peek, size, isEmpty, clear.
4+
5+
export class PriorityQueue {
6+
/**
7+
* @param {(a:any,b:any)=>number} [compare] - return <0 if a<b, 0 if equal, >0 if a>b
8+
*/
9+
constructor(compare = (a, b) => (a < b ? -1 : a > b ? 1 : 0)) {
10+
this._heap = [];
11+
this._cmp = compare;
12+
}
13+
14+
get size() { return this._heap.length; }
15+
isEmpty() { return this.size === 0; }
16+
clear() { this._heap.length = 0; }
17+
18+
peek() { return this._heap[0]; }
19+
20+
push(value) {
21+
this._heap.push(value);
22+
this.#siftUp(this.size - 1);
23+
}
24+
25+
pop() {
26+
const n = this.size;
27+
if (n === 0) return undefined;
28+
this.#swap(0, n - 1);
29+
const out = this._heap.pop();
30+
this.#siftDown(0);
31+
return out;
32+
}
33+
34+
#parent(i) { return ((i - 1) >> 1); }
35+
#left(i) { return (i << 1) + 1; }
36+
#right(i) { return (i << 1) + 2; }
37+
38+
#swap(i, j) {
39+
const a = this._heap;
40+
[a[i], a[j]] = [a[j], a[i]];
41+
}
42+
43+
#siftUp(i) {
44+
for (let p = this.#parent(i); i > 0 && this._cmp(this._heap[i], this._heap[p]) < 0; p = this.#parent(i)) {
45+
this.#swap(i, p);
46+
i = p;
47+
}
48+
}
49+
50+
#siftDown(i) {
51+
for (;;) {
52+
const l = this.#left(i), r = this.#right(i);
53+
let best = i;
54+
if (l < this.size && this._cmp(this._heap[l], this._heap[best]) < 0) best = l;
55+
if (r < this.size && this._cmp(this._heap[r], this._heap[best]) < 0) best = r;
56+
if (best === i) break;
57+
this.#swap(i, best);
58+
i = best;
59+
}
60+
}
61+
}
62+
63+
// // Example:
64+
// const pq = new PriorityQueue(); // min-heap numbers
65+
// [5,1,4,2,3].forEach(x => pq.push(x));
66+
// while (!pq.isEmpty()) console.log(pq.pop()); // 1,2,3,4,5

javascript/toposort.js

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// topoSort.js
2+
// Topological sort with cycle detection for dependency graphs.
3+
// Accepts either: adjacency-list object or edge list. Throws on cycles.
4+
5+
/**
6+
* @typedef {Record<string,string[]>} Graph
7+
* @typedef {[string,string]} Edge // [from, to]
8+
*/
9+
10+
/**
11+
* Build adjacency list from edges or return copy of given graph.
12+
* @param {Graph|Edge[]} input
13+
* @returns {Graph}
14+
*/
15+
function toGraph(input) {
16+
/** @type {Graph} */
17+
const g = {};
18+
if (Array.isArray(input)) {
19+
for (const [u, v] of input) {
20+
if (!g[u]) g[u] = [];
21+
if (!g[v]) g[v] = [];
22+
g[u].push(v);
23+
}
24+
} else {
25+
for (const k of Object.keys(input)) g[k] = [...(input[k] || [])];
26+
}
27+
// Ensure all nodes appear
28+
for (const k of Object.keys(g)) for (const v of g[k]) if (!g[v]) g[v] = [];
29+
return g;
30+
}
31+
32+
/**
33+
* Topologically sort nodes. Throws Error with cycle nodes if cyclic.
34+
* @param {Graph|Edge[]} input
35+
* @returns {string[]} order
36+
*/
37+
export function topoSort(input) {
38+
const g = toGraph(input);
39+
/** @type {Record<string,0|1|2>} */ // 0=unvisited,1=visiting,2=done
40+
const state = {};
41+
const order = [];
42+
const stack = [];
43+
44+
const visit = (node) => {
45+
const st = state[node] || 0;
46+
if (st === 1) {
47+
// Found a back-edge → cycle is the suffix of the stack up to node
48+
const idx = stack.lastIndexOf(node);
49+
const cycle = stack.slice(idx).concat(node);
50+
const msg = `Cycle detected: ${cycle.join(" -> ")}`;
51+
const err = new Error(msg);
52+
// @ts-ignore attach for debugging
53+
err.cycle = cycle;
54+
throw err;
55+
}
56+
if (st === 2) return;
57+
58+
state[node] = 1;
59+
stack.push(node);
60+
for (const nei of g[node]) visit(nei);
61+
stack.pop();
62+
state[node] = 2;
63+
order.push(node);
64+
};
65+
66+
for (const node of Object.keys(g)) if (!state[node]) visit(node);
67+
return order.reverse();
68+
}
69+
70+
/**
71+
* Group nodes by "level" (distance from sources) using Kahn's algorithm.
72+
* If graph has a cycle, throws like topoSort.
73+
* @param {Graph|Edge[]} input
74+
* @returns {string[][]} levels, where levels[0] are sources
75+
*/
76+
export function topoLevels(input) {
77+
const g = toGraph(input);
78+
const indeg = Object.fromEntries(Object.keys(g).map(k => [k,0]));
79+
for (const u of Object.keys(g)) for (const v of g[u]) indeg[v]++;
80+
81+
/** @type {string[][]} */
82+
const levels = [];
83+
let layer = Object.keys(indeg).filter(k => indeg[k] === 0);
84+
85+
let visited = 0;
86+
while (layer.length) {
87+
levels.push(layer);
88+
const next = [];
89+
for (const u of layer) {
90+
visited++;
91+
for (const v of g[u]) {
92+
if (--indeg[v] === 0) next.push(v);
93+
}
94+
}
95+
layer = next;
96+
}
97+
98+
if (visited !== Object.keys(g).length) {
99+
// cycle present; derive one cycle path using DFS utility
100+
topoSort(g); // will throw with details
101+
}
102+
return levels;
103+
}
104+
105+
// // Example usage:
106+
// // const order = topoSort([["a","b"],["a","c"],["b","d"],["c","d"]]);
107+
// // const levels = topoLevels({ a:["b","c"], b:["d"], c:["d"], d:[] });

0 commit comments

Comments
 (0)