P4: add expressions dependency recompute and topology signatures
This commit is contained in:
145
src/facade/dependencyGraph.ts
Normal file
145
src/facade/dependencyGraph.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
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[]
|
||||
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)
|
||||
return { affected: [...affected], order, 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: [],
|
||||
})
|
||||
Reference in New Issue
Block a user