Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions javascript/pLimit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// pLimit.js
// Minimal promise concurrency limiter (like p-limit). Queue tasks, run at most N at once.

/**
* @typedef {() => Promise<any>} Task
*/

/**
* Create a concurrency limiter.
* @param {number} concurrency - number of tasks to run simultaneously (>=1)
* @returns {{
* (fn: (...args:any[])=>Promise<any>|any, ...args:any[]): Promise<any>,
* activeCount: () => number,
* pendingCount: () => number,
* clearQueue: () => void
* }}
*/
export function pLimit(concurrency = 4) {
if (!Number.isInteger(concurrency) || concurrency < 1) {
throw new Error("concurrency must be an integer >= 1");
}

/** @type {Task[]} */
const queue = [];
let active = 0;

const next = () => {
if (active >= concurrency) return;
const task = queue.shift();
if (!task) return;
active++;
task().finally(() => {
active--;
next();
});
};

const run = (fn, ...args) => new Promise((resolve, reject) => {
const task = async () => {
try {
resolve(await fn(...args));
} catch (e) {
reject(e);
}
};
queue.push(task);
// Try to start tasks ASAP (microtask ensures consistent ordering)
queue.length && queue.length <= concurrency ? Promise.resolve().then(next) : next();
});

run.activeCount = () => active;
run.pendingCount = () => queue.length;
run.clearQueue = () => { queue.length = 0; };

return run;
}

// // Example usage:
// const limit = pLimit(2);
// const wait = ms => new Promise(r => setTimeout(r, ms));
// await Promise.all([1,2,3,4,5].map(n => limit(async () => { await wait(100*n); return n; })));
107 changes: 107 additions & 0 deletions javascript/toposort.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// topoSort.js
// Topological sort with cycle detection for dependency graphs.
// Accepts either: adjacency-list object or edge list. Throws on cycles.

/**
* @typedef {Record<string,string[]>} Graph
* @typedef {[string,string]} Edge // [from, to]
*/

/**
* Build adjacency list from edges or return copy of given graph.
* @param {Graph|Edge[]} input
* @returns {Graph}
*/
function toGraph(input) {
/** @type {Graph} */
const g = {};
if (Array.isArray(input)) {
for (const [u, v] of input) {
if (!g[u]) g[u] = [];
if (!g[v]) g[v] = [];
g[u].push(v);
}
} else {
for (const k of Object.keys(input)) g[k] = [...(input[k] || [])];
}
// Ensure all nodes appear
for (const k of Object.keys(g)) for (const v of g[k]) if (!g[v]) g[v] = [];
return g;
}

/**
* Topologically sort nodes. Throws Error with cycle nodes if cyclic.
* @param {Graph|Edge[]} input
* @returns {string[]} order
*/
export function topoSort(input) {
const g = toGraph(input);
/** @type {Record<string,0|1|2>} */ // 0=unvisited,1=visiting,2=done
const state = {};
const order = [];
const stack = [];

const visit = (node) => {
const st = state[node] || 0;
if (st === 1) {
// Found a back-edge → cycle is the suffix of the stack up to node
const idx = stack.lastIndexOf(node);
const cycle = stack.slice(idx).concat(node);
const msg = `Cycle detected: ${cycle.join(" -> ")}`;
const err = new Error(msg);
// @ts-ignore attach for debugging
err.cycle = cycle;
throw err;
}
if (st === 2) return;

state[node] = 1;
stack.push(node);
for (const nei of g[node]) visit(nei);
stack.pop();
state[node] = 2;
order.push(node);
};

for (const node of Object.keys(g)) if (!state[node]) visit(node);
return order.reverse();
}

/**
* Group nodes by "level" (distance from sources) using Kahn's algorithm.
* If graph has a cycle, throws like topoSort.
* @param {Graph|Edge[]} input
* @returns {string[][]} levels, where levels[0] are sources
*/
export function topoLevels(input) {
const g = toGraph(input);
const indeg = Object.fromEntries(Object.keys(g).map(k => [k,0]));
for (const u of Object.keys(g)) for (const v of g[u]) indeg[v]++;

/** @type {string[][]} */
const levels = [];
let layer = Object.keys(indeg).filter(k => indeg[k] === 0);

let visited = 0;
while (layer.length) {
levels.push(layer);
const next = [];
for (const u of layer) {
visited++;
for (const v of g[u]) {
if (--indeg[v] === 0) next.push(v);
}
}
layer = next;
}

if (visited !== Object.keys(g).length) {
// cycle present; derive one cycle path using DFS utility
topoSort(g); // will throw with details
}
return levels;
}

// // Example usage:
// // const order = topoSort([["a","b"],["a","c"],["b","d"],["c","d"]]);
// // const levels = topoLevels({ a:["b","c"], b:["d"], c:["d"], d:[] });
Loading