dep_graph.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. /**
  2. * A simple dependency graph
  3. */
  4. /**
  5. * Helper for creating a Depth-First-Search on
  6. * a set of edges.
  7. *
  8. * Detects cycles and throws an Error if one is detected.
  9. *
  10. * @param edges The set of edges to DFS through
  11. * @param leavesOnly Whether to only return "leaf" nodes (ones who have no edges)
  12. * @param result An array in which the results will be populated
  13. * @param circular A boolean to allow circular dependencies
  14. */
  15. function createDFS(edges, leavesOnly, result, circular) {
  16. var currentPath = [];
  17. var visited = {};
  18. return function DFS(currentNode) {
  19. visited[currentNode] = true;
  20. currentPath.push(currentNode);
  21. edges[currentNode].forEach(function (node) {
  22. if (!visited[node]) {
  23. DFS(node);
  24. } else if (currentPath.indexOf(node) >= 0) {
  25. currentPath.push(node);
  26. if (!circular) {
  27. throw new Error('Dependency Cycle Found: ' + currentPath.join(' -> '));
  28. }
  29. }
  30. });
  31. currentPath.pop();
  32. if ((!leavesOnly || edges[currentNode].length === 0) && result.indexOf(currentNode) === -1) {
  33. result.push(currentNode);
  34. }
  35. };
  36. }
  37. /**
  38. * Simple Dependency Graph
  39. */
  40. var DepGraph = exports.DepGraph = function DepGraph(opts) {
  41. this.nodes = {}; // Node -> Node/Data (treated like a Set)
  42. this.outgoingEdges = {}; // Node -> [Dependency Node]
  43. this.incomingEdges = {}; // Node -> [Dependant Node]
  44. this.circular = opts && !!opts.circular; // Allows circular deps
  45. };
  46. DepGraph.prototype = {
  47. /**
  48. * The number of nodes in the graph.
  49. */
  50. size:function () {
  51. return Object.keys(this.nodes).length;
  52. },
  53. /**
  54. * Add a node to the dependency graph. If a node already exists, this method will do nothing.
  55. */
  56. addNode:function (node, data) {
  57. if (!this.hasNode(node)) {
  58. // Checking the arguments length allows the user to add a node with undefined data
  59. if (arguments.length === 2) {
  60. this.nodes[node] = data;
  61. } else {
  62. this.nodes[node] = node;
  63. }
  64. this.outgoingEdges[node] = [];
  65. this.incomingEdges[node] = [];
  66. }
  67. },
  68. /**
  69. * Remove a node from the dependency graph. If a node does not exist, this method will do nothing.
  70. */
  71. removeNode:function (node) {
  72. if (this.hasNode(node)) {
  73. delete this.nodes[node];
  74. delete this.outgoingEdges[node];
  75. delete this.incomingEdges[node];
  76. [this.incomingEdges, this.outgoingEdges].forEach(function (edgeList) {
  77. Object.keys(edgeList).forEach(function (key) {
  78. var idx = edgeList[key].indexOf(node);
  79. if (idx >= 0) {
  80. edgeList[key].splice(idx, 1);
  81. }
  82. }, this);
  83. });
  84. }
  85. },
  86. /**
  87. * Check if a node exists in the graph
  88. */
  89. hasNode:function (node) {
  90. return this.nodes.hasOwnProperty(node);
  91. },
  92. /**
  93. * Get the data associated with a node name
  94. */
  95. getNodeData:function (node) {
  96. if (this.hasNode(node)) {
  97. return this.nodes[node];
  98. } else {
  99. throw new Error('Node does not exist: ' + node);
  100. }
  101. },
  102. /**
  103. * Set the associated data for a given node name. If the node does not exist, this method will throw an error
  104. */
  105. setNodeData:function (node, data) {
  106. if (this.hasNode(node)) {
  107. this.nodes[node] = data;
  108. } else {
  109. throw new Error('Node does not exist: ' + node);
  110. }
  111. },
  112. /**
  113. * Add a dependency between two nodes. If either of the nodes does not exist,
  114. * an Error will be thrown.
  115. */
  116. addDependency:function (from, to) {
  117. if (!this.hasNode(from)) {
  118. throw new Error('Node does not exist: ' + from);
  119. }
  120. if (!this.hasNode(to)) {
  121. throw new Error('Node does not exist: ' + to);
  122. }
  123. if (this.outgoingEdges[from].indexOf(to) === -1) {
  124. this.outgoingEdges[from].push(to);
  125. }
  126. if (this.incomingEdges[to].indexOf(from) === -1) {
  127. this.incomingEdges[to].push(from);
  128. }
  129. return true;
  130. },
  131. /**
  132. * Remove a dependency between two nodes.
  133. */
  134. removeDependency:function (from, to) {
  135. var idx;
  136. if (this.hasNode(from)) {
  137. idx = this.outgoingEdges[from].indexOf(to);
  138. if (idx >= 0) {
  139. this.outgoingEdges[from].splice(idx, 1);
  140. }
  141. }
  142. if (this.hasNode(to)) {
  143. idx = this.incomingEdges[to].indexOf(from);
  144. if (idx >= 0) {
  145. this.incomingEdges[to].splice(idx, 1);
  146. }
  147. }
  148. },
  149. /**
  150. * Return a clone of the dependency graph. If any custom data is attached
  151. * to the nodes, it will only be shallow copied.
  152. */
  153. clone:function () {
  154. var source = this;
  155. var result = new DepGraph();
  156. var keys = Object.keys(source.nodes);
  157. keys.forEach(function (n) {
  158. result.nodes[n] = source.nodes[n];
  159. result.outgoingEdges[n] = source.outgoingEdges[n].slice(0);
  160. result.incomingEdges[n] = source.incomingEdges[n].slice(0);
  161. });
  162. return result;
  163. },
  164. /**
  165. * Get an array containing the nodes that the specified node depends on (transitively).
  166. *
  167. * Throws an Error if the graph has a cycle, or the specified node does not exist.
  168. *
  169. * If `leavesOnly` is true, only nodes that do not depend on any other nodes will be returned
  170. * in the array.
  171. */
  172. dependenciesOf:function (node, leavesOnly) {
  173. if (this.hasNode(node)) {
  174. var result = [];
  175. var DFS = createDFS(this.outgoingEdges, leavesOnly, result, this.circular);
  176. DFS(node);
  177. var idx = result.indexOf(node);
  178. if (idx >= 0) {
  179. result.splice(idx, 1);
  180. }
  181. return result;
  182. }
  183. else {
  184. throw new Error('Node does not exist: ' + node);
  185. }
  186. },
  187. /**
  188. * get an array containing the nodes that depend on the specified node (transitively).
  189. *
  190. * Throws an Error if the graph has a cycle, or the specified node does not exist.
  191. *
  192. * If `leavesOnly` is true, only nodes that do not have any dependants will be returned in the array.
  193. */
  194. dependantsOf:function (node, leavesOnly) {
  195. if (this.hasNode(node)) {
  196. var result = [];
  197. var DFS = createDFS(this.incomingEdges, leavesOnly, result, this.circular);
  198. DFS(node);
  199. var idx = result.indexOf(node);
  200. if (idx >= 0) {
  201. result.splice(idx, 1);
  202. }
  203. return result;
  204. } else {
  205. throw new Error('Node does not exist: ' + node);
  206. }
  207. },
  208. /**
  209. * Construct the overall processing order for the dependency graph.
  210. *
  211. * Throws an Error if the graph has a cycle.
  212. *
  213. * If `leavesOnly` is true, only nodes that do not depend on any other nodes will be returned.
  214. */
  215. overallOrder:function (leavesOnly) {
  216. var self = this;
  217. var result = [];
  218. var keys = Object.keys(this.nodes);
  219. if (keys.length === 0) {
  220. return result; // Empty graph
  221. } else {
  222. // Look for cycles - we run the DFS starting at all the nodes in case there
  223. // are several disconnected subgraphs inside this dependency graph.
  224. var CycleDFS = createDFS(this.outgoingEdges, false, [], this.circular);
  225. keys.forEach(function(n) {
  226. CycleDFS(n);
  227. });
  228. var DFS = createDFS(this.outgoingEdges, leavesOnly, result, this.circular);
  229. // Find all potential starting points (nodes with nothing depending on them) an
  230. // run a DFS starting at these points to get the order
  231. keys.filter(function (node) {
  232. return self.incomingEdges[node].length === 0;
  233. }).forEach(function (n) {
  234. DFS(n);
  235. });
  236. return result;
  237. }
  238. }
  239. };