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