Files
Web_FreeCAD_Bitbybit/src/facade/dependencyGraph.ts

154 lines
6.0 KiB
TypeScript

export type DependencyRelation = 'link' | 'expression' | 'topo-ref' | 'container' | 'view'
export type DependencyEdge = {
sourceId: string
targetId: string
relation: DependencyRelation
propertyName?: string
reference?: string
}
export type RecomputePlan = {
affected: string[]
order: string[]
levels: string[][]
cycles: string[][]
}
export type RecomputeState = 'up-to-date' | 'touched' | 'recomputing' | 'error' | 'upstream-failed'
export type RecomputeSnapshot = {
generation: number
status: 'idle' | 'recomputing' | 'completed' | 'failed'
objectStates: Record<string, RecomputeState>
dirtyObjects: string[]
order: string[]
errors: Array<{ objectId: string; code: string; message: string }>
}
export class DependencyGraph {
private readonly nodes = new Set<string>()
private readonly outgoing = new Map<string, Set<string>>()
private readonly incoming = new Map<string, Set<string>>()
private readonly edgeList: DependencyEdge[] = []
constructor(edges: DependencyEdge[] = [], nodeIds: string[] = []) {
nodeIds.forEach((nodeId) => this.addNode(nodeId))
edges.forEach((edge) => this.addEdge(edge))
}
addNode(nodeId: string) {
this.nodes.add(nodeId)
if (!this.outgoing.has(nodeId)) this.outgoing.set(nodeId, new Set())
if (!this.incoming.has(nodeId)) this.incoming.set(nodeId, new Set())
}
addEdge(edge: DependencyEdge) {
if (!edge.sourceId || !edge.targetId) throw new RangeError('Dependency edge requires sourceId and targetId.')
this.addNode(edge.sourceId)
this.addNode(edge.targetId)
if (this.edgeList.some((candidate) => candidate.sourceId === edge.sourceId && candidate.targetId === edge.targetId && candidate.relation === edge.relation && candidate.propertyName === edge.propertyName)) return
this.edgeList.push({ ...edge })
this.outgoing.get(edge.sourceId)?.add(edge.targetId)
this.incoming.get(edge.targetId)?.add(edge.sourceId)
}
removeEdgesForProperty(sourceId: string, propertyName: string) {
const retained = this.edgeList.filter((edge) => edge.sourceId !== sourceId || edge.propertyName !== propertyName)
this.edgeList.length = 0
retained.forEach((edge) => this.edgeList.push(edge))
this.rebuildIndexes()
}
edges(): DependencyEdge[] { return this.edgeList.map((edge) => ({ ...edge })) }
dependenciesOf(sourceId: string): string[] { return [...(this.outgoing.get(sourceId) ?? [])] }
dependentsOf(targetId: string): string[] { return [...(this.incoming.get(targetId) ?? [])] }
plan(dirtyIds: Iterable<string>): RecomputePlan {
const affected = new Set<string>()
const visitDependents = (nodeId: string) => {
if (affected.has(nodeId)) return
affected.add(nodeId)
for (const dependent of this.dependentsOf(nodeId)) visitDependents(dependent)
}
for (const dirtyId of dirtyIds) if (this.nodes.has(dirtyId)) visitDependents(dirtyId)
const cycles = this.findCycles(affected)
const cyclic = new Set(cycles.flat())
const order: string[] = []
const visited = new Set<string>()
const visitDependencies = (nodeId: string) => {
if (visited.has(nodeId)) return
visited.add(nodeId)
for (const dependency of this.dependenciesOf(nodeId)) if (affected.has(dependency) && !cyclic.has(dependency)) visitDependencies(dependency)
if (!cyclic.has(nodeId)) order.push(nodeId)
}
for (const nodeId of affected) visitDependencies(nodeId)
const levelByNode = new Map<string, number>()
for (const nodeId of order) {
const level = Math.max(0, ...this.dependenciesOf(nodeId).filter((dependency) => affected.has(dependency)).map((dependency) => (levelByNode.get(dependency) ?? 0) + 1))
levelByNode.set(nodeId, level)
}
const levels: string[][] = []
for (const nodeId of order) { const level = levelByNode.get(nodeId) ?? 0; (levels[level] ??= []).push(nodeId) }
return { affected: [...affected], order, levels, cycles }
}
findCycles(scope: Set<string> = this.nodes): string[][] {
const cycles: string[][] = []
const indexByNode = new Map<string, number>()
const lowLinkByNode = new Map<string, number>()
const stack: string[] = []
const onStack = new Set<string>()
let index = 0
const strongConnect = (nodeId: string) => {
indexByNode.set(nodeId, index)
lowLinkByNode.set(nodeId, index)
index += 1
stack.push(nodeId)
onStack.add(nodeId)
for (const dependency of this.dependenciesOf(nodeId)) {
if (!scope.has(dependency)) continue
if (!indexByNode.has(dependency)) {
strongConnect(dependency)
lowLinkByNode.set(nodeId, Math.min(lowLinkByNode.get(nodeId) as number, lowLinkByNode.get(dependency) as number))
} else if (onStack.has(dependency)) {
lowLinkByNode.set(nodeId, Math.min(lowLinkByNode.get(nodeId) as number, indexByNode.get(dependency) as number))
}
}
if (lowLinkByNode.get(nodeId) !== indexByNode.get(nodeId)) return
const component: string[] = []
let candidate = ''
do {
candidate = stack.pop() as string
onStack.delete(candidate)
component.push(candidate)
} while (candidate !== nodeId)
if (component.length > 1 || this.dependenciesOf(nodeId).includes(nodeId)) {
const members = new Set(component)
cycles.push([...scope].filter((candidate) => members.has(candidate)))
}
}
for (const nodeId of scope) if (!indexByNode.has(nodeId)) strongConnect(nodeId)
return cycles
}
private rebuildIndexes() {
this.outgoing.clear()
this.incoming.clear()
for (const nodeId of this.nodes) { this.outgoing.set(nodeId, new Set()); this.incoming.set(nodeId, new Set()) }
for (const edge of this.edgeList) { this.outgoing.get(edge.sourceId)?.add(edge.targetId); this.incoming.get(edge.targetId)?.add(edge.sourceId) }
}
}
export const createRecomputeSnapshot = (objectIds: string[], generation = 0): RecomputeSnapshot => ({
generation,
status: 'idle',
objectStates: Object.fromEntries(objectIds.map((objectId) => [objectId, 'up-to-date'])),
dirtyObjects: [],
order: [],
errors: [],
})