this repo has no description
at v1.3.0 5.4 kB view raw
1import Logger from "./logger"; 2 3const logger = new Logger("core/util/dependency"); 4 5export type Dependency<T, D> = { 6 id: T; 7 data: D; 8}; 9type Dependencies<T, D> = Dependency<T, D>[]; 10type DependencyGraph<T> = Map<T, Set<T> | null>; 11 12type FetchDep<T, D> = (id: T) => D | null; 13type GetDeps<T, D> = (item: Dependency<T, D>) => T[]; 14type GetIncompatible<T, D> = (item: Dependency<T, D>) => T[]; 15type GetEnabled<T, D> = (item: Dependency<T, D>) => boolean; 16 17function buildDependencyGraph<T, D>( 18 origItems: Dependencies<T, D>, 19 { 20 fetchDep, 21 getDeps, 22 getIncompatible, 23 getEnabled 24 }: { 25 fetchDep: FetchDep<T, D>; 26 getDeps: GetDeps<T, D>; 27 getIncompatible?: GetIncompatible<T, D>; 28 getEnabled?: GetEnabled<T, D>; 29 } 30): [Dependencies<T, D>, DependencyGraph<T>] { 31 let items = [...origItems]; 32 const dependencyGraph: DependencyGraph<T> = new Map(); 33 34 for (const item of items) { 35 const fullDeps: Set<T> = new Set(); 36 let failed = false; 37 38 function resolveDeps(id: T, root: boolean) { 39 if (id === item.id && !root) { 40 logger.warn(`Circular dependency detected: "${item.id}"`); 41 failed = true; 42 return; 43 } 44 45 const obj = fetchDep(id); 46 if (obj == null) { 47 logger.warn(`Missing dependency detected`, id); 48 failed = true; 49 return; 50 } 51 52 if (!root) fullDeps.add(id); 53 54 for (const dep of getDeps({ id, data: obj })) { 55 resolveDeps(dep, false); 56 } 57 } 58 59 resolveDeps(item.id, true); 60 dependencyGraph.set(item.id, failed ? null : fullDeps); 61 } 62 63 logger.trace("Failed stage", items); 64 function isFailed(id: T) { 65 const deps = dependencyGraph.get(id); 66 if (deps === null) return true; 67 68 // For some reason this can be undefined. If it is, it's not null, so we 69 // didn't explicitly fail. FIXME too tired to investigate 70 if (deps === undefined) return false; 71 72 for (const dep of deps) { 73 if (isFailed(dep)) return true; 74 } 75 76 return false; 77 } 78 79 const failed = items.filter((item) => isFailed(item.id)); 80 if (failed.length > 0) { 81 logger.warn("Skipping failed items", failed); 82 items = items.filter((item) => !failed.includes(item)); 83 } 84 85 return [items, dependencyGraph]; 86} 87 88export default function calculateDependencies<T, D>( 89 origItems: Dependencies<T, D>, 90 { 91 fetchDep, 92 getDeps, 93 getIncompatible, 94 getEnabled 95 }: { 96 fetchDep: FetchDep<T, D>; 97 getDeps: GetDeps<T, D>; 98 getIncompatible?: GetIncompatible<T, D>; 99 getEnabled?: GetEnabled<T, D>; 100 } 101): [Dependencies<T, D>, DependencyGraph<T>] { 102 logger.trace("sortDependencies begin", origItems); 103 // eslint-disable-next-line prefer-const 104 let [itemsOrig, dependencyGraphOrig] = buildDependencyGraph(origItems, { 105 fetchDep, 106 getDeps, 107 getIncompatible, 108 getEnabled 109 }); 110 111 if (getEnabled != null) { 112 logger.trace("Enabled stage", itemsOrig); 113 const implicitlyEnabled: T[] = []; 114 115 function validateDeps(dep: Dependency<T, D>) { 116 if (getEnabled!(dep)) { 117 const deps = dependencyGraphOrig.get(dep.id)!; 118 for (const id of deps.values()) { 119 const data = fetchDep(id)!; 120 validateDeps({ id, data }); 121 } 122 } else { 123 const dependsOnMe = Array.from(dependencyGraphOrig.entries()).filter(([, v]) => v?.has(dep.id)); 124 125 if (dependsOnMe.length > 0) { 126 logger.debug("Implicitly enabling dependency", dep.id); 127 implicitlyEnabled.push(dep.id); 128 } 129 } 130 } 131 132 for (const dep of itemsOrig) validateDeps(dep); 133 itemsOrig = itemsOrig.filter((x) => getEnabled(x) || implicitlyEnabled.includes(x.id)); 134 } 135 136 if (getIncompatible != null) { 137 logger.trace("Incompatible stage", itemsOrig); 138 139 for (const item of itemsOrig) { 140 // JavaScript iterator moment 141 if (!itemsOrig.includes(item)) continue; 142 143 const incompatibleItems = getIncompatible(item); 144 for (const incompatibleItem of incompatibleItems) { 145 if (itemsOrig.find((x) => x.id === incompatibleItem) != null) { 146 logger.warn( 147 `Incompatible dependency detected: "${item.id}" and "${incompatibleItem}" - removing "${incompatibleItem}"` 148 ); 149 150 itemsOrig = itemsOrig.filter((x) => x.id !== incompatibleItem); 151 } 152 } 153 } 154 } 155 156 logger.trace("Verification stage", itemsOrig); 157 const [items, dependencyGraph] = buildDependencyGraph(itemsOrig, { 158 fetchDep, 159 getDeps, 160 getIncompatible, 161 getEnabled 162 }); 163 164 logger.trace("Sorting stage", items); 165 const sorted: Dependency<T, D>[] = []; 166 167 // Clone the dependency graph to return it later 168 const backupDependencyGraph = new Map(dependencyGraph); 169 for (const item of items) { 170 dependencyGraph.set(item.id, new Set(dependencyGraph.get(item.id))); 171 } 172 173 while (Array.from(dependencyGraph.values()).filter((x) => x != null).length > 0) { 174 const noDependents = items.filter((e) => dependencyGraph.get(e.id)?.size === 0); 175 176 if (noDependents.length === 0) { 177 logger.warn("Stuck dependency graph detected", dependencyGraph); 178 break; 179 } 180 181 for (const item of noDependents) { 182 sorted.push(item); 183 dependencyGraph.delete(item.id); 184 } 185 186 for (const deps of dependencyGraph.values()) { 187 for (const item of noDependents) { 188 deps?.delete(item.id); 189 } 190 } 191 } 192 193 logger.trace("sortDependencies end", sorted); 194 return [sorted, backupDependencyGraph]; 195}