diff --git a/javascript/pLimit.js b/javascript/pLimit.js new file mode 100644 index 00000000..8c374d99 --- /dev/null +++ b/javascript/pLimit.js @@ -0,0 +1,61 @@ +// pLimit.js +// Minimal promise concurrency limiter (like p-limit). Queue tasks, run at most N at once. + +/** + * @typedef {() => Promise} Task + */ + +/** + * Create a concurrency limiter. + * @param {number} concurrency - number of tasks to run simultaneously (>=1) + * @returns {{ + * (fn: (...args:any[])=>Promise|any, ...args:any[]): Promise, + * 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; }))); diff --git a/javascript/toposort.js b/javascript/toposort.js new file mode 100644 index 00000000..b12cd198 --- /dev/null +++ b/javascript/toposort.js @@ -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} 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} */ // 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:[] });