|
| 1 | +import { DirectEdge } from './queries' |
| 2 | + |
| 3 | +export interface Graph { |
| 4 | + nodeIndex: Map<number, number> |
| 5 | + nodeIds: number[] |
| 6 | + numDeps: Int32Array // how many packages each node depends on (divides its vote) |
| 7 | + rowPtr: Int32Array // rowPtr[v] = start of v's dependents in colData |
| 8 | + colData: Int32Array // flat array of dependent node indices |
| 9 | + N: number |
| 10 | +} |
| 11 | + |
| 12 | +export function buildGraph(edges: DirectEdge[]): Graph { |
| 13 | + // Pass 0: assign contiguous indices |
| 14 | + const nodeIndex = new Map<number, number>() |
| 15 | + const nodeIds: number[] = [] |
| 16 | + |
| 17 | + for (const { packageId, dependsOnId } of edges) { |
| 18 | + if (!nodeIndex.has(packageId)) { nodeIndex.set(packageId, nodeIds.length); nodeIds.push(packageId) } |
| 19 | + if (!nodeIndex.has(dependsOnId)) { nodeIndex.set(dependsOnId, nodeIds.length); nodeIds.push(dependsOnId) } |
| 20 | + } |
| 21 | + |
| 22 | + const N = nodeIds.length |
| 23 | + |
| 24 | + // Pass 1: count |
| 25 | + const depCount = new Int32Array(N) |
| 26 | + const numDeps = new Int32Array(N) |
| 27 | + |
| 28 | + for (const { packageId, dependsOnId } of edges) { |
| 29 | + depCount[nodeIndex.get(dependsOnId)!]++ |
| 30 | + numDeps[nodeIndex.get(packageId)!]++ |
| 31 | + } |
| 32 | + |
| 33 | + // Build rowPtr (prefix sum of depCount) |
| 34 | + const rowPtr = new Int32Array(N + 1) |
| 35 | + for (let i = 0; i < N; i++) rowPtr[i + 1] = rowPtr[i] + depCount[i] |
| 36 | + |
| 37 | + // Pass 2: fill colData |
| 38 | + const colData = new Int32Array(edges.length) |
| 39 | + const fillIdx = new Int32Array(N) |
| 40 | + |
| 41 | + for (const { packageId, dependsOnId } of edges) { |
| 42 | + const v = nodeIndex.get(dependsOnId)! |
| 43 | + colData[rowPtr[v] + fillIdx[v]++] = nodeIndex.get(packageId)! |
| 44 | + } |
| 45 | + |
| 46 | + return { nodeIndex, nodeIds, numDeps, rowPtr, colData, N } |
| 47 | +} |
| 48 | + |
| 49 | +export function computePageRank( |
| 50 | + { numDeps, rowPtr, colData, N }: Graph, |
| 51 | + damping = 0.85, |
| 52 | + maxIter = 100, |
| 53 | + convergence = 1e-6, |
| 54 | + onIteration?: (iter: number, delta: number) => void, |
| 55 | +): { scores: Float64Array; iterations: number } { |
| 56 | + const teleportation = (1 - damping) / N // base score every node gets regardless of links |
| 57 | + let scores = new Float64Array(N).fill(1 / N) // start: equal weight for all nodes |
| 58 | + let next = new Float64Array(N) // scratch buffer, swapped each iteration |
| 59 | + let iters = 0 |
| 60 | + |
| 61 | + for (iters = 1; iters <= maxIter; iters++) { |
| 62 | + |
| 63 | + // Every node starts with the teleportation floor |
| 64 | + next.fill(teleportation) |
| 65 | + |
| 66 | + // Each node v collects votes from packages that depend on it. |
| 67 | + // numDeps[dependent] is always >= 1 here — only packages with at least one |
| 68 | + // outgoing edge appear in colData, so division by zero cannot occur. |
| 69 | + // Dangling nodes (numDeps = 0) never appear in colData; their score |
| 70 | + // accumulates but never redistributes. This is acceptable because scores |
| 71 | + // are used for relative ranking via pct_rank(), not as absolute values. |
| 72 | + for (let v = 0; v < N; v++) { |
| 73 | + let incoming = 0 |
| 74 | + for (let j = rowPtr[v]; j < rowPtr[v + 1]; j++) { |
| 75 | + const dependent = colData[j] |
| 76 | + incoming += scores[dependent] / numDeps[dependent] |
| 77 | + } |
| 78 | + next[v] = teleportation + damping * incoming |
| 79 | + } |
| 80 | + |
| 81 | + // L1 delta: total change in scores across all nodes |
| 82 | + let delta = 0 |
| 83 | + for (let i = 0; i < N; i++) delta += Math.abs(next[i] - scores[i]) |
| 84 | + |
| 85 | + ;[scores, next] = [next, scores] // swap buffers — no data copy |
| 86 | + onIteration?.(iters, delta) |
| 87 | + |
| 88 | + if (delta < convergence) break // scores have stabilised |
| 89 | + } |
| 90 | + |
| 91 | + return { scores, iterations: iters } |
| 92 | +} |
| 93 | + |
| 94 | +export function getDependents(graph: Graph, packageId: number): number[] { |
| 95 | + const idx = graph.nodeIndex.get(packageId) |
| 96 | + if (idx === undefined) return [] |
| 97 | + const { rowPtr, colData, nodeIds } = graph |
| 98 | + const result: number[] = [] |
| 99 | + for (let j = rowPtr[idx]; j < rowPtr[idx + 1]; j++) { |
| 100 | + result.push(nodeIds[colData[j]]) |
| 101 | + } |
| 102 | + return result |
| 103 | +} |
0 commit comments