Files
pezkuwi-api/packages/metadata-builders/upstream-src/lookup-graph.ts
T
SatoshiQaziMuhammed e3c72817fb chore(papi): commit the sources behind the four polkadot-api packages (#15)
* chore(papi): commit the sources behind the four polkadot-api packages

papi-utils, bizinikiwi-bindings, metadata-builders and merkleize-metadata
held only dist/ builds, and no repository held their source. Every
source map in dist/ embeds the TypeScript it was compiled from; that is
now committed as src/ (59 files), and it matches upstream polkadot-api
(MIT) exactly once @polkadot-api/* imports are renamed:

  papi-utils           @polkadot-api/utils 0.2.0
  bizinikiwi-bindings  @polkadot-api/substrate-bindings 0.16.6
  metadata-builders    @polkadot-api/metadata-builders 0.13.0-0.13.2
  merkleize-metadata   @polkadot-api/merkleize-metadata 1.1.21-1.1.23

scripts/check-papi-sources.mjs, run in the lint job, fails unless the
source embedded in dist/ is exactly the committed src/. tsconfig.json
excludes these src/ trees; ESLint already skipped the packages. Each
README now states the upstream package and version.

* chore(papi): keep the upstream sources in upstream-src/

The pezkuwi-dev build treats any package with a src/ directory as its
own: it requires our license header on every file and builds it. These
are upstream files that must stay byte-identical to the source embedded
in dist/, so they live in upstream-src/ instead, which neither the build
nor the root tsconfig picks up.
2026-10-19 22:50:59 +04:00

215 lines
5.6 KiB
TypeScript

import { LookupEntry } from "./lookups"
export type LookupGraph = Map<
number,
{ entry: LookupEntry; backRefs: Set<number>; refs: Set<number> }
>
export function buildLookupGraph(
lookupFn: (id: number) => LookupEntry,
lookupLength: number,
): LookupGraph {
const result: LookupGraph = new Map()
const visited = new Set<number>()
const addEdge = (from: number, to: number) => {
if (!result.has(from))
result.set(from, {
entry: lookupFn(from),
backRefs: new Set(),
refs: new Set(),
})
if (!result.has(to))
result.set(to, {
entry: lookupFn(to),
backRefs: new Set(),
refs: new Set(),
})
result.get(from)!.refs.add(to)
result.get(to)!.backRefs.add(from)
}
for (let i = 0; i < lookupLength; i++) {
const entry = lookupFn(i)
if (i !== entry.id) {
// Lookup solved a pointer, but we still need to add that node into our
// graph in case someone access it from there.
addEdge(i, entry.id)
}
if (visited.has(entry.id)) continue
visited.add(entry.id)
switch (entry.type) {
case "array":
case "option":
case "sequence":
addEdge(entry.id, entry.value.id)
break
case "enum":
Object.values(entry.value).forEach((enumEntry) => {
switch (enumEntry.type) {
case "array":
case "lookupEntry":
addEdge(entry.id, enumEntry.value.id)
break
case "struct":
case "tuple":
Object.values(enumEntry.value).forEach((v) =>
addEdge(entry.id, v.id),
)
break
}
})
break
case "result":
addEdge(entry.id, entry.value.ok.id)
addEdge(entry.id, entry.value.ko.id)
break
case "struct":
case "tuple":
Object.values(entry.value).forEach((v) => addEdge(entry.id, v.id))
break
}
// It could be that this node is not being referenced by any other type
// nor it references anything. We still have to add it into the graph.
if (!result.has(entry.id)) {
result.set(entry.id, {
backRefs: new Set(),
refs: new Set(),
entry,
})
}
}
return result
}
const subgraphCache = new WeakMap<LookupGraph, Map<number, LookupGraph>>()
function _getSubgraph(
id: number,
graph: LookupGraph,
result: LookupGraph,
cache: Map<number, LookupGraph>,
) {
if (result.has(id)) return
const node = graph.get(id)!
result.set(id, node)
cache.set(id, result)
node.refs.forEach((ref) => _getSubgraph(ref, graph, result, cache))
node.backRefs.forEach((ref) => _getSubgraph(ref, graph, result, cache))
}
export function getSubgraph(id: number, graph: LookupGraph) {
if (!subgraphCache.has(graph)) {
subgraphCache.set(graph, new Map())
}
const cache = subgraphCache.get(graph)!
if (cache.has(id)) return cache.get(id)!
const result: LookupGraph = new Map()
_getSubgraph(id, graph, result, cache)
return result
}
export function getStronglyConnectedComponents(graph: LookupGraph) {
// Tarjan's strongly connected components
const tarjanState = new Map<
number,
{
index: number
lowLink: number
onStack: boolean
}
>()
let index = 0
const stack: number[] = []
const result: Array<Set<number>> = []
function strongConnect(v: number): void {
const state = {
index: index,
lowLink: index,
onStack: true,
}
tarjanState.set(v, state)
index++
stack.push(v)
const edges = graph.get(v)!.refs
for (let w of edges) {
const edgeState = tarjanState.get(w)
if (!edgeState) {
strongConnect(w)
state.lowLink = Math.min(state.lowLink, tarjanState.get(w)!.lowLink)
} else if (edgeState.onStack) {
state.lowLink = Math.min(state.lowLink, edgeState.index)
}
}
if (state.lowLink === state.index) {
const component = new Set<number>()
let poppedNode = -1
do {
poppedNode = stack.pop()!
tarjanState.get(poppedNode)!.onStack = false
component.add(poppedNode)
} while (poppedNode !== v)
result.push(component)
}
}
for (const node of graph.keys()) {
if (!tarjanState.has(node)) {
strongConnect(node)
}
}
return result
}
export function mergeSCCsWithCommonNodes(
stronglyConnectedComponents: Array<Set<number>>,
) {
/**
* For Nodes that are shared between two sets of SCCs, we need to calculate
* the checksum for the both of them, which wouldn't work (it would give
* different checksums).
* So we merge the SCCs that are using shared nodes into one group.
*/
const scc = stronglyConnectedComponents
const ungroupedCycles = new Set(scc.map((_, i) => i))
const edges = new Map(scc.map((_, i) => [i, new Set<number>()]))
scc.forEach((cycle, i) => {
scc.slice(i + 1).forEach((otherCycle, _j) => {
const j = _j + i + 1
const combined = new Set([...cycle, ...otherCycle])
if (combined.size !== cycle.size + otherCycle.size) {
edges.get(i)!.add(j)
edges.get(j)!.add(i)
}
})
})
const groups: Array<Set<number>> = []
while (ungroupedCycles.size) {
const group = new Set<number>()
const toVisit = [ungroupedCycles.values().next().value!]
while (toVisit.length) {
const idx = toVisit.pop()!
if (!ungroupedCycles.has(idx)) continue
ungroupedCycles.delete(idx)
const cycle = scc[idx]
cycle.forEach((v) => group.add(Number(v)))
edges.get(idx)!.forEach((n) => toVisit.push(n))
}
groups.push(group)
}
return groups
}