import { EventEmitter } from 'events'; import Iterator from 'obliterator/iterator'; import take from 'obliterator/take'; import chain from 'obliterator/chain'; /** * Graphology Utilities * ===================== * * Collection of helpful functions used by the implementation. */ /** * Object.assign-like polyfill. * * @param {object} target - First object. * @param {object} [...objects] - Objects to merge. * @return {object} */ function assignPolyfill() { const target = arguments[0]; for (let i = 1, l = arguments.length; i < l; i++) { if (!arguments[i]) continue; for (const k in arguments[i]) target[k] = arguments[i][k]; } return target; } let assign = assignPolyfill; if (typeof Object.assign === 'function') assign = Object.assign; /** * Function returning the first matching edge for given path. * Note: this function does not check the existence of source & target. This * must be performed by the caller. * * @param {Graph} graph - Target graph. * @param {any} source - Source node. * @param {any} target - Target node. * @param {string} type - Type of the edge (mixed, directed or undirected). * @return {string|null} */ function getMatchingEdge(graph, source, target, type) { const sourceData = graph._nodes.get(source); let edge = null; if (!sourceData) return edge; if (type === 'mixed') { edge = (sourceData.out && sourceData.out[target]) || (sourceData.undirected && sourceData.undirected[target]); } else if (type === 'directed') { edge = sourceData.out && sourceData.out[target]; } else { edge = sourceData.undirected && sourceData.undirected[target]; } return edge; } /** * Checks whether the given value is a plain object. * * @param {mixed} value - Target value. * @return {boolean} */ function isPlainObject(value) { // NOTE: as per https://github.com/graphology/graphology/issues/149 // this function has been loosened not to reject object instances // coming from other JavaScript contexts. It has also been chosen // not to improve it to avoid obvious false positives and avoid // taking a performance hit. People should really use TypeScript // if they want to avoid feeding subtly irrelvant attribute objects. return typeof value === 'object' && value !== null; } /** * Checks whether the given object is empty. * * @param {object} o - Target Object. * @return {boolean} */ function isEmpty(o) { let k; for (k in o) return false; return true; } /** * Creates a "private" property for the given member name by concealing it * using the `enumerable` option. * * @param {object} target - Target object. * @param {string} name - Member name. */ function privateProperty(target, name, value) { Object.defineProperty(target, name, { enumerable: false, configurable: false, writable: true, value }); } /** * Creates a read-only property for the given member name & the given getter. * * @param {object} target - Target object. * @param {string} name - Member name. * @param {mixed} value - The attached getter or fixed value. */ function readOnlyProperty(target, name, value) { const descriptor = { enumerable: true, configurable: true }; if (typeof value === 'function') { descriptor.get = value; } else { descriptor.value = value; descriptor.writable = false; } Object.defineProperty(target, name, descriptor); } /** * Returns whether the given object constitute valid hints. * * @param {object} hints - Target object. */ function validateHints(hints) { if (!isPlainObject(hints)) return false; if (hints.attributes && !Array.isArray(hints.attributes)) return false; return true; } /** * Creates a function generating incremental ids for edges. * * @return {function} */ function incrementalIdStartingFromRandomByte() { let i = Math.floor(Math.random() * 256) & 0xff; return () => { return i++; }; } /** * Graphology Custom Errors * ========================= * * Defining custom errors for ease of use & easy unit tests across * implementations (normalized typology rather than relying on error * messages to check whether the correct error was found). */ class GraphError extends Error { constructor(message) { super(); this.name = 'GraphError'; this.message = message; } } class InvalidArgumentsGraphError extends GraphError { constructor(message) { super(message); this.name = 'InvalidArgumentsGraphError'; // This is V8 specific to enhance stack readability if (typeof Error.captureStackTrace === 'function') Error.captureStackTrace( this, InvalidArgumentsGraphError.prototype.constructor ); } } class NotFoundGraphError extends GraphError { constructor(message) { super(message); this.name = 'NotFoundGraphError'; // This is V8 specific to enhance stack readability if (typeof Error.captureStackTrace === 'function') Error.captureStackTrace(this, NotFoundGraphError.prototype.constructor); } } class UsageGraphError extends GraphError { constructor(message) { super(message); this.name = 'UsageGraphError'; // This is V8 specific to enhance stack readability if (typeof Error.captureStackTrace === 'function') Error.captureStackTrace(this, UsageGraphError.prototype.constructor); } } /** * Graphology Internal Data Classes * ================================= * * Internal classes hopefully reduced to structs by engines & storing * necessary information for nodes & edges. * * Note that those classes don't rely on the `class` keyword to avoid some * cruft introduced by most of ES2015 transpilers. */ /** * MixedNodeData class. * * @constructor * @param {string} string - The node's key. * @param {object} attributes - Node's attributes. */ function MixedNodeData(key, attributes) { // Attributes this.key = key; this.attributes = attributes; this.clear(); } MixedNodeData.prototype.clear = function () { // Degrees this.inDegree = 0; this.outDegree = 0; this.undirectedDegree = 0; this.undirectedLoops = 0; this.directedLoops = 0; // Indices this.in = {}; this.out = {}; this.undirected = {}; }; /** * DirectedNodeData class. * * @constructor * @param {string} string - The node's key. * @param {object} attributes - Node's attributes. */ function DirectedNodeData(key, attributes) { // Attributes this.key = key; this.attributes = attributes; this.clear(); } DirectedNodeData.prototype.clear = function () { // Degrees this.inDegree = 0; this.outDegree = 0; this.directedLoops = 0; // Indices this.in = {}; this.out = {}; }; /** * UndirectedNodeData class. * * @constructor * @param {string} string - The node's key. * @param {object} attributes - Node's attributes. */ function UndirectedNodeData(key, attributes) { // Attributes this.key = key; this.attributes = attributes; this.clear(); } UndirectedNodeData.prototype.clear = function () { // Degrees this.undirectedDegree = 0; this.undirectedLoops = 0; // Indices this.undirected = {}; }; /** * EdgeData class. * * @constructor * @param {boolean} undirected - Whether the edge is undirected. * @param {string} string - The edge's key. * @param {string} source - Source of the edge. * @param {string} target - Target of the edge. * @param {object} attributes - Edge's attributes. */ function EdgeData(undirected, key, source, target, attributes) { // Attributes this.key = key; this.attributes = attributes; this.undirected = undirected; // Extremities this.source = source; this.target = target; } EdgeData.prototype.attach = function () { let outKey = 'out'; let inKey = 'in'; if (this.undirected) outKey = inKey = 'undirected'; const source = this.source.key; const target = this.target.key; // Handling source this.source[outKey][target] = this; if (this.undirected && source === target) return; // Handling target this.target[inKey][source] = this; }; EdgeData.prototype.attachMulti = function () { let outKey = 'out'; let inKey = 'in'; const source = this.source.key; const target = this.target.key; if (this.undirected) outKey = inKey = 'undirected'; // Handling source const adj = this.source[outKey]; const head = adj[target]; if (typeof head === 'undefined') { adj[target] = this; // Self-loop optimization if (!(this.undirected && source === target)) { // Handling target this.target[inKey][source] = this; } return; } // Prepending to doubly-linked list head.previous = this; this.next = head; // Pointing to new head // NOTE: use mutating swap later to avoid lookup? adj[target] = this; this.target[inKey][source] = this; }; EdgeData.prototype.detach = function () { const source = this.source.key; const target = this.target.key; let outKey = 'out'; let inKey = 'in'; if (this.undirected) outKey = inKey = 'undirected'; delete this.source[outKey][target]; // No-op delete in case of undirected self-loop delete this.target[inKey][source]; }; EdgeData.prototype.detachMulti = function () { const source = this.source.key; const target = this.target.key; let outKey = 'out'; let inKey = 'in'; if (this.undirected) outKey = inKey = 'undirected'; // Deleting from doubly-linked list if (this.previous === undefined) { // We are dealing with the head // Should we delete the adjacency entry because it is now empty? if (this.next === undefined) { delete this.source[outKey][target]; // No-op delete in case of undirected self-loop delete this.target[inKey][source]; } else { // Detaching this.next.previous = undefined; // NOTE: could avoid the lookups by creating a #.become mutating method this.source[outKey][target] = this.next; // No-op delete in case of undirected self-loop this.target[inKey][source] = this.next; } } else { // We are dealing with another list node this.previous.next = this.next; // If not last if (this.next !== undefined) { this.next.previous = this.previous; } } }; /** * Graphology Node Attributes methods * =================================== */ const NODE = 0; const SOURCE = 1; const TARGET = 2; const OPPOSITE = 3; function findRelevantNodeData( graph, method, mode, nodeOrEdge, nameOrEdge, add1, add2 ) { let nodeData, edgeData, arg1, arg2; nodeOrEdge = '' + nodeOrEdge; if (mode === NODE) { nodeData = graph._nodes.get(nodeOrEdge); if (!nodeData) throw new NotFoundGraphError( `Graph.${method}: could not find the "${nodeOrEdge}" node in the graph.` ); arg1 = nameOrEdge; arg2 = add1; } else if (mode === OPPOSITE) { nameOrEdge = '' + nameOrEdge; edgeData = graph._edges.get(nameOrEdge); if (!edgeData) throw new NotFoundGraphError( `Graph.${method}: could not find the "${nameOrEdge}" edge in the graph.` ); const source = edgeData.source.key; const target = edgeData.target.key; if (nodeOrEdge === source) { nodeData = edgeData.target; } else if (nodeOrEdge === target) { nodeData = edgeData.source; } else { throw new NotFoundGraphError( `Graph.${method}: the "${nodeOrEdge}" node is not attached to the "${nameOrEdge}" edge (${source}, ${target}).` ); } arg1 = add1; arg2 = add2; } else { edgeData = graph._edges.get(nodeOrEdge); if (!edgeData) throw new NotFoundGraphError( `Graph.${method}: could not find the "${nodeOrEdge}" edge in the graph.` ); if (mode === SOURCE) { nodeData = edgeData.source; } else { nodeData = edgeData.target; } arg1 = nameOrEdge; arg2 = add1; } return [nodeData, arg1, arg2]; } function attachNodeAttributeGetter(Class, method, mode) { Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) { const [data, name] = findRelevantNodeData( this, method, mode, nodeOrEdge, nameOrEdge, add1 ); return data.attributes[name]; }; } function attachNodeAttributesGetter(Class, method, mode) { Class.prototype[method] = function (nodeOrEdge, nameOrEdge) { const [data] = findRelevantNodeData( this, method, mode, nodeOrEdge, nameOrEdge ); return data.attributes; }; } function attachNodeAttributeChecker(Class, method, mode) { Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) { const [data, name] = findRelevantNodeData( this, method, mode, nodeOrEdge, nameOrEdge, add1 ); return data.attributes.hasOwnProperty(name); }; } function attachNodeAttributeSetter(Class, method, mode) { Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1, add2) { const [data, name, value] = findRelevantNodeData( this, method, mode, nodeOrEdge, nameOrEdge, add1, add2 ); data.attributes[name] = value; // Emitting this.emit('nodeAttributesUpdated', { key: data.key, type: 'set', attributes: data.attributes, name }); return this; }; } function attachNodeAttributeUpdater(Class, method, mode) { Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1, add2) { const [data, name, updater] = findRelevantNodeData( this, method, mode, nodeOrEdge, nameOrEdge, add1, add2 ); if (typeof updater !== 'function') throw new InvalidArgumentsGraphError( `Graph.${method}: updater should be a function.` ); const attributes = data.attributes; const value = updater(attributes[name]); attributes[name] = value; // Emitting this.emit('nodeAttributesUpdated', { key: data.key, type: 'set', attributes: data.attributes, name }); return this; }; } function attachNodeAttributeRemover(Class, method, mode) { Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) { const [data, name] = findRelevantNodeData( this, method, mode, nodeOrEdge, nameOrEdge, add1 ); delete data.attributes[name]; // Emitting this.emit('nodeAttributesUpdated', { key: data.key, type: 'remove', attributes: data.attributes, name }); return this; }; } function attachNodeAttributesReplacer(Class, method, mode) { Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) { const [data, attributes] = findRelevantNodeData( this, method, mode, nodeOrEdge, nameOrEdge, add1 ); if (!isPlainObject(attributes)) throw new InvalidArgumentsGraphError( `Graph.${method}: provided attributes are not a plain object.` ); data.attributes = attributes; // Emitting this.emit('nodeAttributesUpdated', { key: data.key, type: 'replace', attributes: data.attributes }); return this; }; } function attachNodeAttributesMerger(Class, method, mode) { Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) { const [data, attributes] = findRelevantNodeData( this, method, mode, nodeOrEdge, nameOrEdge, add1 ); if (!isPlainObject(attributes)) throw new InvalidArgumentsGraphError( `Graph.${method}: provided attributes are not a plain object.` ); assign(data.attributes, attributes); // Emitting this.emit('nodeAttributesUpdated', { key: data.key, type: 'merge', attributes: data.attributes, data: attributes }); return this; }; } function attachNodeAttributesUpdater(Class, method, mode) { Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) { const [data, updater] = findRelevantNodeData( this, method, mode, nodeOrEdge, nameOrEdge, add1 ); if (typeof updater !== 'function') throw new InvalidArgumentsGraphError( `Graph.${method}: provided updater is not a function.` ); data.attributes = updater(data.attributes); // Emitting this.emit('nodeAttributesUpdated', { key: data.key, type: 'update', attributes: data.attributes }); return this; }; } /** * List of methods to attach. */ const NODE_ATTRIBUTES_METHODS = [ { name: element => `get${element}Attribute`, attacher: attachNodeAttributeGetter }, { name: element => `get${element}Attributes`, attacher: attachNodeAttributesGetter }, { name: element => `has${element}Attribute`, attacher: attachNodeAttributeChecker }, { name: element => `set${element}Attribute`, attacher: attachNodeAttributeSetter }, { name: element => `update${element}Attribute`, attacher: attachNodeAttributeUpdater }, { name: element => `remove${element}Attribute`, attacher: attachNodeAttributeRemover }, { name: element => `replace${element}Attributes`, attacher: attachNodeAttributesReplacer }, { name: element => `merge${element}Attributes`, attacher: attachNodeAttributesMerger }, { name: element => `update${element}Attributes`, attacher: attachNodeAttributesUpdater } ]; /** * Attach every attributes-related methods to a Graph class. * * @param {function} Graph - Target class. */ function attachNodeAttributesMethods(Graph) { NODE_ATTRIBUTES_METHODS.forEach(function ({name, attacher}) { // For nodes attacher(Graph, name('Node'), NODE); // For sources attacher(Graph, name('Source'), SOURCE); // For targets attacher(Graph, name('Target'), TARGET); // For opposites attacher(Graph, name('Opposite'), OPPOSITE); }); } /** * Graphology Edge Attributes methods * =================================== */ /** * Attach an attribute getter method onto the provided class. * * @param {function} Class - Target class. * @param {string} method - Method name. * @param {string} type - Type of the edge to find. */ function attachEdgeAttributeGetter(Class, method, type) { /** * Get the desired attribute for the given element (node or edge). * * Arity 2: * @param {any} element - Target element. * @param {string} name - Attribute's name. * * Arity 3 (only for edges): * @param {any} source - Source element. * @param {any} target - Target element. * @param {string} name - Attribute's name. * * @return {mixed} - The attribute's value. * * @throws {Error} - Will throw if too many arguments are provided. * @throws {Error} - Will throw if any of the elements is not found. */ Class.prototype[method] = function (element, name) { let data; if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError( `Graph.${method}: cannot find this type of edges in your ${this.type} graph.` ); if (arguments.length > 2) { if (this.multi) throw new UsageGraphError( `Graph.${method}: cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about.` ); const source = '' + element; const target = '' + name; name = arguments[2]; data = getMatchingEdge(this, source, target, type); if (!data) throw new NotFoundGraphError( `Graph.${method}: could not find an edge for the given path ("${source}" - "${target}").` ); } else { if (type !== 'mixed') throw new UsageGraphError( `Graph.${method}: calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type.` ); element = '' + element; data = this._edges.get(element); if (!data) throw new NotFoundGraphError( `Graph.${method}: could not find the "${element}" edge in the graph.` ); } return data.attributes[name]; }; } /** * Attach an attributes getter method onto the provided class. * * @param {function} Class - Target class. * @param {string} method - Method name. * @param {string} type - Type of the edge to find. */ function attachEdgeAttributesGetter(Class, method, type) { /** * Retrieves all the target element's attributes. * * Arity 2: * @param {any} element - Target element. * * Arity 3 (only for edges): * @param {any} source - Source element. * @param {any} target - Target element. * * @return {object} - The element's attributes. * * @throws {Error} - Will throw if too many arguments are provided. * @throws {Error} - Will throw if any of the elements is not found. */ Class.prototype[method] = function (element) { let data; if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError( `Graph.${method}: cannot find this type of edges in your ${this.type} graph.` ); if (arguments.length > 1) { if (this.multi) throw new UsageGraphError( `Graph.${method}: cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about.` ); const source = '' + element, target = '' + arguments[1]; data = getMatchingEdge(this, source, target, type); if (!data) throw new NotFoundGraphError( `Graph.${method}: could not find an edge for the given path ("${source}" - "${target}").` ); } else { if (type !== 'mixed') throw new UsageGraphError( `Graph.${method}: calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type.` ); element = '' + element; data = this._edges.get(element); if (!data) throw new NotFoundGraphError( `Graph.${method}: could not find the "${element}" edge in the graph.` ); } return data.attributes; }; } /** * Attach an attribute checker method onto the provided class. * * @param {function} Class - Target class. * @param {string} method - Method name. * @param {string} type - Type of the edge to find. */ function attachEdgeAttributeChecker(Class, method, type) { /** * Checks whether the desired attribute is set for the given element (node or edge). * * Arity 2: * @param {any} element - Target element. * @param {string} name - Attribute's name. * * Arity 3 (only for edges): * @param {any} source - Source element. * @param {any} target - Target element. * @param {string} name - Attribute's name. * * @return {boolean} * * @throws {Error} - Will throw if too many arguments are provided. * @throws {Error} - Will throw if any of the elements is not found. */ Class.prototype[method] = function (element, name) { let data; if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError( `Graph.${method}: cannot find this type of edges in your ${this.type} graph.` ); if (arguments.length > 2) { if (this.multi) throw new UsageGraphError( `Graph.${method}: cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about.` ); const source = '' + element; const target = '' + name; name = arguments[2]; data = getMatchingEdge(this, source, target, type); if (!data) throw new NotFoundGraphError( `Graph.${method}: could not find an edge for the given path ("${source}" - "${target}").` ); } else { if (type !== 'mixed') throw new UsageGraphError( `Graph.${method}: calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type.` ); element = '' + element; data = this._edges.get(element); if (!data) throw new NotFoundGraphError( `Graph.${method}: could not find the "${element}" edge in the graph.` ); } return data.attributes.hasOwnProperty(name); }; } /** * Attach an attribute setter method onto the provided class. * * @param {function} Class - Target class. * @param {string} method - Method name. * @param {string} type - Type of the edge to find. */ function attachEdgeAttributeSetter(Class, method, type) { /** * Set the desired attribute for the given element (node or edge). * * Arity 2: * @param {any} element - Target element. * @param {string} name - Attribute's name. * @param {mixed} value - New attribute value. * * Arity 3 (only for edges): * @param {any} source - Source element. * @param {any} target - Target element. * @param {string} name - Attribute's name. * @param {mixed} value - New attribute value. * * @return {Graph} - Returns itself for chaining. * * @throws {Error} - Will throw if too many arguments are provided. * @throws {Error} - Will throw if any of the elements is not found. */ Class.prototype[method] = function (element, name, value) { let data; if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError( `Graph.${method}: cannot find this type of edges in your ${this.type} graph.` ); if (arguments.length > 3) { if (this.multi) throw new UsageGraphError( `Graph.${method}: cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about.` ); const source = '' + element; const target = '' + name; name = arguments[2]; value = arguments[3]; data = getMatchingEdge(this, source, target, type); if (!data) throw new NotFoundGraphError( `Graph.${method}: could not find an edge for the given path ("${source}" - "${target}").` ); } else { if (type !== 'mixed') throw new UsageGraphError( `Graph.${method}: calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type.` ); element = '' + element; data = this._edges.get(element); if (!data) throw new NotFoundGraphError( `Graph.${method}: could not find the "${element}" edge in the graph.` ); } data.attributes[name] = value; // Emitting this.emit('edgeAttributesUpdated', { key: data.key, type: 'set', attributes: data.attributes, name }); return this; }; } /** * Attach an attribute updater method onto the provided class. * * @param {function} Class - Target class. * @param {string} method - Method name. * @param {string} type - Type of the edge to find. */ function attachEdgeAttributeUpdater(Class, method, type) { /** * Update the desired attribute for the given element (node or edge) using * the provided function. * * Arity 2: * @param {any} element - Target element. * @param {string} name - Attribute's name. * @param {function} updater - Updater function. * * Arity 3 (only for edges): * @param {any} source - Source element. * @param {any} target - Target element. * @param {string} name - Attribute's name. * @param {function} updater - Updater function. * * @return {Graph} - Returns itself for chaining. * * @throws {Error} - Will throw if too many arguments are provided. * @throws {Error} - Will throw if any of the elements is not found. */ Class.prototype[method] = function (element, name, updater) { let data; if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError( `Graph.${method}: cannot find this type of edges in your ${this.type} graph.` ); if (arguments.length > 3) { if (this.multi) throw new UsageGraphError( `Graph.${method}: cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about.` ); const source = '' + element; const target = '' + name; name = arguments[2]; updater = arguments[3]; data = getMatchingEdge(this, source, target, type); if (!data) throw new NotFoundGraphError( `Graph.${method}: could not find an edge for the given path ("${source}" - "${target}").` ); } else { if (type !== 'mixed') throw new UsageGraphError( `Graph.${method}: calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type.` ); element = '' + element; data = this._edges.get(element); if (!data) throw new NotFoundGraphError( `Graph.${method}: could not find the "${element}" edge in the graph.` ); } if (typeof updater !== 'function') throw new InvalidArgumentsGraphError( `Graph.${method}: updater should be a function.` ); data.attributes[name] = updater(data.attributes[name]); // Emitting this.emit('edgeAttributesUpdated', { key: data.key, type: 'set', attributes: data.attributes, name }); return this; }; } /** * Attach an attribute remover method onto the provided class. * * @param {function} Class - Target class. * @param {string} method - Method name. * @param {string} type - Type of the edge to find. */ function attachEdgeAttributeRemover(Class, method, type) { /** * Remove the desired attribute for the given element (node or edge). * * Arity 2: * @param {any} element - Target element. * @param {string} name - Attribute's name. * * Arity 3 (only for edges): * @param {any} source - Source element. * @param {any} target - Target element. * @param {string} name - Attribute's name. * * @return {Graph} - Returns itself for chaining. * * @throws {Error} - Will throw if too many arguments are provided. * @throws {Error} - Will throw if any of the elements is not found. */ Class.prototype[method] = function (element, name) { let data; if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError( `Graph.${method}: cannot find this type of edges in your ${this.type} graph.` ); if (arguments.length > 2) { if (this.multi) throw new UsageGraphError( `Graph.${method}: cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about.` ); const source = '' + element; const target = '' + name; name = arguments[2]; data = getMatchingEdge(this, source, target, type); if (!data) throw new NotFoundGraphError( `Graph.${method}: could not find an edge for the given path ("${source}" - "${target}").` ); } else { if (type !== 'mixed') throw new UsageGraphError( `Graph.${method}: calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type.` ); element = '' + element; data = this._edges.get(element); if (!data) throw new NotFoundGraphError( `Graph.${method}: could not find the "${element}" edge in the graph.` ); } delete data.attributes[name]; // Emitting this.emit('edgeAttributesUpdated', { key: data.key, type: 'remove', attributes: data.attributes, name }); return this; }; } /** * Attach an attribute replacer method onto the provided class. * * @param {function} Class - Target class. * @param {string} method - Method name. * @param {string} type - Type of the edge to find. */ function attachEdgeAttributesReplacer(Class, method, type) { /** * Replace the attributes for the given element (node or edge). * * Arity 2: * @param {any} element - Target element. * @param {object} attributes - New attributes. * * Arity 3 (only for edges): * @param {any} source - Source element. * @param {any} target - Target element. * @param {object} attributes - New attributes. * * @return {Graph} - Returns itself for chaining. * * @throws {Error} - Will throw if too many arguments are provided. * @throws {Error} - Will throw if any of the elements is not found. */ Class.prototype[method] = function (element, attributes) { let data; if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError( `Graph.${method}: cannot find this type of edges in your ${this.type} graph.` ); if (arguments.length > 2) { if (this.multi) throw new UsageGraphError( `Graph.${method}: cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about.` ); const source = '' + element, target = '' + attributes; attributes = arguments[2]; data = getMatchingEdge(this, source, target, type); if (!data) throw new NotFoundGraphError( `Graph.${method}: could not find an edge for the given path ("${source}" - "${target}").` ); } else { if (type !== 'mixed') throw new UsageGraphError( `Graph.${method}: calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type.` ); element = '' + element; data = this._edges.get(element); if (!data) throw new NotFoundGraphError( `Graph.${method}: could not find the "${element}" edge in the graph.` ); } if (!isPlainObject(attributes)) throw new InvalidArgumentsGraphError( `Graph.${method}: provided attributes are not a plain object.` ); data.attributes = attributes; // Emitting this.emit('edgeAttributesUpdated', { key: data.key, type: 'replace', attributes: data.attributes }); return this; }; } /** * Attach an attribute merger method onto the provided class. * * @param {function} Class - Target class. * @param {string} method - Method name. * @param {string} type - Type of the edge to find. */ function attachEdgeAttributesMerger(Class, method, type) { /** * Merge the attributes for the given element (node or edge). * * Arity 2: * @param {any} element - Target element. * @param {object} attributes - Attributes to merge. * * Arity 3 (only for edges): * @param {any} source - Source element. * @param {any} target - Target element. * @param {object} attributes - Attributes to merge. * * @return {Graph} - Returns itself for chaining. * * @throws {Error} - Will throw if too many arguments are provided. * @throws {Error} - Will throw if any of the elements is not found. */ Class.prototype[method] = function (element, attributes) { let data; if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError( `Graph.${method}: cannot find this type of edges in your ${this.type} graph.` ); if (arguments.length > 2) { if (this.multi) throw new UsageGraphError( `Graph.${method}: cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about.` ); const source = '' + element, target = '' + attributes; attributes = arguments[2]; data = getMatchingEdge(this, source, target, type); if (!data) throw new NotFoundGraphError( `Graph.${method}: could not find an edge for the given path ("${source}" - "${target}").` ); } else { if (type !== 'mixed') throw new UsageGraphError( `Graph.${method}: calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type.` ); element = '' + element; data = this._edges.get(element); if (!data) throw new NotFoundGraphError( `Graph.${method}: could not find the "${element}" edge in the graph.` ); } if (!isPlainObject(attributes)) throw new InvalidArgumentsGraphError( `Graph.${method}: provided attributes are not a plain object.` ); assign(data.attributes, attributes); // Emitting this.emit('edgeAttributesUpdated', { key: data.key, type: 'merge', attributes: data.attributes, data: attributes }); return this; }; } /** * Attach an attribute updater method onto the provided class. * * @param {function} Class - Target class. * @param {string} method - Method name. * @param {string} type - Type of the edge to find. */ function attachEdgeAttributesUpdater(Class, method, type) { /** * Update the attributes of the given element (node or edge). * * Arity 2: * @param {any} element - Target element. * @param {function} updater - Updater function. * * Arity 3 (only for edges): * @param {any} source - Source element. * @param {any} target - Target element. * @param {function} updater - Updater function. * * @return {Graph} - Returns itself for chaining. * * @throws {Error} - Will throw if too many arguments are provided. * @throws {Error} - Will throw if any of the elements is not found. */ Class.prototype[method] = function (element, updater) { let data; if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError( `Graph.${method}: cannot find this type of edges in your ${this.type} graph.` ); if (arguments.length > 2) { if (this.multi) throw new UsageGraphError( `Graph.${method}: cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about.` ); const source = '' + element, target = '' + updater; updater = arguments[2]; data = getMatchingEdge(this, source, target, type); if (!data) throw new NotFoundGraphError( `Graph.${method}: could not find an edge for the given path ("${source}" - "${target}").` ); } else { if (type !== 'mixed') throw new UsageGraphError( `Graph.${method}: calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type.` ); element = '' + element; data = this._edges.get(element); if (!data) throw new NotFoundGraphError( `Graph.${method}: could not find the "${element}" edge in the graph.` ); } if (typeof updater !== 'function') throw new InvalidArgumentsGraphError( `Graph.${method}: provided updater is not a function.` ); data.attributes = updater(data.attributes); // Emitting this.emit('edgeAttributesUpdated', { key: data.key, type: 'update', attributes: data.attributes }); return this; }; } /** * List of methods to attach. */ const EDGE_ATTRIBUTES_METHODS = [ { name: element => `get${element}Attribute`, attacher: attachEdgeAttributeGetter }, { name: element => `get${element}Attributes`, attacher: attachEdgeAttributesGetter }, { name: element => `has${element}Attribute`, attacher: attachEdgeAttributeChecker }, { name: element => `set${element}Attribute`, attacher: attachEdgeAttributeSetter }, { name: element => `update${element}Attribute`, attacher: attachEdgeAttributeUpdater }, { name: element => `remove${element}Attribute`, attacher: attachEdgeAttributeRemover }, { name: element => `replace${element}Attributes`, attacher: attachEdgeAttributesReplacer }, { name: element => `merge${element}Attributes`, attacher: attachEdgeAttributesMerger }, { name: element => `update${element}Attributes`, attacher: attachEdgeAttributesUpdater } ]; /** * Attach every attributes-related methods to a Graph class. * * @param {function} Graph - Target class. */ function attachEdgeAttributesMethods(Graph) { EDGE_ATTRIBUTES_METHODS.forEach(function ({name, attacher}) { // For edges attacher(Graph, name('Edge'), 'mixed'); // For directed edges attacher(Graph, name('DirectedEdge'), 'directed'); // For undirected edges attacher(Graph, name('UndirectedEdge'), 'undirected'); }); } /** * Graphology Edge Iteration * ========================== * * Attaching some methods to the Graph class to be able to iterate over a * graph's edges. */ /** * Definitions. */ const EDGES_ITERATION = [ { name: 'edges', type: 'mixed' }, { name: 'inEdges', type: 'directed', direction: 'in' }, { name: 'outEdges', type: 'directed', direction: 'out' }, { name: 'inboundEdges', type: 'mixed', direction: 'in' }, { name: 'outboundEdges', type: 'mixed', direction: 'out' }, { name: 'directedEdges', type: 'directed' }, { name: 'undirectedEdges', type: 'undirected' } ]; /** * Function iterating over edges from the given object to match one of them. * * @param {object} object - Target object. * @param {function} callback - Function to call. */ function forEachSimple(breakable, object, callback, avoid) { let shouldBreak = false; for (const k in object) { if (k === avoid) continue; const edgeData = object[k]; shouldBreak = callback( edgeData.key, edgeData.attributes, edgeData.source.key, edgeData.target.key, edgeData.source.attributes, edgeData.target.attributes, edgeData.undirected ); if (breakable && shouldBreak) return edgeData.key; } return; } function forEachMulti(breakable, object, callback, avoid) { let edgeData, source, target; let shouldBreak = false; for (const k in object) { if (k === avoid) continue; edgeData = object[k]; do { source = edgeData.source; target = edgeData.target; shouldBreak = callback( edgeData.key, edgeData.attributes, source.key, target.key, source.attributes, target.attributes, edgeData.undirected ); if (breakable && shouldBreak) return edgeData.key; edgeData = edgeData.next; } while (edgeData !== undefined); } return; } /** * Function returning an iterator over edges from the given object. * * @param {object} object - Target object. * @return {Iterator} */ function createIterator(object, avoid) { const keys = Object.keys(object); const l = keys.length; let edgeData; let i = 0; return new Iterator(function next() { do { if (!edgeData) { if (i >= l) return {done: true}; const k = keys[i++]; if (k === avoid) { edgeData = undefined; continue; } edgeData = object[k]; } else { edgeData = edgeData.next; } } while (!edgeData); return { done: false, value: { edge: edgeData.key, attributes: edgeData.attributes, source: edgeData.source.key, target: edgeData.target.key, sourceAttributes: edgeData.source.attributes, targetAttributes: edgeData.target.attributes, undirected: edgeData.undirected } }; }); } /** * Function iterating over the egdes from the object at given key to match * one of them. * * @param {object} object - Target object. * @param {mixed} k - Neighbor key. * @param {function} callback - Callback to use. */ function forEachForKeySimple(breakable, object, k, callback) { const edgeData = object[k]; if (!edgeData) return; const sourceData = edgeData.source; const targetData = edgeData.target; if ( callback( edgeData.key, edgeData.attributes, sourceData.key, targetData.key, sourceData.attributes, targetData.attributes, edgeData.undirected ) && breakable ) return edgeData.key; } function forEachForKeyMulti(breakable, object, k, callback) { let edgeData = object[k]; if (!edgeData) return; let shouldBreak = false; do { shouldBreak = callback( edgeData.key, edgeData.attributes, edgeData.source.key, edgeData.target.key, edgeData.source.attributes, edgeData.target.attributes, edgeData.undirected ); if (breakable && shouldBreak) return edgeData.key; edgeData = edgeData.next; } while (edgeData !== undefined); return; } /** * Function returning an iterator over the egdes from the object at given key. * * @param {object} object - Target object. * @param {mixed} k - Neighbor key. * @return {Iterator} */ function createIteratorForKey(object, k) { let edgeData = object[k]; if (edgeData.next !== undefined) { return new Iterator(function () { if (!edgeData) return {done: true}; const value = { edge: edgeData.key, attributes: edgeData.attributes, source: edgeData.source.key, target: edgeData.target.key, sourceAttributes: edgeData.source.attributes, targetAttributes: edgeData.target.attributes, undirected: edgeData.undirected }; edgeData = edgeData.next; return { done: false, value }; }); } return Iterator.of({ edge: edgeData.key, attributes: edgeData.attributes, source: edgeData.source.key, target: edgeData.target.key, sourceAttributes: edgeData.source.attributes, targetAttributes: edgeData.target.attributes, undirected: edgeData.undirected }); } /** * Function creating an array of edges for the given type. * * @param {Graph} graph - Target Graph instance. * @param {string} type - Type of edges to retrieve. * @return {array} - Array of edges. */ function createEdgeArray(graph, type) { if (graph.size === 0) return []; if (type === 'mixed' || type === graph.type) { if (typeof Array.from === 'function') return Array.from(graph._edges.keys()); return take(graph._edges.keys(), graph._edges.size); } const size = type === 'undirected' ? graph.undirectedSize : graph.directedSize; const list = new Array(size), mask = type === 'undirected'; const iterator = graph._edges.values(); let i = 0; let step, data; while (((step = iterator.next()), step.done !== true)) { data = step.value; if (data.undirected === mask) list[i++] = data.key; } return list; } /** * Function iterating over a graph's edges using a callback to match one of * them. * * @param {Graph} graph - Target Graph instance. * @param {string} type - Type of edges to retrieve. * @param {function} callback - Function to call. */ function forEachEdge(breakable, graph, type, callback) { if (graph.size === 0) return; const shouldFilter = type !== 'mixed' && type !== graph.type; const mask = type === 'undirected'; let step, data; let shouldBreak = false; const iterator = graph._edges.values(); while (((step = iterator.next()), step.done !== true)) { data = step.value; if (shouldFilter && data.undirected !== mask) continue; const {key, attributes, source, target} = data; shouldBreak = callback( key, attributes, source.key, target.key, source.attributes, target.attributes, data.undirected ); if (breakable && shouldBreak) return key; } return; } /** * Function creating an iterator of edges for the given type. * * @param {Graph} graph - Target Graph instance. * @param {string} type - Type of edges to retrieve. * @return {Iterator} */ function createEdgeIterator(graph, type) { if (graph.size === 0) return Iterator.empty(); const shouldFilter = type !== 'mixed' && type !== graph.type; const mask = type === 'undirected'; const iterator = graph._edges.values(); return new Iterator(function next() { let step, data; // eslint-disable-next-line no-constant-condition while (true) { step = iterator.next(); if (step.done) return step; data = step.value; if (shouldFilter && data.undirected !== mask) continue; break; } const value = { edge: data.key, attributes: data.attributes, source: data.source.key, target: data.target.key, sourceAttributes: data.source.attributes, targetAttributes: data.target.attributes, undirected: data.undirected }; return {value, done: false}; }); } /** * Function iterating over a node's edges using a callback to match one of them. * * @param {boolean} multi - Whether the graph is multi or not. * @param {string} type - Type of edges to retrieve. * @param {string} direction - In or out? * @param {any} nodeData - Target node's data. * @param {function} callback - Function to call. */ function forEachEdgeForNode( breakable, multi, type, direction, nodeData, callback ) { const fn = multi ? forEachMulti : forEachSimple; let found; if (type !== 'undirected') { if (direction !== 'out') { found = fn(breakable, nodeData.in, callback); if (breakable && found) return found; } if (direction !== 'in') { found = fn( breakable, nodeData.out, callback, !direction ? nodeData.key : undefined ); if (breakable && found) return found; } } if (type !== 'directed') { found = fn(breakable, nodeData.undirected, callback); if (breakable && found) return found; } return; } /** * Function creating an array of edges for the given type & the given node. * * @param {boolean} multi - Whether the graph is multi or not. * @param {string} type - Type of edges to retrieve. * @param {string} direction - In or out? * @param {any} nodeData - Target node's data. * @return {array} - Array of edges. */ function createEdgeArrayForNode(multi, type, direction, nodeData) { const edges = []; // TODO: possibility to know size beforehand or factorize with map forEachEdgeForNode(false, multi, type, direction, nodeData, function (key) { edges.push(key); }); return edges; } /** * Function iterating over a node's edges using a callback. * * @param {string} type - Type of edges to retrieve. * @param {string} direction - In or out? * @param {any} nodeData - Target node's data. * @return {Iterator} */ function createEdgeIteratorForNode(type, direction, nodeData) { let iterator = Iterator.empty(); if (type !== 'undirected') { if (direction !== 'out' && typeof nodeData.in !== 'undefined') iterator = chain(iterator, createIterator(nodeData.in)); if (direction !== 'in' && typeof nodeData.out !== 'undefined') iterator = chain( iterator, createIterator(nodeData.out, !direction ? nodeData.key : undefined) ); } if (type !== 'directed' && typeof nodeData.undirected !== 'undefined') { iterator = chain(iterator, createIterator(nodeData.undirected)); } return iterator; } /** * Function iterating over edges for the given path using a callback to match * one of them. * * @param {string} type - Type of edges to retrieve. * @param {boolean} multi - Whether the graph is multi. * @param {string} direction - In or out? * @param {NodeData} sourceData - Source node's data. * @param {string} target - Target node. * @param {function} callback - Function to call. */ function forEachEdgeForPath( breakable, type, multi, direction, sourceData, target, callback ) { const fn = multi ? forEachForKeyMulti : forEachForKeySimple; let found; if (type !== 'undirected') { if (typeof sourceData.in !== 'undefined' && direction !== 'out') { found = fn(breakable, sourceData.in, target, callback); if (breakable && found) return found; } if ( typeof sourceData.out !== 'undefined' && direction !== 'in' && (direction || sourceData.key !== target) ) { found = fn(breakable, sourceData.out, target, callback); if (breakable && found) return found; } } if (type !== 'directed') { if (typeof sourceData.undirected !== 'undefined') { found = fn(breakable, sourceData.undirected, target, callback); if (breakable && found) return found; } } return; } /** * Function creating an array of edges for the given path. * * @param {string} type - Type of edges to retrieve. * @param {boolean} multi - Whether the graph is multi. * @param {string} direction - In or out? * @param {NodeData} sourceData - Source node's data. * @param {any} target - Target node. * @return {array} - Array of edges. */ function createEdgeArrayForPath(type, multi, direction, sourceData, target) { const edges = []; // TODO: possibility to know size beforehand or factorize with map forEachEdgeForPath( false, type, multi, direction, sourceData, target, function (key) { edges.push(key); } ); return edges; } /** * Function returning an iterator over edges for the given path. * * @param {string} type - Type of edges to retrieve. * @param {string} direction - In or out? * @param {NodeData} sourceData - Source node's data. * @param {string} target - Target node. * @param {function} callback - Function to call. */ function createEdgeIteratorForPath(type, direction, sourceData, target) { let iterator = Iterator.empty(); if (type !== 'undirected') { if ( typeof sourceData.in !== 'undefined' && direction !== 'out' && target in sourceData.in ) iterator = chain(iterator, createIteratorForKey(sourceData.in, target)); if ( typeof sourceData.out !== 'undefined' && direction !== 'in' && target in sourceData.out && (direction || sourceData.key !== target) ) iterator = chain(iterator, createIteratorForKey(sourceData.out, target)); } if (type !== 'directed') { if ( typeof sourceData.undirected !== 'undefined' && target in sourceData.undirected ) iterator = chain( iterator, createIteratorForKey(sourceData.undirected, target) ); } return iterator; } /** * Function attaching an edge array creator method to the Graph prototype. * * @param {function} Class - Target class. * @param {object} description - Method description. */ function attachEdgeArrayCreator(Class, description) { const {name, type, direction} = description; /** * Function returning an array of certain edges. * * Arity 0: Return all the relevant edges. * * Arity 1: Return all of a node's relevant edges. * @param {any} node - Target node. * * Arity 2: Return the relevant edges across the given path. * @param {any} source - Source node. * @param {any} target - Target node. * * @return {array|number} - The edges or the number of edges. * * @throws {Error} - Will throw if there are too many arguments. */ Class.prototype[name] = function (source, target) { // Early termination if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return []; if (!arguments.length) return createEdgeArray(this, type); if (arguments.length === 1) { source = '' + source; const nodeData = this._nodes.get(source); if (typeof nodeData === 'undefined') throw new NotFoundGraphError( `Graph.${name}: could not find the "${source}" node in the graph.` ); // Iterating over a node's edges return createEdgeArrayForNode( this.multi, type === 'mixed' ? this.type : type, direction, nodeData ); } if (arguments.length === 2) { source = '' + source; target = '' + target; const sourceData = this._nodes.get(source); if (!sourceData) throw new NotFoundGraphError( `Graph.${name}: could not find the "${source}" source node in the graph.` ); if (!this._nodes.has(target)) throw new NotFoundGraphError( `Graph.${name}: could not find the "${target}" target node in the graph.` ); // Iterating over the edges between source & target return createEdgeArrayForPath( type, this.multi, direction, sourceData, target ); } throw new InvalidArgumentsGraphError( `Graph.${name}: too many arguments (expecting 0, 1 or 2 and got ${arguments.length}).` ); }; } /** * Function attaching a edge callback iterator method to the Graph prototype. * * @param {function} Class - Target class. * @param {object} description - Method description. */ function attachForEachEdge(Class, description) { const {name, type, direction} = description; const forEachName = 'forEach' + name[0].toUpperCase() + name.slice(1, -1); /** * Function iterating over the graph's relevant edges by applying the given * callback. * * Arity 1: Iterate over all the relevant edges. * @param {function} callback - Callback to use. * * Arity 2: Iterate over all of a node's relevant edges. * @param {any} node - Target node. * @param {function} callback - Callback to use. * * Arity 3: Iterate over the relevant edges across the given path. * @param {any} source - Source node. * @param {any} target - Target node. * @param {function} callback - Callback to use. * * @return {undefined} * * @throws {Error} - Will throw if there are too many arguments. */ Class.prototype[forEachName] = function (source, target, callback) { // Early termination if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return; if (arguments.length === 1) { callback = source; return forEachEdge(false, this, type, callback); } if (arguments.length === 2) { source = '' + source; callback = target; const nodeData = this._nodes.get(source); if (typeof nodeData === 'undefined') throw new NotFoundGraphError( `Graph.${forEachName}: could not find the "${source}" node in the graph.` ); // Iterating over a node's edges // TODO: maybe attach the sub method to the instance dynamically? return forEachEdgeForNode( false, this.multi, type === 'mixed' ? this.type : type, direction, nodeData, callback ); } if (arguments.length === 3) { source = '' + source; target = '' + target; const sourceData = this._nodes.get(source); if (!sourceData) throw new NotFoundGraphError( `Graph.${forEachName}: could not find the "${source}" source node in the graph.` ); if (!this._nodes.has(target)) throw new NotFoundGraphError( `Graph.${forEachName}: could not find the "${target}" target node in the graph.` ); // Iterating over the edges between source & target return forEachEdgeForPath( false, type, this.multi, direction, sourceData, target, callback ); } throw new InvalidArgumentsGraphError( `Graph.${forEachName}: too many arguments (expecting 1, 2 or 3 and got ${arguments.length}).` ); }; /** * Function mapping the graph's relevant edges by applying the given * callback. * * Arity 1: Map all the relevant edges. * @param {function} callback - Callback to use. * * Arity 2: Map all of a node's relevant edges. * @param {any} node - Target node. * @param {function} callback - Callback to use. * * Arity 3: Map the relevant edges across the given path. * @param {any} source - Source node. * @param {any} target - Target node. * @param {function} callback - Callback to use. * * @return {undefined} * * @throws {Error} - Will throw if there are too many arguments. */ const mapName = 'map' + name[0].toUpperCase() + name.slice(1); Class.prototype[mapName] = function () { const args = Array.prototype.slice.call(arguments); const callback = args.pop(); let result; // We know the result length beforehand if (args.length === 0) { let length = 0; if (type !== 'directed') length += this.undirectedSize; if (type !== 'undirected') length += this.directedSize; result = new Array(length); let i = 0; args.push((e, ea, s, t, sa, ta, u) => { result[i++] = callback(e, ea, s, t, sa, ta, u); }); } // We don't know the result length beforehand // TODO: we can in some instances of simple graphs, knowing degree else { result = []; args.push((e, ea, s, t, sa, ta, u) => { result.push(callback(e, ea, s, t, sa, ta, u)); }); } this[forEachName].apply(this, args); return result; }; /** * Function filtering the graph's relevant edges using the provided predicate * function. * * Arity 1: Filter all the relevant edges. * @param {function} predicate - Predicate to use. * * Arity 2: Filter all of a node's relevant edges. * @param {any} node - Target node. * @param {function} predicate - Predicate to use. * * Arity 3: Filter the relevant edges across the given path. * @param {any} source - Source node. * @param {any} target - Target node. * @param {function} predicate - Predicate to use. * * @return {undefined} * * @throws {Error} - Will throw if there are too many arguments. */ const filterName = 'filter' + name[0].toUpperCase() + name.slice(1); Class.prototype[filterName] = function () { const args = Array.prototype.slice.call(arguments); const callback = args.pop(); const result = []; args.push((e, ea, s, t, sa, ta, u) => { if (callback(e, ea, s, t, sa, ta, u)) result.push(e); }); this[forEachName].apply(this, args); return result; }; /** * Function reducing the graph's relevant edges using the provided accumulator * function. * * Arity 1: Reduce all the relevant edges. * @param {function} accumulator - Accumulator to use. * @param {any} initialValue - Initial value. * * Arity 2: Reduce all of a node's relevant edges. * @param {any} node - Target node. * @param {function} accumulator - Accumulator to use. * @param {any} initialValue - Initial value. * * Arity 3: Reduce the relevant edges across the given path. * @param {any} source - Source node. * @param {any} target - Target node. * @param {function} accumulator - Accumulator to use. * @param {any} initialValue - Initial value. * * @return {undefined} * * @throws {Error} - Will throw if there are too many arguments. */ const reduceName = 'reduce' + name[0].toUpperCase() + name.slice(1); Class.prototype[reduceName] = function () { let args = Array.prototype.slice.call(arguments); if (args.length < 2 || args.length > 4) { throw new InvalidArgumentsGraphError( `Graph.${reduceName}: invalid number of arguments (expecting 2, 3 or 4 and got ${args.length}).` ); } if ( typeof args[args.length - 1] === 'function' && typeof args[args.length - 2] !== 'function' ) { throw new InvalidArgumentsGraphError( `Graph.${reduceName}: missing initial value. You must provide it because the callback takes more than one argument and we cannot infer the initial value from the first iteration, as you could with a simple array.` ); } let callback; let initialValue; if (args.length === 2) { callback = args[0]; initialValue = args[1]; args = []; } else if (args.length === 3) { callback = args[1]; initialValue = args[2]; args = [args[0]]; } else if (args.length === 4) { callback = args[2]; initialValue = args[3]; args = [args[0], args[1]]; } let accumulator = initialValue; args.push((e, ea, s, t, sa, ta, u) => { accumulator = callback(accumulator, e, ea, s, t, sa, ta, u); }); this[forEachName].apply(this, args); return accumulator; }; } /** * Function attaching a breakable edge callback iterator method to the Graph * prototype. * * @param {function} Class - Target class. * @param {object} description - Method description. */ function attachFindEdge(Class, description) { const {name, type, direction} = description; const findEdgeName = 'find' + name[0].toUpperCase() + name.slice(1, -1); /** * Function iterating over the graph's relevant edges in order to match * one of them using the provided predicate function. * * Arity 1: Iterate over all the relevant edges. * @param {function} callback - Callback to use. * * Arity 2: Iterate over all of a node's relevant edges. * @param {any} node - Target node. * @param {function} callback - Callback to use. * * Arity 3: Iterate over the relevant edges across the given path. * @param {any} source - Source node. * @param {any} target - Target node. * @param {function} callback - Callback to use. * * @return {undefined} * * @throws {Error} - Will throw if there are too many arguments. */ Class.prototype[findEdgeName] = function (source, target, callback) { // Early termination if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return false; if (arguments.length === 1) { callback = source; return forEachEdge(true, this, type, callback); } if (arguments.length === 2) { source = '' + source; callback = target; const nodeData = this._nodes.get(source); if (typeof nodeData === 'undefined') throw new NotFoundGraphError( `Graph.${findEdgeName}: could not find the "${source}" node in the graph.` ); // Iterating over a node's edges // TODO: maybe attach the sub method to the instance dynamically? return forEachEdgeForNode( true, this.multi, type === 'mixed' ? this.type : type, direction, nodeData, callback ); } if (arguments.length === 3) { source = '' + source; target = '' + target; const sourceData = this._nodes.get(source); if (!sourceData) throw new NotFoundGraphError( `Graph.${findEdgeName}: could not find the "${source}" source node in the graph.` ); if (!this._nodes.has(target)) throw new NotFoundGraphError( `Graph.${findEdgeName}: could not find the "${target}" target node in the graph.` ); // Iterating over the edges between source & target return forEachEdgeForPath( true, type, this.multi, direction, sourceData, target, callback ); } throw new InvalidArgumentsGraphError( `Graph.${findEdgeName}: too many arguments (expecting 1, 2 or 3 and got ${arguments.length}).` ); }; /** * Function iterating over the graph's relevant edges in order to assert * whether any one of them matches the provided predicate function. * * Arity 1: Iterate over all the relevant edges. * @param {function} callback - Callback to use. * * Arity 2: Iterate over all of a node's relevant edges. * @param {any} node - Target node. * @param {function} callback - Callback to use. * * Arity 3: Iterate over the relevant edges across the given path. * @param {any} source - Source node. * @param {any} target - Target node. * @param {function} callback - Callback to use. * * @return {undefined} * * @throws {Error} - Will throw if there are too many arguments. */ const someName = 'some' + name[0].toUpperCase() + name.slice(1, -1); Class.prototype[someName] = function () { const args = Array.prototype.slice.call(arguments); const callback = args.pop(); args.push((e, ea, s, t, sa, ta, u) => { return callback(e, ea, s, t, sa, ta, u); }); const found = this[findEdgeName].apply(this, args); if (found) return true; return false; }; /** * Function iterating over the graph's relevant edges in order to assert * whether all of them matche the provided predicate function. * * Arity 1: Iterate over all the relevant edges. * @param {function} callback - Callback to use. * * Arity 2: Iterate over all of a node's relevant edges. * @param {any} node - Target node. * @param {function} callback - Callback to use. * * Arity 3: Iterate over the relevant edges across the given path. * @param {any} source - Source node. * @param {any} target - Target node. * @param {function} callback - Callback to use. * * @return {undefined} * * @throws {Error} - Will throw if there are too many arguments. */ const everyName = 'every' + name[0].toUpperCase() + name.slice(1, -1); Class.prototype[everyName] = function () { const args = Array.prototype.slice.call(arguments); const callback = args.pop(); args.push((e, ea, s, t, sa, ta, u) => { return !callback(e, ea, s, t, sa, ta, u); }); const found = this[findEdgeName].apply(this, args); if (found) return false; return true; }; } /** * Function attaching an edge iterator method to the Graph prototype. * * @param {function} Class - Target class. * @param {object} description - Method description. */ function attachEdgeIteratorCreator(Class, description) { const {name: originalName, type, direction} = description; const name = originalName.slice(0, -1) + 'Entries'; /** * Function returning an iterator over the graph's edges. * * Arity 0: Iterate over all the relevant edges. * * Arity 1: Iterate over all of a node's relevant edges. * @param {any} node - Target node. * * Arity 2: Iterate over the relevant edges across the given path. * @param {any} source - Source node. * @param {any} target - Target node. * * @return {array|number} - The edges or the number of edges. * * @throws {Error} - Will throw if there are too many arguments. */ Class.prototype[name] = function (source, target) { // Early termination if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return Iterator.empty(); if (!arguments.length) return createEdgeIterator(this, type); if (arguments.length === 1) { source = '' + source; const sourceData = this._nodes.get(source); if (!sourceData) throw new NotFoundGraphError( `Graph.${name}: could not find the "${source}" node in the graph.` ); // Iterating over a node's edges return createEdgeIteratorForNode(type, direction, sourceData); } if (arguments.length === 2) { source = '' + source; target = '' + target; const sourceData = this._nodes.get(source); if (!sourceData) throw new NotFoundGraphError( `Graph.${name}: could not find the "${source}" source node in the graph.` ); if (!this._nodes.has(target)) throw new NotFoundGraphError( `Graph.${name}: could not find the "${target}" target node in the graph.` ); // Iterating over the edges between source & target return createEdgeIteratorForPath(type, direction, sourceData, target); } throw new InvalidArgumentsGraphError( `Graph.${name}: too many arguments (expecting 0, 1 or 2 and got ${arguments.length}).` ); }; } /** * Function attaching every edge iteration method to the Graph class. * * @param {function} Graph - Graph class. */ function attachEdgeIterationMethods(Graph) { EDGES_ITERATION.forEach(description => { attachEdgeArrayCreator(Graph, description); attachForEachEdge(Graph, description); attachFindEdge(Graph, description); attachEdgeIteratorCreator(Graph, description); }); } /** * Graphology Neighbor Iteration * ============================== * * Attaching some methods to the Graph class to be able to iterate over * neighbors. */ /** * Definitions. */ const NEIGHBORS_ITERATION = [ { name: 'neighbors', type: 'mixed' }, { name: 'inNeighbors', type: 'directed', direction: 'in' }, { name: 'outNeighbors', type: 'directed', direction: 'out' }, { name: 'inboundNeighbors', type: 'mixed', direction: 'in' }, { name: 'outboundNeighbors', type: 'mixed', direction: 'out' }, { name: 'directedNeighbors', type: 'directed' }, { name: 'undirectedNeighbors', type: 'undirected' } ]; /** * Helpers. */ function CompositeSetWrapper() { this.A = null; this.B = null; } CompositeSetWrapper.prototype.wrap = function (set) { if (this.A === null) this.A = set; else if (this.B === null) this.B = set; }; CompositeSetWrapper.prototype.has = function (key) { if (this.A !== null && key in this.A) return true; if (this.B !== null && key in this.B) return true; return false; }; /** * Function iterating over the given node's relevant neighbors to match * one of them using a predicated function. * * @param {string} type - Type of neighbors. * @param {string} direction - Direction. * @param {any} nodeData - Target node's data. * @param {function} callback - Callback to use. */ function forEachInObjectOnce(breakable, visited, nodeData, object, callback) { for (const k in object) { const edgeData = object[k]; const sourceData = edgeData.source; const targetData = edgeData.target; const neighborData = sourceData === nodeData ? targetData : sourceData; if (visited && visited.has(neighborData.key)) continue; const shouldBreak = callback(neighborData.key, neighborData.attributes); if (breakable && shouldBreak) return neighborData.key; } return; } function forEachNeighbor(breakable, type, direction, nodeData, callback) { // If we want only undirected or in or out, we can roll some optimizations if (type !== 'mixed') { if (type === 'undirected') return forEachInObjectOnce( breakable, null, nodeData, nodeData.undirected, callback ); if (typeof direction === 'string') return forEachInObjectOnce( breakable, null, nodeData, nodeData[direction], callback ); } // Else we need to keep a set of neighbors not to return duplicates // We cheat by querying the other adjacencies const visited = new CompositeSetWrapper(); let found; if (type !== 'undirected') { if (direction !== 'out') { found = forEachInObjectOnce( breakable, null, nodeData, nodeData.in, callback ); if (breakable && found) return found; visited.wrap(nodeData.in); } if (direction !== 'in') { found = forEachInObjectOnce( breakable, visited, nodeData, nodeData.out, callback ); if (breakable && found) return found; visited.wrap(nodeData.out); } } if (type !== 'directed') { found = forEachInObjectOnce( breakable, visited, nodeData, nodeData.undirected, callback ); if (breakable && found) return found; } return; } /** * Function creating an array of relevant neighbors for the given node. * * @param {string} type - Type of neighbors. * @param {string} direction - Direction. * @param {any} nodeData - Target node's data. * @return {Array} - The list of neighbors. */ function createNeighborArrayForNode(type, direction, nodeData) { // If we want only undirected or in or out, we can roll some optimizations if (type !== 'mixed') { if (type === 'undirected') return Object.keys(nodeData.undirected); if (typeof direction === 'string') return Object.keys(nodeData[direction]); } const neighbors = []; forEachNeighbor(false, type, direction, nodeData, function (key) { neighbors.push(key); }); return neighbors; } /** * Function returning an iterator over the given node's relevant neighbors. * * @param {string} type - Type of neighbors. * @param {string} direction - Direction. * @param {any} nodeData - Target node's data. * @return {Iterator} */ function createDedupedObjectIterator(visited, nodeData, object) { const keys = Object.keys(object); const l = keys.length; let i = 0; return new Iterator(function next() { let neighborData = null; do { if (i >= l) { if (visited) visited.wrap(object); return {done: true}; } const edgeData = object[keys[i++]]; const sourceData = edgeData.source; const targetData = edgeData.target; neighborData = sourceData === nodeData ? targetData : sourceData; if (visited && visited.has(neighborData.key)) { neighborData = null; continue; } } while (neighborData === null); return { done: false, value: {neighbor: neighborData.key, attributes: neighborData.attributes} }; }); } function createNeighborIterator(type, direction, nodeData) { // If we want only undirected or in or out, we can roll some optimizations if (type !== 'mixed') { if (type === 'undirected') return createDedupedObjectIterator(null, nodeData, nodeData.undirected); if (typeof direction === 'string') return createDedupedObjectIterator(null, nodeData, nodeData[direction]); } let iterator = Iterator.empty(); // Else we need to keep a set of neighbors not to return duplicates // We cheat by querying the other adjacencies const visited = new CompositeSetWrapper(); if (type !== 'undirected') { if (direction !== 'out') { iterator = chain( iterator, createDedupedObjectIterator(visited, nodeData, nodeData.in) ); } if (direction !== 'in') { iterator = chain( iterator, createDedupedObjectIterator(visited, nodeData, nodeData.out) ); } } if (type !== 'directed') { iterator = chain( iterator, createDedupedObjectIterator(visited, nodeData, nodeData.undirected) ); } return iterator; } /** * Function attaching a neighbors array creator method to the Graph prototype. * * @param {function} Class - Target class. * @param {object} description - Method description. */ function attachNeighborArrayCreator(Class, description) { const {name, type, direction} = description; /** * Function returning an array of certain neighbors. * * @param {any} node - Target node. * @return {array} - The neighbors of neighbors. * * @throws {Error} - Will throw if node is not found in the graph. */ Class.prototype[name] = function (node) { // Early termination if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return []; node = '' + node; const nodeData = this._nodes.get(node); if (typeof nodeData === 'undefined') throw new NotFoundGraphError( `Graph.${name}: could not find the "${node}" node in the graph.` ); // Here, we want to iterate over a node's relevant neighbors return createNeighborArrayForNode( type === 'mixed' ? this.type : type, direction, nodeData ); }; } /** * Function attaching a neighbors callback iterator method to the Graph prototype. * * @param {function} Class - Target class. * @param {object} description - Method description. */ function attachForEachNeighbor(Class, description) { const {name, type, direction} = description; const forEachName = 'forEach' + name[0].toUpperCase() + name.slice(1, -1); /** * Function iterating over all the relevant neighbors using a callback. * * @param {any} node - Target node. * @param {function} callback - Callback to use. * @return {undefined} * * @throws {Error} - Will throw if there are too many arguments. */ Class.prototype[forEachName] = function (node, callback) { // Early termination if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return; node = '' + node; const nodeData = this._nodes.get(node); if (typeof nodeData === 'undefined') throw new NotFoundGraphError( `Graph.${forEachName}: could not find the "${node}" node in the graph.` ); // Here, we want to iterate over a node's relevant neighbors forEachNeighbor( false, type === 'mixed' ? this.type : type, direction, nodeData, callback ); }; /** * Function mapping the relevant neighbors using a callback. * * @param {any} node - Target node. * @param {function} callback - Callback to use. * * @throws {Error} - Will throw if there are too many arguments. */ const mapName = 'map' + name[0].toUpperCase() + name.slice(1); Class.prototype[mapName] = function (node, callback) { // TODO: optimize when size is known beforehand const result = []; this[forEachName](node, (n, a) => { result.push(callback(n, a)); }); return result; }; /** * Function filtering the relevant neighbors using a callback. * * @param {any} node - Target node. * @param {function} callback - Callback to use. * * @throws {Error} - Will throw if there are too many arguments. */ const filterName = 'filter' + name[0].toUpperCase() + name.slice(1); Class.prototype[filterName] = function (node, callback) { const result = []; this[forEachName](node, (n, a) => { if (callback(n, a)) result.push(n); }); return result; }; /** * Function reducing the relevant neighbors using a callback. * * @param {any} node - Target node. * @param {function} callback - Callback to use. * * @throws {Error} - Will throw if there are too many arguments. */ const reduceName = 'reduce' + name[0].toUpperCase() + name.slice(1); Class.prototype[reduceName] = function (node, callback, initialValue) { if (arguments.length < 3) throw new InvalidArgumentsGraphError( `Graph.${reduceName}: missing initial value. You must provide it because the callback takes more than one argument and we cannot infer the initial value from the first iteration, as you could with a simple array.` ); let accumulator = initialValue; this[forEachName](node, (n, a) => { accumulator = callback(accumulator, n, a); }); return accumulator; }; } /** * Function attaching a breakable neighbors callback iterator method to the * Graph prototype. * * @param {function} Class - Target class. * @param {object} description - Method description. */ function attachFindNeighbor(Class, description) { const {name, type, direction} = description; const capitalizedSingular = name[0].toUpperCase() + name.slice(1, -1); const findName = 'find' + capitalizedSingular; /** * Function iterating over all the relevant neighbors using a callback. * * @param {any} node - Target node. * @param {function} callback - Callback to use. * @return {undefined} * * @throws {Error} - Will throw if there are too many arguments. */ Class.prototype[findName] = function (node, callback) { // Early termination if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return; node = '' + node; const nodeData = this._nodes.get(node); if (typeof nodeData === 'undefined') throw new NotFoundGraphError( `Graph.${findName}: could not find the "${node}" node in the graph.` ); // Here, we want to iterate over a node's relevant neighbors return forEachNeighbor( true, type === 'mixed' ? this.type : type, direction, nodeData, callback ); }; /** * Function iterating over all the relevant neighbors to find if any of them * matches the given predicate. * * @param {any} node - Target node. * @param {function} callback - Callback to use. * @return {boolean} * * @throws {Error} - Will throw if there are too many arguments. */ const someName = 'some' + capitalizedSingular; Class.prototype[someName] = function (node, callback) { const found = this[findName](node, callback); if (found) return true; return false; }; /** * Function iterating over all the relevant neighbors to find if all of them * matche the given predicate. * * @param {any} node - Target node. * @param {function} callback - Callback to use. * @return {boolean} * * @throws {Error} - Will throw if there are too many arguments. */ const everyName = 'every' + capitalizedSingular; Class.prototype[everyName] = function (node, callback) { const found = this[findName](node, (n, a) => { return !callback(n, a); }); if (found) return false; return true; }; } /** * Function attaching a neighbors callback iterator method to the Graph prototype. * * @param {function} Class - Target class. * @param {object} description - Method description. */ function attachNeighborIteratorCreator(Class, description) { const {name, type, direction} = description; const iteratorName = name.slice(0, -1) + 'Entries'; /** * Function returning an iterator over all the relevant neighbors. * * @param {any} node - Target node. * @return {Iterator} * * @throws {Error} - Will throw if there are too many arguments. */ Class.prototype[iteratorName] = function (node) { // Early termination if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return Iterator.empty(); node = '' + node; const nodeData = this._nodes.get(node); if (typeof nodeData === 'undefined') throw new NotFoundGraphError( `Graph.${iteratorName}: could not find the "${node}" node in the graph.` ); // Here, we want to iterate over a node's relevant neighbors return createNeighborIterator( type === 'mixed' ? this.type : type, direction, nodeData ); }; } /** * Function attaching every neighbor iteration method to the Graph class. * * @param {function} Graph - Graph class. */ function attachNeighborIterationMethods(Graph) { NEIGHBORS_ITERATION.forEach(description => { attachNeighborArrayCreator(Graph, description); attachForEachNeighbor(Graph, description); attachFindNeighbor(Graph, description); attachNeighborIteratorCreator(Graph, description); }); } /** * Graphology Adjacency Iteration * =============================== * * Attaching some methods to the Graph class to be able to iterate over a * graph's adjacency. */ /** * Function iterating over a simple graph's adjacency using a callback. * * @param {boolean} breakable - Can we break? * @param {boolean} assymetric - Whether to emit undirected edges only once. * @param {boolean} disconnectedNodes - Whether to emit disconnected nodes. * @param {Graph} graph - Target Graph instance. * @param {callback} function - Iteration callback. */ function forEachAdjacency( breakable, assymetric, disconnectedNodes, graph, callback ) { const iterator = graph._nodes.values(); const type = graph.type; let step, sourceData, neighbor, adj, edgeData, targetData, shouldBreak; while (((step = iterator.next()), step.done !== true)) { let hasEdges = false; sourceData = step.value; if (type !== 'undirected') { adj = sourceData.out; for (neighbor in adj) { edgeData = adj[neighbor]; do { targetData = edgeData.target; hasEdges = true; shouldBreak = callback( sourceData.key, targetData.key, sourceData.attributes, targetData.attributes, edgeData.key, edgeData.attributes, edgeData.undirected ); if (breakable && shouldBreak) return edgeData; edgeData = edgeData.next; } while (edgeData); } } if (type !== 'directed') { adj = sourceData.undirected; for (neighbor in adj) { if (assymetric && sourceData.key > neighbor) continue; edgeData = adj[neighbor]; do { targetData = edgeData.target; if (targetData.key !== neighbor) targetData = edgeData.source; hasEdges = true; shouldBreak = callback( sourceData.key, targetData.key, sourceData.attributes, targetData.attributes, edgeData.key, edgeData.attributes, edgeData.undirected ); if (breakable && shouldBreak) return edgeData; edgeData = edgeData.next; } while (edgeData); } } if (disconnectedNodes && !hasEdges) { shouldBreak = callback( sourceData.key, null, sourceData.attributes, null, null, null, null ); if (breakable && shouldBreak) return null; } } return; } /** * Graphology Serialization Utilities * =================================== * * Collection of functions used by the graph serialization schemes. */ /** * Formats internal node data into a serialized node. * * @param {any} key - The node's key. * @param {object} data - Internal node's data. * @return {array} - The serialized node. */ function serializeNode(key, data) { const serialized = {key}; if (!isEmpty(data.attributes)) serialized.attributes = assign({}, data.attributes); return serialized; } /** * Formats internal edge data into a serialized edge. * * @param {string} type - The graph's type. * @param {any} key - The edge's key. * @param {object} data - Internal edge's data. * @return {array} - The serialized edge. */ function serializeEdge(type, key, data) { const serialized = { key, source: data.source.key, target: data.target.key }; if (!isEmpty(data.attributes)) serialized.attributes = assign({}, data.attributes); if (type === 'mixed' && data.undirected) serialized.undirected = true; return serialized; } /** * Checks whether the given value is a serialized node. * * @param {mixed} value - Target value. * @return {string|null} */ function validateSerializedNode(value) { if (!isPlainObject(value)) throw new InvalidArgumentsGraphError( 'Graph.import: invalid serialized node. A serialized node should be a plain object with at least a "key" property.' ); if (!('key' in value)) throw new InvalidArgumentsGraphError( 'Graph.import: serialized node is missing its key.' ); if ( 'attributes' in value && (!isPlainObject(value.attributes) || value.attributes === null) ) throw new InvalidArgumentsGraphError( 'Graph.import: invalid attributes. Attributes should be a plain object, null or omitted.' ); } /** * Checks whether the given value is a serialized edge. * * @param {mixed} value - Target value. * @return {string|null} */ function validateSerializedEdge(value) { if (!isPlainObject(value)) throw new InvalidArgumentsGraphError( 'Graph.import: invalid serialized edge. A serialized edge should be a plain object with at least a "source" & "target" property.' ); if (!('source' in value)) throw new InvalidArgumentsGraphError( 'Graph.import: serialized edge is missing its source.' ); if (!('target' in value)) throw new InvalidArgumentsGraphError( 'Graph.import: serialized edge is missing its target.' ); if ( 'attributes' in value && (!isPlainObject(value.attributes) || value.attributes === null) ) throw new InvalidArgumentsGraphError( 'Graph.import: invalid attributes. Attributes should be a plain object, null or omitted.' ); if ('undirected' in value && typeof value.undirected !== 'boolean') throw new InvalidArgumentsGraphError( 'Graph.import: invalid undirectedness information. Undirected should be boolean or omitted.' ); } /* eslint no-nested-ternary: 0 */ /** * Constants. */ const INSTANCE_ID = incrementalIdStartingFromRandomByte(); /** * Enums. */ const TYPES = new Set(['directed', 'undirected', 'mixed']); const EMITTER_PROPS = new Set([ 'domain', '_events', '_eventsCount', '_maxListeners' ]); const EDGE_ADD_METHODS = [ { name: verb => `${verb}Edge`, generateKey: true }, { name: verb => `${verb}DirectedEdge`, generateKey: true, type: 'directed' }, { name: verb => `${verb}UndirectedEdge`, generateKey: true, type: 'undirected' }, { name: verb => `${verb}EdgeWithKey` }, { name: verb => `${verb}DirectedEdgeWithKey`, type: 'directed' }, { name: verb => `${verb}UndirectedEdgeWithKey`, type: 'undirected' } ]; /** * Default options. */ const DEFAULTS = { allowSelfLoops: true, multi: false, type: 'mixed' }; /** * Abstract functions used by the Graph class for various methods. */ /** * Internal method used to add a node to the given graph * * @param {Graph} graph - Target graph. * @param {any} node - The node's key. * @param {object} [attributes] - Optional attributes. * @return {NodeData} - Created node data. */ function addNode(graph, node, attributes) { if (attributes && !isPlainObject(attributes)) throw new InvalidArgumentsGraphError( `Graph.addNode: invalid attributes. Expecting an object but got "${attributes}"` ); // String coercion node = '' + node; attributes = attributes || {}; if (graph._nodes.has(node)) throw new UsageGraphError( `Graph.addNode: the "${node}" node already exist in the graph.` ); const data = new graph.NodeDataClass(node, attributes); // Adding the node to internal register graph._nodes.set(node, data); // Emitting graph.emit('nodeAdded', { key: node, attributes }); return data; } /** * Same as the above but without sanity checks because we call this in contexts * where necessary checks were already done. */ function unsafeAddNode(graph, node, attributes) { const data = new graph.NodeDataClass(node, attributes); graph._nodes.set(node, data); graph.emit('nodeAdded', { key: node, attributes }); return data; } /** * Internal method used to add an arbitrary edge to the given graph. * * @param {Graph} graph - Target graph. * @param {string} name - Name of the child method for errors. * @param {boolean} mustGenerateKey - Should the graph generate an id? * @param {boolean} undirected - Whether the edge is undirected. * @param {any} edge - The edge's key. * @param {any} source - The source node. * @param {any} target - The target node. * @param {object} [attributes] - Optional attributes. * @return {any} - The edge. * * @throws {Error} - Will throw if the graph is of the wrong type. * @throws {Error} - Will throw if the given attributes are not an object. * @throws {Error} - Will throw if source or target doesn't exist. * @throws {Error} - Will throw if the edge already exist. */ function addEdge( graph, name, mustGenerateKey, undirected, edge, source, target, attributes ) { // Checking validity of operation if (!undirected && graph.type === 'undirected') throw new UsageGraphError( `Graph.${name}: you cannot add a directed edge to an undirected graph. Use the #.addEdge or #.addUndirectedEdge instead.` ); if (undirected && graph.type === 'directed') throw new UsageGraphError( `Graph.${name}: you cannot add an undirected edge to a directed graph. Use the #.addEdge or #.addDirectedEdge instead.` ); if (attributes && !isPlainObject(attributes)) throw new InvalidArgumentsGraphError( `Graph.${name}: invalid attributes. Expecting an object but got "${attributes}"` ); // Coercion of source & target: source = '' + source; target = '' + target; attributes = attributes || {}; if (!graph.allowSelfLoops && source === target) throw new UsageGraphError( `Graph.${name}: source & target are the same ("${source}"), thus creating a loop explicitly forbidden by this graph 'allowSelfLoops' option set to false.` ); const sourceData = graph._nodes.get(source), targetData = graph._nodes.get(target); if (!sourceData) throw new NotFoundGraphError( `Graph.${name}: source node "${source}" not found.` ); if (!targetData) throw new NotFoundGraphError( `Graph.${name}: target node "${target}" not found.` ); // Must the graph generate an id for this edge? const eventData = { key: null, undirected, source, target, attributes }; if (mustGenerateKey) { // NOTE: in this case we can guarantee that the key does not already // exist and is already correctly casted as a string edge = graph._edgeKeyGenerator(); } else { // Coercion of edge key edge = '' + edge; // Here, we have a key collision if (graph._edges.has(edge)) throw new UsageGraphError( `Graph.${name}: the "${edge}" edge already exists in the graph.` ); } // Here, we might have a source / target collision if ( !graph.multi && (undirected ? typeof sourceData.undirected[target] !== 'undefined' : typeof sourceData.out[target] !== 'undefined') ) { throw new UsageGraphError( `Graph.${name}: an edge linking "${source}" to "${target}" already exists. If you really want to add multiple edges linking those nodes, you should create a multi graph by using the 'multi' option.` ); } // Storing some data const edgeData = new EdgeData( undirected, edge, sourceData, targetData, attributes ); // Adding the edge to the internal register graph._edges.set(edge, edgeData); // Incrementing node degree counters const isSelfLoop = source === target; if (undirected) { sourceData.undirectedDegree++; targetData.undirectedDegree++; if (isSelfLoop) { sourceData.undirectedLoops++; graph._undirectedSelfLoopCount++; } } else { sourceData.outDegree++; targetData.inDegree++; if (isSelfLoop) { sourceData.directedLoops++; graph._directedSelfLoopCount++; } } // Updating relevant index if (graph.multi) edgeData.attachMulti(); else edgeData.attach(); if (undirected) graph._undirectedSize++; else graph._directedSize++; // Emitting eventData.key = edge; graph.emit('edgeAdded', eventData); return edge; } /** * Internal method used to add an arbitrary edge to the given graph. * * @param {Graph} graph - Target graph. * @param {string} name - Name of the child method for errors. * @param {boolean} mustGenerateKey - Should the graph generate an id? * @param {boolean} undirected - Whether the edge is undirected. * @param {any} edge - The edge's key. * @param {any} source - The source node. * @param {any} target - The target node. * @param {object} [attributes] - Optional attributes. * @param {boolean} [asUpdater] - Are we updating or merging? * @return {any} - The edge. * * @throws {Error} - Will throw if the graph is of the wrong type. * @throws {Error} - Will throw if the given attributes are not an object. * @throws {Error} - Will throw if source or target doesn't exist. * @throws {Error} - Will throw if the edge already exist. */ function mergeEdge( graph, name, mustGenerateKey, undirected, edge, source, target, attributes, asUpdater ) { // Checking validity of operation if (!undirected && graph.type === 'undirected') throw new UsageGraphError( `Graph.${name}: you cannot merge/update a directed edge to an undirected graph. Use the #.mergeEdge/#.updateEdge or #.addUndirectedEdge instead.` ); if (undirected && graph.type === 'directed') throw new UsageGraphError( `Graph.${name}: you cannot merge/update an undirected edge to a directed graph. Use the #.mergeEdge/#.updateEdge or #.addDirectedEdge instead.` ); if (attributes) { if (asUpdater) { if (typeof attributes !== 'function') throw new InvalidArgumentsGraphError( `Graph.${name}: invalid updater function. Expecting a function but got "${attributes}"` ); } else { if (!isPlainObject(attributes)) throw new InvalidArgumentsGraphError( `Graph.${name}: invalid attributes. Expecting an object but got "${attributes}"` ); } } // Coercion of source & target: source = '' + source; target = '' + target; let updater; if (asUpdater) { updater = attributes; attributes = undefined; } if (!graph.allowSelfLoops && source === target) throw new UsageGraphError( `Graph.${name}: source & target are the same ("${source}"), thus creating a loop explicitly forbidden by this graph 'allowSelfLoops' option set to false.` ); let sourceData = graph._nodes.get(source); let targetData = graph._nodes.get(target); let edgeData; // Do we need to handle duplicate? let alreadyExistingEdgeData; if (!mustGenerateKey) { edgeData = graph._edges.get(edge); if (edgeData) { // Here, we need to ensure, if the user gave a key, that source & target // are consistent if (edgeData.source.key !== source || edgeData.target.key !== target) { // If source or target inconsistent if ( !undirected || edgeData.source.key !== target || edgeData.target.key !== source ) { // If directed, or source/target aren't flipped throw new UsageGraphError( `Graph.${name}: inconsistency detected when attempting to merge the "${edge}" edge with "${source}" source & "${target}" target vs. ("${edgeData.source.key}", "${edgeData.target.key}").` ); } } alreadyExistingEdgeData = edgeData; } } // Here, we might have a source / target collision if (!alreadyExistingEdgeData && !graph.multi && sourceData) { alreadyExistingEdgeData = undirected ? sourceData.undirected[target] : sourceData.out[target]; } // Handling duplicates if (alreadyExistingEdgeData) { const info = [alreadyExistingEdgeData.key, false, false, false]; // We can skip the attribute merging part if the user did not provide them if (asUpdater ? !updater : !attributes) return info; // Updating the attributes if (asUpdater) { const oldAttributes = alreadyExistingEdgeData.attributes; alreadyExistingEdgeData.attributes = updater(oldAttributes); graph.emit('edgeAttributesUpdated', { type: 'replace', key: alreadyExistingEdgeData.key, attributes: alreadyExistingEdgeData.attributes }); } // Merging the attributes else { assign(alreadyExistingEdgeData.attributes, attributes); graph.emit('edgeAttributesUpdated', { type: 'merge', key: alreadyExistingEdgeData.key, attributes: alreadyExistingEdgeData.attributes, data: attributes }); } return info; } attributes = attributes || {}; if (asUpdater && updater) attributes = updater(attributes); // Must the graph generate an id for this edge? const eventData = { key: null, undirected, source, target, attributes }; if (mustGenerateKey) { // NOTE: in this case we can guarantee that the key does not already // exist and is already correctly casted as a string edge = graph._edgeKeyGenerator(); } else { // Coercion of edge key edge = '' + edge; // Here, we have a key collision if (graph._edges.has(edge)) throw new UsageGraphError( `Graph.${name}: the "${edge}" edge already exists in the graph.` ); } let sourceWasAdded = false; let targetWasAdded = false; if (!sourceData) { sourceData = unsafeAddNode(graph, source, {}); sourceWasAdded = true; if (source === target) { targetData = sourceData; targetWasAdded = true; } } if (!targetData) { targetData = unsafeAddNode(graph, target, {}); targetWasAdded = true; } // Storing some data edgeData = new EdgeData(undirected, edge, sourceData, targetData, attributes); // Adding the edge to the internal register graph._edges.set(edge, edgeData); // Incrementing node degree counters const isSelfLoop = source === target; if (undirected) { sourceData.undirectedDegree++; targetData.undirectedDegree++; if (isSelfLoop) { sourceData.undirectedLoops++; graph._undirectedSelfLoopCount++; } } else { sourceData.outDegree++; targetData.inDegree++; if (isSelfLoop) { sourceData.directedLoops++; graph._directedSelfLoopCount++; } } // Updating relevant index if (graph.multi) edgeData.attachMulti(); else edgeData.attach(); if (undirected) graph._undirectedSize++; else graph._directedSize++; // Emitting eventData.key = edge; graph.emit('edgeAdded', eventData); return [edge, true, sourceWasAdded, targetWasAdded]; } /** * Internal method used to drop an edge. * * @param {Graph} graph - Target graph. * @param {EdgeData} edgeData - Data of the edge to drop. */ function dropEdgeFromData(graph, edgeData) { // Dropping the edge from the register graph._edges.delete(edgeData.key); // Updating related degrees const {source: sourceData, target: targetData, attributes} = edgeData; const undirected = edgeData.undirected; const isSelfLoop = sourceData === targetData; if (undirected) { sourceData.undirectedDegree--; targetData.undirectedDegree--; if (isSelfLoop) { sourceData.undirectedLoops--; graph._undirectedSelfLoopCount--; } } else { sourceData.outDegree--; targetData.inDegree--; if (isSelfLoop) { sourceData.directedLoops--; graph._directedSelfLoopCount--; } } // Clearing index if (graph.multi) edgeData.detachMulti(); else edgeData.detach(); if (undirected) graph._undirectedSize--; else graph._directedSize--; // Emitting graph.emit('edgeDropped', { key: edgeData.key, attributes, source: sourceData.key, target: targetData.key, undirected }); } /** * Graph class * * @constructor * @param {object} [options] - Options: * @param {boolean} [allowSelfLoops] - Allow self loops? * @param {string} [type] - Type of the graph. * @param {boolean} [map] - Allow references as keys? * @param {boolean} [multi] - Allow parallel edges? * * @throws {Error} - Will throw if the arguments are not valid. */ class Graph extends EventEmitter { constructor(options) { super(); //-- Solving options options = assign({}, DEFAULTS, options); // Enforcing options validity if (typeof options.multi !== 'boolean') throw new InvalidArgumentsGraphError( `Graph.constructor: invalid 'multi' option. Expecting a boolean but got "${options.multi}".` ); if (!TYPES.has(options.type)) throw new InvalidArgumentsGraphError( `Graph.constructor: invalid 'type' option. Should be one of "mixed", "directed" or "undirected" but got "${options.type}".` ); if (typeof options.allowSelfLoops !== 'boolean') throw new InvalidArgumentsGraphError( `Graph.constructor: invalid 'allowSelfLoops' option. Expecting a boolean but got "${options.allowSelfLoops}".` ); //-- Private properties // Utilities const NodeDataClass = options.type === 'mixed' ? MixedNodeData : options.type === 'directed' ? DirectedNodeData : UndirectedNodeData; privateProperty(this, 'NodeDataClass', NodeDataClass); // Internal edge key generator // NOTE: this internal generator produce keys that are strings // composed of a weird prefix, an incremental instance id starting from // a random byte and finally an internal instance incremental id. // All this to avoid intra-frame and cross-frame adversarial inputs // that can force a single #.addEdge call to degenerate into a O(n) // available key search loop. // It also ensures that automatically generated edge keys are unlikely // to produce collisions with arbitrary keys given by users. const instancePrefix = 'geid_' + INSTANCE_ID() + '_'; let edgeId = 0; const edgeKeyGenerator = () => { let availableEdgeKey; do { availableEdgeKey = instancePrefix + edgeId++; } while (this._edges.has(availableEdgeKey)); return availableEdgeKey; }; // Indexes privateProperty(this, '_attributes', {}); privateProperty(this, '_nodes', new Map()); privateProperty(this, '_edges', new Map()); privateProperty(this, '_directedSize', 0); privateProperty(this, '_undirectedSize', 0); privateProperty(this, '_directedSelfLoopCount', 0); privateProperty(this, '_undirectedSelfLoopCount', 0); privateProperty(this, '_edgeKeyGenerator', edgeKeyGenerator); // Options privateProperty(this, '_options', options); // Emitter properties EMITTER_PROPS.forEach(prop => privateProperty(this, prop, this[prop])); //-- Properties readers readOnlyProperty(this, 'order', () => this._nodes.size); readOnlyProperty(this, 'size', () => this._edges.size); readOnlyProperty(this, 'directedSize', () => this._directedSize); readOnlyProperty(this, 'undirectedSize', () => this._undirectedSize); readOnlyProperty( this, 'selfLoopCount', () => this._directedSelfLoopCount + this._undirectedSelfLoopCount ); readOnlyProperty( this, 'directedSelfLoopCount', () => this._directedSelfLoopCount ); readOnlyProperty( this, 'undirectedSelfLoopCount', () => this._undirectedSelfLoopCount ); readOnlyProperty(this, 'multi', this._options.multi); readOnlyProperty(this, 'type', this._options.type); readOnlyProperty(this, 'allowSelfLoops', this._options.allowSelfLoops); readOnlyProperty(this, 'implementation', () => 'graphology'); } _resetInstanceCounters() { this._directedSize = 0; this._undirectedSize = 0; this._directedSelfLoopCount = 0; this._undirectedSelfLoopCount = 0; } /**--------------------------------------------------------------------------- * Read **--------------------------------------------------------------------------- */ /** * Method returning whether the given node is found in the graph. * * @param {any} node - The node. * @return {boolean} */ hasNode(node) { return this._nodes.has('' + node); } /** * Method returning whether the given directed edge is found in the graph. * * Arity 1: * @param {any} edge - The edge's key. * * Arity 2: * @param {any} source - The edge's source. * @param {any} target - The edge's target. * * @return {boolean} * * @throws {Error} - Will throw if the arguments are invalid. */ hasDirectedEdge(source, target) { // Early termination if (this.type === 'undirected') return false; if (arguments.length === 1) { const edge = '' + source; const edgeData = this._edges.get(edge); return !!edgeData && !edgeData.undirected; } else if (arguments.length === 2) { source = '' + source; target = '' + target; // If the node source or the target is not in the graph we break const nodeData = this._nodes.get(source); if (!nodeData) return false; // Is there a directed edge pointing toward target? return nodeData.out.hasOwnProperty(target); } throw new InvalidArgumentsGraphError( `Graph.hasDirectedEdge: invalid arity (${arguments.length}, instead of 1 or 2). You can either ask for an edge id or for the existence of an edge between a source & a target.` ); } /** * Method returning whether the given undirected edge is found in the graph. * * Arity 1: * @param {any} edge - The edge's key. * * Arity 2: * @param {any} source - The edge's source. * @param {any} target - The edge's target. * * @return {boolean} * * @throws {Error} - Will throw if the arguments are invalid. */ hasUndirectedEdge(source, target) { // Early termination if (this.type === 'directed') return false; if (arguments.length === 1) { const edge = '' + source; const edgeData = this._edges.get(edge); return !!edgeData && edgeData.undirected; } else if (arguments.length === 2) { source = '' + source; target = '' + target; // If the node source or the target is not in the graph we break const nodeData = this._nodes.get(source); if (!nodeData) return false; // Is there a directed edge pointing toward target? return nodeData.undirected.hasOwnProperty(target); } throw new InvalidArgumentsGraphError( `Graph.hasDirectedEdge: invalid arity (${arguments.length}, instead of 1 or 2). You can either ask for an edge id or for the existence of an edge between a source & a target.` ); } /** * Method returning whether the given edge is found in the graph. * * Arity 1: * @param {any} edge - The edge's key. * * Arity 2: * @param {any} source - The edge's source. * @param {any} target - The edge's target. * * @return {boolean} * * @throws {Error} - Will throw if the arguments are invalid. */ hasEdge(source, target) { if (arguments.length === 1) { const edge = '' + source; return this._edges.has(edge); } else if (arguments.length === 2) { source = '' + source; target = '' + target; // If the node source or the target is not in the graph we break const nodeData = this._nodes.get(source); if (!nodeData) return false; // Is there a directed edge pointing toward target? return ( (typeof nodeData.out !== 'undefined' && nodeData.out.hasOwnProperty(target)) || (typeof nodeData.undirected !== 'undefined' && nodeData.undirected.hasOwnProperty(target)) ); } throw new InvalidArgumentsGraphError( `Graph.hasEdge: invalid arity (${arguments.length}, instead of 1 or 2). You can either ask for an edge id or for the existence of an edge between a source & a target.` ); } /** * Method returning the edge matching source & target in a directed fashion. * * @param {any} source - The edge's source. * @param {any} target - The edge's target. * * @return {any|undefined} * * @throws {Error} - Will throw if the graph is multi. * @throws {Error} - Will throw if source or target doesn't exist. */ directedEdge(source, target) { if (this.type === 'undirected') return; source = '' + source; target = '' + target; if (this.multi) throw new UsageGraphError( 'Graph.directedEdge: this method is irrelevant with multigraphs since there might be multiple edges between source & target. See #.directedEdges instead.' ); const sourceData = this._nodes.get(source); if (!sourceData) throw new NotFoundGraphError( `Graph.directedEdge: could not find the "${source}" source node in the graph.` ); if (!this._nodes.has(target)) throw new NotFoundGraphError( `Graph.directedEdge: could not find the "${target}" target node in the graph.` ); const edgeData = (sourceData.out && sourceData.out[target]) || undefined; if (edgeData) return edgeData.key; } /** * Method returning the edge matching source & target in a undirected fashion. * * @param {any} source - The edge's source. * @param {any} target - The edge's target. * * @return {any|undefined} * * @throws {Error} - Will throw if the graph is multi. * @throws {Error} - Will throw if source or target doesn't exist. */ undirectedEdge(source, target) { if (this.type === 'directed') return; source = '' + source; target = '' + target; if (this.multi) throw new UsageGraphError( 'Graph.undirectedEdge: this method is irrelevant with multigraphs since there might be multiple edges between source & target. See #.undirectedEdges instead.' ); const sourceData = this._nodes.get(source); if (!sourceData) throw new NotFoundGraphError( `Graph.undirectedEdge: could not find the "${source}" source node in the graph.` ); if (!this._nodes.has(target)) throw new NotFoundGraphError( `Graph.undirectedEdge: could not find the "${target}" target node in the graph.` ); const edgeData = (sourceData.undirected && sourceData.undirected[target]) || undefined; if (edgeData) return edgeData.key; } /** * Method returning the edge matching source & target in a mixed fashion. * * @param {any} source - The edge's source. * @param {any} target - The edge's target. * * @return {any|undefined} * * @throws {Error} - Will throw if the graph is multi. * @throws {Error} - Will throw if source or target doesn't exist. */ edge(source, target) { if (this.multi) throw new UsageGraphError( 'Graph.edge: this method is irrelevant with multigraphs since there might be multiple edges between source & target. See #.edges instead.' ); source = '' + source; target = '' + target; const sourceData = this._nodes.get(source); if (!sourceData) throw new NotFoundGraphError( `Graph.edge: could not find the "${source}" source node in the graph.` ); if (!this._nodes.has(target)) throw new NotFoundGraphError( `Graph.edge: could not find the "${target}" target node in the graph.` ); const edgeData = (sourceData.out && sourceData.out[target]) || (sourceData.undirected && sourceData.undirected[target]) || undefined; if (edgeData) return edgeData.key; } /** * Method returning whether two nodes are directed neighbors. * * @param {any} node - The node's key. * @param {any} neighbor - The neighbor's key. * @return {boolean} * * @throws {Error} - Will throw if the node isn't in the graph. */ areDirectedNeighbors(node, neighbor) { node = '' + node; neighbor = '' + neighbor; const nodeData = this._nodes.get(node); if (!nodeData) throw new NotFoundGraphError( `Graph.areDirectedNeighbors: could not find the "${node}" node in the graph.` ); if (this.type === 'undirected') return false; return neighbor in nodeData.in || neighbor in nodeData.out; } /** * Method returning whether two nodes are out neighbors. * * @param {any} node - The node's key. * @param {any} neighbor - The neighbor's key. * @return {boolean} * * @throws {Error} - Will throw if the node isn't in the graph. */ areOutNeighbors(node, neighbor) { node = '' + node; neighbor = '' + neighbor; const nodeData = this._nodes.get(node); if (!nodeData) throw new NotFoundGraphError( `Graph.areOutNeighbors: could not find the "${node}" node in the graph.` ); if (this.type === 'undirected') return false; return neighbor in nodeData.out; } /** * Method returning whether two nodes are in neighbors. * * @param {any} node - The node's key. * @param {any} neighbor - The neighbor's key. * @return {boolean} * * @throws {Error} - Will throw if the node isn't in the graph. */ areInNeighbors(node, neighbor) { node = '' + node; neighbor = '' + neighbor; const nodeData = this._nodes.get(node); if (!nodeData) throw new NotFoundGraphError( `Graph.areInNeighbors: could not find the "${node}" node in the graph.` ); if (this.type === 'undirected') return false; return neighbor in nodeData.in; } /** * Method returning whether two nodes are undirected neighbors. * * @param {any} node - The node's key. * @param {any} neighbor - The neighbor's key. * @return {boolean} * * @throws {Error} - Will throw if the node isn't in the graph. */ areUndirectedNeighbors(node, neighbor) { node = '' + node; neighbor = '' + neighbor; const nodeData = this._nodes.get(node); if (!nodeData) throw new NotFoundGraphError( `Graph.areUndirectedNeighbors: could not find the "${node}" node in the graph.` ); if (this.type === 'directed') return false; return neighbor in nodeData.undirected; } /** * Method returning whether two nodes are neighbors. * * @param {any} node - The node's key. * @param {any} neighbor - The neighbor's key. * @return {boolean} * * @throws {Error} - Will throw if the node isn't in the graph. */ areNeighbors(node, neighbor) { node = '' + node; neighbor = '' + neighbor; const nodeData = this._nodes.get(node); if (!nodeData) throw new NotFoundGraphError( `Graph.areNeighbors: could not find the "${node}" node in the graph.` ); if (this.type !== 'undirected') { if (neighbor in nodeData.in || neighbor in nodeData.out) return true; } if (this.type !== 'directed') { if (neighbor in nodeData.undirected) return true; } return false; } /** * Method returning whether two nodes are inbound neighbors. * * @param {any} node - The node's key. * @param {any} neighbor - The neighbor's key. * @return {boolean} * * @throws {Error} - Will throw if the node isn't in the graph. */ areInboundNeighbors(node, neighbor) { node = '' + node; neighbor = '' + neighbor; const nodeData = this._nodes.get(node); if (!nodeData) throw new NotFoundGraphError( `Graph.areInboundNeighbors: could not find the "${node}" node in the graph.` ); if (this.type !== 'undirected') { if (neighbor in nodeData.in) return true; } if (this.type !== 'directed') { if (neighbor in nodeData.undirected) return true; } return false; } /** * Method returning whether two nodes are outbound neighbors. * * @param {any} node - The node's key. * @param {any} neighbor - The neighbor's key. * @return {boolean} * * @throws {Error} - Will throw if the node isn't in the graph. */ areOutboundNeighbors(node, neighbor) { node = '' + node; neighbor = '' + neighbor; const nodeData = this._nodes.get(node); if (!nodeData) throw new NotFoundGraphError( `Graph.areOutboundNeighbors: could not find the "${node}" node in the graph.` ); if (this.type !== 'undirected') { if (neighbor in nodeData.out) return true; } if (this.type !== 'directed') { if (neighbor in nodeData.undirected) return true; } return false; } /** * Method returning the given node's in degree. * * @param {any} node - The node's key. * @return {number} - The node's in degree. * * @throws {Error} - Will throw if the node isn't in the graph. */ inDegree(node) { node = '' + node; const nodeData = this._nodes.get(node); if (!nodeData) throw new NotFoundGraphError( `Graph.inDegree: could not find the "${node}" node in the graph.` ); if (this.type === 'undirected') return 0; return nodeData.inDegree; } /** * Method returning the given node's out degree. * * @param {any} node - The node's key. * @return {number} - The node's in degree. * * @throws {Error} - Will throw if the node isn't in the graph. */ outDegree(node) { node = '' + node; const nodeData = this._nodes.get(node); if (!nodeData) throw new NotFoundGraphError( `Graph.outDegree: could not find the "${node}" node in the graph.` ); if (this.type === 'undirected') return 0; return nodeData.outDegree; } /** * Method returning the given node's directed degree. * * @param {any} node - The node's key. * @return {number} - The node's in degree. * * @throws {Error} - Will throw if the node isn't in the graph. */ directedDegree(node) { node = '' + node; const nodeData = this._nodes.get(node); if (!nodeData) throw new NotFoundGraphError( `Graph.directedDegree: could not find the "${node}" node in the graph.` ); if (this.type === 'undirected') return 0; return nodeData.inDegree + nodeData.outDegree; } /** * Method returning the given node's undirected degree. * * @param {any} node - The node's key. * @return {number} - The node's in degree. * * @throws {Error} - Will throw if the node isn't in the graph. */ undirectedDegree(node) { node = '' + node; const nodeData = this._nodes.get(node); if (!nodeData) throw new NotFoundGraphError( `Graph.undirectedDegree: could not find the "${node}" node in the graph.` ); if (this.type === 'directed') return 0; return nodeData.undirectedDegree; } /** * Method returning the given node's inbound degree. * * @param {any} node - The node's key. * @return {number} - The node's inbound degree. * * @throws {Error} - Will throw if the node isn't in the graph. */ inboundDegree(node) { node = '' + node; const nodeData = this._nodes.get(node); if (!nodeData) throw new NotFoundGraphError( `Graph.inboundDegree: could not find the "${node}" node in the graph.` ); let degree = 0; if (this.type !== 'directed') { degree += nodeData.undirectedDegree; } if (this.type !== 'undirected') { degree += nodeData.inDegree; } return degree; } /** * Method returning the given node's outbound degree. * * @param {any} node - The node's key. * @return {number} - The node's outbound degree. * * @throws {Error} - Will throw if the node isn't in the graph. */ outboundDegree(node) { node = '' + node; const nodeData = this._nodes.get(node); if (!nodeData) throw new NotFoundGraphError( `Graph.outboundDegree: could not find the "${node}" node in the graph.` ); let degree = 0; if (this.type !== 'directed') { degree += nodeData.undirectedDegree; } if (this.type !== 'undirected') { degree += nodeData.outDegree; } return degree; } /** * Method returning the given node's directed degree. * * @param {any} node - The node's key. * @return {number} - The node's degree. * * @throws {Error} - Will throw if the node isn't in the graph. */ degree(node) { node = '' + node; const nodeData = this._nodes.get(node); if (!nodeData) throw new NotFoundGraphError( `Graph.degree: could not find the "${node}" node in the graph.` ); let degree = 0; if (this.type !== 'directed') { degree += nodeData.undirectedDegree; } if (this.type !== 'undirected') { degree += nodeData.inDegree + nodeData.outDegree; } return degree; } /** * Method returning the given node's in degree without considering self loops. * * @param {any} node - The node's key. * @return {number} - The node's in degree. * * @throws {Error} - Will throw if the node isn't in the graph. */ inDegreeWithoutSelfLoops(node) { node = '' + node; const nodeData = this._nodes.get(node); if (!nodeData) throw new NotFoundGraphError( `Graph.inDegreeWithoutSelfLoops: could not find the "${node}" node in the graph.` ); if (this.type === 'undirected') return 0; return nodeData.inDegree - nodeData.directedLoops; } /** * Method returning the given node's out degree without considering self loops. * * @param {any} node - The node's key. * @return {number} - The node's in degree. * * @throws {Error} - Will throw if the node isn't in the graph. */ outDegreeWithoutSelfLoops(node) { node = '' + node; const nodeData = this._nodes.get(node); if (!nodeData) throw new NotFoundGraphError( `Graph.outDegreeWithoutSelfLoops: could not find the "${node}" node in the graph.` ); if (this.type === 'undirected') return 0; return nodeData.outDegree - nodeData.directedLoops; } /** * Method returning the given node's directed degree without considering self loops. * * @param {any} node - The node's key. * @return {number} - The node's in degree. * * @throws {Error} - Will throw if the node isn't in the graph. */ directedDegreeWithoutSelfLoops(node) { node = '' + node; const nodeData = this._nodes.get(node); if (!nodeData) throw new NotFoundGraphError( `Graph.directedDegreeWithoutSelfLoops: could not find the "${node}" node in the graph.` ); if (this.type === 'undirected') return 0; return nodeData.inDegree + nodeData.outDegree - nodeData.directedLoops * 2; } /** * Method returning the given node's undirected degree without considering self loops. * * @param {any} node - The node's key. * @return {number} - The node's in degree. * * @throws {Error} - Will throw if the node isn't in the graph. */ undirectedDegreeWithoutSelfLoops(node) { node = '' + node; const nodeData = this._nodes.get(node); if (!nodeData) throw new NotFoundGraphError( `Graph.undirectedDegreeWithoutSelfLoops: could not find the "${node}" node in the graph.` ); if (this.type === 'directed') return 0; return nodeData.undirectedDegree - nodeData.undirectedLoops * 2; } /** * Method returning the given node's inbound degree without considering self loops. * * @param {any} node - The node's key. * @return {number} - The node's inbound degree. * * @throws {Error} - Will throw if the node isn't in the graph. */ inboundDegreeWithoutSelfLoops(node) { node = '' + node; const nodeData = this._nodes.get(node); if (!nodeData) throw new NotFoundGraphError( `Graph.inboundDegreeWithoutSelfLoops: could not find the "${node}" node in the graph.` ); let degree = 0; let loops = 0; if (this.type !== 'directed') { degree += nodeData.undirectedDegree; loops += nodeData.undirectedLoops * 2; } if (this.type !== 'undirected') { degree += nodeData.inDegree; loops += nodeData.directedLoops; } return degree - loops; } /** * Method returning the given node's outbound degree without considering self loops. * * @param {any} node - The node's key. * @return {number} - The node's outbound degree. * * @throws {Error} - Will throw if the node isn't in the graph. */ outboundDegreeWithoutSelfLoops(node) { node = '' + node; const nodeData = this._nodes.get(node); if (!nodeData) throw new NotFoundGraphError( `Graph.outboundDegreeWithoutSelfLoops: could not find the "${node}" node in the graph.` ); let degree = 0; let loops = 0; if (this.type !== 'directed') { degree += nodeData.undirectedDegree; loops += nodeData.undirectedLoops * 2; } if (this.type !== 'undirected') { degree += nodeData.outDegree; loops += nodeData.directedLoops; } return degree - loops; } /** * Method returning the given node's directed degree without considering self loops. * * @param {any} node - The node's key. * @return {number} - The node's degree. * * @throws {Error} - Will throw if the node isn't in the graph. */ degreeWithoutSelfLoops(node) { node = '' + node; const nodeData = this._nodes.get(node); if (!nodeData) throw new NotFoundGraphError( `Graph.degreeWithoutSelfLoops: could not find the "${node}" node in the graph.` ); let degree = 0; let loops = 0; if (this.type !== 'directed') { degree += nodeData.undirectedDegree; loops += nodeData.undirectedLoops * 2; } if (this.type !== 'undirected') { degree += nodeData.inDegree + nodeData.outDegree; loops += nodeData.directedLoops * 2; } return degree - loops; } /** * Method returning the given edge's source. * * @param {any} edge - The edge's key. * @return {any} - The edge's source. * * @throws {Error} - Will throw if the edge isn't in the graph. */ source(edge) { edge = '' + edge; const data = this._edges.get(edge); if (!data) throw new NotFoundGraphError( `Graph.source: could not find the "${edge}" edge in the graph.` ); return data.source.key; } /** * Method returning the given edge's target. * * @param {any} edge - The edge's key. * @return {any} - The edge's target. * * @throws {Error} - Will throw if the edge isn't in the graph. */ target(edge) { edge = '' + edge; const data = this._edges.get(edge); if (!data) throw new NotFoundGraphError( `Graph.target: could not find the "${edge}" edge in the graph.` ); return data.target.key; } /** * Method returning the given edge's extremities. * * @param {any} edge - The edge's key. * @return {array} - The edge's extremities. * * @throws {Error} - Will throw if the edge isn't in the graph. */ extremities(edge) { edge = '' + edge; const edgeData = this._edges.get(edge); if (!edgeData) throw new NotFoundGraphError( `Graph.extremities: could not find the "${edge}" edge in the graph.` ); return [edgeData.source.key, edgeData.target.key]; } /** * Given a node & an edge, returns the other extremity of the edge. * * @param {any} node - The node's key. * @param {any} edge - The edge's key. * @return {any} - The related node. * * @throws {Error} - Will throw if the edge isn't in the graph or if the * edge & node are not related. */ opposite(node, edge) { node = '' + node; edge = '' + edge; const data = this._edges.get(edge); if (!data) throw new NotFoundGraphError( `Graph.opposite: could not find the "${edge}" edge in the graph.` ); const source = data.source.key; const target = data.target.key; if (node === source) return target; if (node === target) return source; throw new NotFoundGraphError( `Graph.opposite: the "${node}" node is not attached to the "${edge}" edge (${source}, ${target}).` ); } /** * Returns whether the given edge has the given node as extremity. * * @param {any} edge - The edge's key. * @param {any} node - The node's key. * @return {boolean} - The related node. * * @throws {Error} - Will throw if either the node or the edge isn't in the graph. */ hasExtremity(edge, node) { edge = '' + edge; node = '' + node; const data = this._edges.get(edge); if (!data) throw new NotFoundGraphError( `Graph.hasExtremity: could not find the "${edge}" edge in the graph.` ); return data.source.key === node || data.target.key === node; } /** * Method returning whether the given edge is undirected. * * @param {any} edge - The edge's key. * @return {boolean} * * @throws {Error} - Will throw if the edge isn't in the graph. */ isUndirected(edge) { edge = '' + edge; const data = this._edges.get(edge); if (!data) throw new NotFoundGraphError( `Graph.isUndirected: could not find the "${edge}" edge in the graph.` ); return data.undirected; } /** * Method returning whether the given edge is directed. * * @param {any} edge - The edge's key. * @return {boolean} * * @throws {Error} - Will throw if the edge isn't in the graph. */ isDirected(edge) { edge = '' + edge; const data = this._edges.get(edge); if (!data) throw new NotFoundGraphError( `Graph.isDirected: could not find the "${edge}" edge in the graph.` ); return !data.undirected; } /** * Method returning whether the given edge is a self loop. * * @param {any} edge - The edge's key. * @return {boolean} * * @throws {Error} - Will throw if the edge isn't in the graph. */ isSelfLoop(edge) { edge = '' + edge; const data = this._edges.get(edge); if (!data) throw new NotFoundGraphError( `Graph.isSelfLoop: could not find the "${edge}" edge in the graph.` ); return data.source === data.target; } /**--------------------------------------------------------------------------- * Mutation **--------------------------------------------------------------------------- */ /** * Method used to add a node to the graph. * * @param {any} node - The node. * @param {object} [attributes] - Optional attributes. * @return {any} - The node. * * @throws {Error} - Will throw if the given node already exist. * @throws {Error} - Will throw if the given attributes are not an object. */ addNode(node, attributes) { const nodeData = addNode(this, node, attributes); return nodeData.key; } /** * Method used to merge a node into the graph. * * @param {any} node - The node. * @param {object} [attributes] - Optional attributes. * @return {any} - The node. */ mergeNode(node, attributes) { if (attributes && !isPlainObject(attributes)) throw new InvalidArgumentsGraphError( `Graph.mergeNode: invalid attributes. Expecting an object but got "${attributes}"` ); // String coercion node = '' + node; attributes = attributes || {}; // If the node already exists, we merge the attributes let data = this._nodes.get(node); if (data) { if (attributes) { assign(data.attributes, attributes); this.emit('nodeAttributesUpdated', { type: 'merge', key: node, attributes: data.attributes, data: attributes }); } return [node, false]; } data = new this.NodeDataClass(node, attributes); // Adding the node to internal register this._nodes.set(node, data); // Emitting this.emit('nodeAdded', { key: node, attributes }); return [node, true]; } /** * Method used to add a node if it does not exist in the graph or else to * update its attributes using a function. * * @param {any} node - The node. * @param {function} [updater] - Optional updater function. * @return {any} - The node. */ updateNode(node, updater) { if (updater && typeof updater !== 'function') throw new InvalidArgumentsGraphError( `Graph.updateNode: invalid updater function. Expecting a function but got "${updater}"` ); // String coercion node = '' + node; // If the node already exists, we update the attributes let data = this._nodes.get(node); if (data) { if (updater) { const oldAttributes = data.attributes; data.attributes = updater(oldAttributes); this.emit('nodeAttributesUpdated', { type: 'replace', key: node, attributes: data.attributes }); } return [node, false]; } const attributes = updater ? updater({}) : {}; data = new this.NodeDataClass(node, attributes); // Adding the node to internal register this._nodes.set(node, data); // Emitting this.emit('nodeAdded', { key: node, attributes }); return [node, true]; } /** * Method used to drop a single node & all its attached edges from the graph. * * @param {any} node - The node. * @return {Graph} * * @throws {Error} - Will throw if the node doesn't exist. */ dropNode(node) { node = '' + node; const nodeData = this._nodes.get(node); if (!nodeData) throw new NotFoundGraphError( `Graph.dropNode: could not find the "${node}" node in the graph.` ); let edgeData; // Removing attached edges // NOTE: we could be faster here, but this is such a pain to maintain if (this.type !== 'undirected') { for (const neighbor in nodeData.out) { edgeData = nodeData.out[neighbor]; do { dropEdgeFromData(this, edgeData); edgeData = edgeData.next; } while (edgeData); } for (const neighbor in nodeData.in) { edgeData = nodeData.in[neighbor]; do { dropEdgeFromData(this, edgeData); edgeData = edgeData.next; } while (edgeData); } } if (this.type !== 'directed') { for (const neighbor in nodeData.undirected) { edgeData = nodeData.undirected[neighbor]; do { dropEdgeFromData(this, edgeData); edgeData = edgeData.next; } while (edgeData); } } // Dropping the node from the register this._nodes.delete(node); // Emitting this.emit('nodeDropped', { key: node, attributes: nodeData.attributes }); } /** * Method used to drop a single edge from the graph. * * Arity 1: * @param {any} edge - The edge. * * Arity 2: * @param {any} source - Source node. * @param {any} target - Target node. * * @return {Graph} * * @throws {Error} - Will throw if the edge doesn't exist. */ dropEdge(edge) { let edgeData; if (arguments.length > 1) { const source = '' + arguments[0]; const target = '' + arguments[1]; edgeData = getMatchingEdge(this, source, target, this.type); if (!edgeData) throw new NotFoundGraphError( `Graph.dropEdge: could not find the "${source}" -> "${target}" edge in the graph.` ); } else { edge = '' + edge; edgeData = this._edges.get(edge); if (!edgeData) throw new NotFoundGraphError( `Graph.dropEdge: could not find the "${edge}" edge in the graph.` ); } dropEdgeFromData(this, edgeData); return this; } /** * Method used to drop a single directed edge from the graph. * * @param {any} source - Source node. * @param {any} target - Target node. * * @return {Graph} * * @throws {Error} - Will throw if the edge doesn't exist. */ dropDirectedEdge(source, target) { if (arguments.length < 2) throw new UsageGraphError( 'Graph.dropDirectedEdge: it does not make sense to try and drop a directed edge by key. What if the edge with this key is undirected? Use #.dropEdge for this purpose instead.' ); if (this.multi) throw new UsageGraphError( 'Graph.dropDirectedEdge: cannot use a {source,target} combo when dropping an edge in a MultiGraph since we cannot infer the one you want to delete as there could be multiple ones.' ); source = '' + source; target = '' + target; const edgeData = getMatchingEdge(this, source, target, 'directed'); if (!edgeData) throw new NotFoundGraphError( `Graph.dropDirectedEdge: could not find a "${source}" -> "${target}" edge in the graph.` ); dropEdgeFromData(this, edgeData); return this; } /** * Method used to drop a single undirected edge from the graph. * * @param {any} source - Source node. * @param {any} target - Target node. * * @return {Graph} * * @throws {Error} - Will throw if the edge doesn't exist. */ dropUndirectedEdge(source, target) { if (arguments.length < 2) throw new UsageGraphError( 'Graph.dropUndirectedEdge: it does not make sense to drop a directed edge by key. What if the edge with this key is undirected? Use #.dropEdge for this purpose instead.' ); if (this.multi) throw new UsageGraphError( 'Graph.dropUndirectedEdge: cannot use a {source,target} combo when dropping an edge in a MultiGraph since we cannot infer the one you want to delete as there could be multiple ones.' ); const edgeData = getMatchingEdge(this, source, target, 'undirected'); if (!edgeData) throw new NotFoundGraphError( `Graph.dropUndirectedEdge: could not find a "${source}" -> "${target}" edge in the graph.` ); dropEdgeFromData(this, edgeData); return this; } /** * Method used to remove every edge & every node from the graph. * * @return {Graph} */ clear() { // Clearing edges this._edges.clear(); // Clearing nodes this._nodes.clear(); // Reset counters this._resetInstanceCounters(); // Emitting this.emit('cleared'); } /** * Method used to remove every edge from the graph. * * @return {Graph} */ clearEdges() { // Clearing structure index const iterator = this._nodes.values(); let step; while (((step = iterator.next()), step.done !== true)) { step.value.clear(); } // Clearing edges this._edges.clear(); // Reset counters this._resetInstanceCounters(); // Emitting this.emit('edgesCleared'); } /**--------------------------------------------------------------------------- * Attributes-related methods **--------------------------------------------------------------------------- */ /** * Method returning the desired graph's attribute. * * @param {string} name - Name of the attribute. * @return {any} */ getAttribute(name) { return this._attributes[name]; } /** * Method returning the graph's attributes. * * @return {object} */ getAttributes() { return this._attributes; } /** * Method returning whether the graph has the desired attribute. * * @param {string} name - Name of the attribute. * @return {boolean} */ hasAttribute(name) { return this._attributes.hasOwnProperty(name); } /** * Method setting a value for the desired graph's attribute. * * @param {string} name - Name of the attribute. * @param {any} value - Value for the attribute. * @return {Graph} */ setAttribute(name, value) { this._attributes[name] = value; // Emitting this.emit('attributesUpdated', { type: 'set', attributes: this._attributes, name }); return this; } /** * Method using a function to update the desired graph's attribute's value. * * @param {string} name - Name of the attribute. * @param {function} updater - Function use to update the attribute's value. * @return {Graph} */ updateAttribute(name, updater) { if (typeof updater !== 'function') throw new InvalidArgumentsGraphError( 'Graph.updateAttribute: updater should be a function.' ); const value = this._attributes[name]; this._attributes[name] = updater(value); // Emitting this.emit('attributesUpdated', { type: 'set', attributes: this._attributes, name }); return this; } /** * Method removing the desired graph's attribute. * * @param {string} name - Name of the attribute. * @return {Graph} */ removeAttribute(name) { delete this._attributes[name]; // Emitting this.emit('attributesUpdated', { type: 'remove', attributes: this._attributes, name }); return this; } /** * Method replacing the graph's attributes. * * @param {object} attributes - New attributes. * @return {Graph} * * @throws {Error} - Will throw if given attributes are not a plain object. */ replaceAttributes(attributes) { if (!isPlainObject(attributes)) throw new InvalidArgumentsGraphError( 'Graph.replaceAttributes: provided attributes are not a plain object.' ); this._attributes = attributes; // Emitting this.emit('attributesUpdated', { type: 'replace', attributes: this._attributes }); return this; } /** * Method merging the graph's attributes. * * @param {object} attributes - Attributes to merge. * @return {Graph} * * @throws {Error} - Will throw if given attributes are not a plain object. */ mergeAttributes(attributes) { if (!isPlainObject(attributes)) throw new InvalidArgumentsGraphError( 'Graph.mergeAttributes: provided attributes are not a plain object.' ); assign(this._attributes, attributes); // Emitting this.emit('attributesUpdated', { type: 'merge', attributes: this._attributes, data: attributes }); return this; } /** * Method updating the graph's attributes. * * @param {function} updater - Function used to update the attributes. * @return {Graph} * * @throws {Error} - Will throw if given updater is not a function. */ updateAttributes(updater) { if (typeof updater !== 'function') throw new InvalidArgumentsGraphError( 'Graph.updateAttributes: provided updater is not a function.' ); this._attributes = updater(this._attributes); // Emitting this.emit('attributesUpdated', { type: 'update', attributes: this._attributes }); return this; } /** * Method used to update each node's attributes using the given function. * * @param {function} updater - Updater function to use. * @param {object} [hints] - Optional hints. */ updateEachNodeAttributes(updater, hints) { if (typeof updater !== 'function') throw new InvalidArgumentsGraphError( 'Graph.updateEachNodeAttributes: expecting an updater function.' ); if (hints && !validateHints(hints)) throw new InvalidArgumentsGraphError( 'Graph.updateEachNodeAttributes: invalid hints. Expecting an object having the following shape: {attributes?: [string]}' ); const iterator = this._nodes.values(); let step, nodeData; while (((step = iterator.next()), step.done !== true)) { nodeData = step.value; nodeData.attributes = updater(nodeData.key, nodeData.attributes); } this.emit('eachNodeAttributesUpdated', { hints: hints ? hints : null }); } /** * Method used to update each edge's attributes using the given function. * * @param {function} updater - Updater function to use. * @param {object} [hints] - Optional hints. */ updateEachEdgeAttributes(updater, hints) { if (typeof updater !== 'function') throw new InvalidArgumentsGraphError( 'Graph.updateEachEdgeAttributes: expecting an updater function.' ); if (hints && !validateHints(hints)) throw new InvalidArgumentsGraphError( 'Graph.updateEachEdgeAttributes: invalid hints. Expecting an object having the following shape: {attributes?: [string]}' ); const iterator = this._edges.values(); let step, edgeData, sourceData, targetData; while (((step = iterator.next()), step.done !== true)) { edgeData = step.value; sourceData = edgeData.source; targetData = edgeData.target; edgeData.attributes = updater( edgeData.key, edgeData.attributes, sourceData.key, targetData.key, sourceData.attributes, targetData.attributes, edgeData.undirected ); } this.emit('eachEdgeAttributesUpdated', { hints: hints ? hints : null }); } /**--------------------------------------------------------------------------- * Iteration-related methods **--------------------------------------------------------------------------- */ /** * Method iterating over the graph's adjacency using the given callback. * * @param {function} callback - Callback to use. */ forEachAdjacencyEntry(callback) { if (typeof callback !== 'function') throw new InvalidArgumentsGraphError( 'Graph.forEachAdjacencyEntry: expecting a callback.' ); forEachAdjacency(false, false, false, this, callback); } forEachAdjacencyEntryWithOrphans(callback) { if (typeof callback !== 'function') throw new InvalidArgumentsGraphError( 'Graph.forEachAdjacencyEntryWithOrphans: expecting a callback.' ); forEachAdjacency(false, false, true, this, callback); } /** * Method iterating over the graph's assymetric adjacency using the given callback. * * @param {function} callback - Callback to use. */ forEachAssymetricAdjacencyEntry(callback) { if (typeof callback !== 'function') throw new InvalidArgumentsGraphError( 'Graph.forEachAssymetricAdjacencyEntry: expecting a callback.' ); forEachAdjacency(false, true, false, this, callback); } forEachAssymetricAdjacencyEntryWithOrphans(callback) { if (typeof callback !== 'function') throw new InvalidArgumentsGraphError( 'Graph.forEachAssymetricAdjacencyEntryWithOrphans: expecting a callback.' ); forEachAdjacency(false, true, true, this, callback); } /** * Method returning the list of the graph's nodes. * * @return {array} - The nodes. */ nodes() { if (typeof Array.from === 'function') return Array.from(this._nodes.keys()); return take(this._nodes.keys(), this._nodes.size); } /** * Method iterating over the graph's nodes using the given callback. * * @param {function} callback - Callback (key, attributes, index). */ forEachNode(callback) { if (typeof callback !== 'function') throw new InvalidArgumentsGraphError( 'Graph.forEachNode: expecting a callback.' ); const iterator = this._nodes.values(); let step, nodeData; while (((step = iterator.next()), step.done !== true)) { nodeData = step.value; callback(nodeData.key, nodeData.attributes); } } /** * Method iterating attempting to find a node matching the given predicate * function. * * @param {function} callback - Callback (key, attributes). */ findNode(callback) { if (typeof callback !== 'function') throw new InvalidArgumentsGraphError( 'Graph.findNode: expecting a callback.' ); const iterator = this._nodes.values(); let step, nodeData; while (((step = iterator.next()), step.done !== true)) { nodeData = step.value; if (callback(nodeData.key, nodeData.attributes)) return nodeData.key; } return; } /** * Method mapping nodes. * * @param {function} callback - Callback (key, attributes). */ mapNodes(callback) { if (typeof callback !== 'function') throw new InvalidArgumentsGraphError( 'Graph.mapNode: expecting a callback.' ); const iterator = this._nodes.values(); let step, nodeData; const result = new Array(this.order); let i = 0; while (((step = iterator.next()), step.done !== true)) { nodeData = step.value; result[i++] = callback(nodeData.key, nodeData.attributes); } return result; } /** * Method returning whether some node verify the given predicate. * * @param {function} callback - Callback (key, attributes). */ someNode(callback) { if (typeof callback !== 'function') throw new InvalidArgumentsGraphError( 'Graph.someNode: expecting a callback.' ); const iterator = this._nodes.values(); let step, nodeData; while (((step = iterator.next()), step.done !== true)) { nodeData = step.value; if (callback(nodeData.key, nodeData.attributes)) return true; } return false; } /** * Method returning whether all node verify the given predicate. * * @param {function} callback - Callback (key, attributes). */ everyNode(callback) { if (typeof callback !== 'function') throw new InvalidArgumentsGraphError( 'Graph.everyNode: expecting a callback.' ); const iterator = this._nodes.values(); let step, nodeData; while (((step = iterator.next()), step.done !== true)) { nodeData = step.value; if (!callback(nodeData.key, nodeData.attributes)) return false; } return true; } /** * Method filtering nodes. * * @param {function} callback - Callback (key, attributes). */ filterNodes(callback) { if (typeof callback !== 'function') throw new InvalidArgumentsGraphError( 'Graph.filterNodes: expecting a callback.' ); const iterator = this._nodes.values(); let step, nodeData; const result = []; while (((step = iterator.next()), step.done !== true)) { nodeData = step.value; if (callback(nodeData.key, nodeData.attributes)) result.push(nodeData.key); } return result; } /** * Method reducing nodes. * * @param {function} callback - Callback (accumulator, key, attributes). */ reduceNodes(callback, initialValue) { if (typeof callback !== 'function') throw new InvalidArgumentsGraphError( 'Graph.reduceNodes: expecting a callback.' ); if (arguments.length < 2) throw new InvalidArgumentsGraphError( 'Graph.reduceNodes: missing initial value. You must provide it because the callback takes more than one argument and we cannot infer the initial value from the first iteration, as you could with a simple array.' ); let accumulator = initialValue; const iterator = this._nodes.values(); let step, nodeData; while (((step = iterator.next()), step.done !== true)) { nodeData = step.value; accumulator = callback(accumulator, nodeData.key, nodeData.attributes); } return accumulator; } /** * Method returning an iterator over the graph's node entries. * * @return {Iterator} */ nodeEntries() { const iterator = this._nodes.values(); return new Iterator(() => { const step = iterator.next(); if (step.done) return step; const data = step.value; return { value: {node: data.key, attributes: data.attributes}, done: false }; }); } /**--------------------------------------------------------------------------- * Serialization **--------------------------------------------------------------------------- */ /** * Method used to export the whole graph. * * @return {object} - The serialized graph. */ export() { const nodes = new Array(this._nodes.size); let i = 0; this._nodes.forEach((data, key) => { nodes[i++] = serializeNode(key, data); }); const edges = new Array(this._edges.size); i = 0; this._edges.forEach((data, key) => { edges[i++] = serializeEdge(this.type, key, data); }); return { options: { type: this.type, multi: this.multi, allowSelfLoops: this.allowSelfLoops }, attributes: this.getAttributes(), nodes, edges }; } /** * Method used to import a serialized graph. * * @param {object|Graph} data - The serialized graph. * @param {boolean} merge - Whether to merge data. * @return {Graph} - Returns itself for chaining. */ import(data, merge = false) { // Importing a Graph instance directly if (data instanceof Graph) { // Nodes data.forEachNode((n, a) => { if (merge) this.mergeNode(n, a); else this.addNode(n, a); }); // Edges data.forEachEdge((e, a, s, t, _sa, _ta, u) => { if (merge) { if (u) this.mergeUndirectedEdgeWithKey(e, s, t, a); else this.mergeDirectedEdgeWithKey(e, s, t, a); } else { if (u) this.addUndirectedEdgeWithKey(e, s, t, a); else this.addDirectedEdgeWithKey(e, s, t, a); } }); return this; } // Importing a serialized graph if (!isPlainObject(data)) throw new InvalidArgumentsGraphError( 'Graph.import: invalid argument. Expecting a serialized graph or, alternatively, a Graph instance.' ); if (data.attributes) { if (!isPlainObject(data.attributes)) throw new InvalidArgumentsGraphError( 'Graph.import: invalid attributes. Expecting a plain object.' ); if (merge) this.mergeAttributes(data.attributes); else this.replaceAttributes(data.attributes); } let i, l, list, node, edge; if (data.nodes) { list = data.nodes; if (!Array.isArray(list)) throw new InvalidArgumentsGraphError( 'Graph.import: invalid nodes. Expecting an array.' ); for (i = 0, l = list.length; i < l; i++) { node = list[i]; // Validating validateSerializedNode(node); // Adding the node const {key, attributes} = node; if (merge) this.mergeNode(key, attributes); else this.addNode(key, attributes); } } if (data.edges) { let undirectedByDefault = false; if (this.type === 'undirected') { undirectedByDefault = true; } list = data.edges; if (!Array.isArray(list)) throw new InvalidArgumentsGraphError( 'Graph.import: invalid edges. Expecting an array.' ); for (i = 0, l = list.length; i < l; i++) { edge = list[i]; // Validating validateSerializedEdge(edge); // Adding the edge const { source, target, attributes, undirected = undirectedByDefault } = edge; let method; if ('key' in edge) { method = merge ? undirected ? this.mergeUndirectedEdgeWithKey : this.mergeDirectedEdgeWithKey : undirected ? this.addUndirectedEdgeWithKey : this.addDirectedEdgeWithKey; method.call(this, edge.key, source, target, attributes); } else { method = merge ? undirected ? this.mergeUndirectedEdge : this.mergeDirectedEdge : undirected ? this.addUndirectedEdge : this.addDirectedEdge; method.call(this, source, target, attributes); } } } return this; } /**--------------------------------------------------------------------------- * Utils **--------------------------------------------------------------------------- */ /** * Method returning a null copy of the graph, i.e. a graph without nodes * & edges but with the exact same options. * * @param {object} options - Options to merge with the current ones. * @return {Graph} - The null copy. */ nullCopy(options) { const graph = new Graph(assign({}, this._options, options)); graph.replaceAttributes(assign({}, this.getAttributes())); return graph; } /** * Method returning an empty copy of the graph, i.e. a graph without edges but * with the exact same options. * * @param {object} options - Options to merge with the current ones. * @return {Graph} - The empty copy. */ emptyCopy(options) { const graph = this.nullCopy(options); this._nodes.forEach((nodeData, key) => { const attributes = assign({}, nodeData.attributes); // NOTE: no need to emit events since user cannot access the instance yet nodeData = new graph.NodeDataClass(key, attributes); graph._nodes.set(key, nodeData); }); return graph; } /** * Method returning an exact copy of the graph. * * @param {object} options - Upgrade options. * @return {Graph} - The copy. */ copy(options) { options = options || {}; if ( typeof options.type === 'string' && options.type !== this.type && options.type !== 'mixed' ) throw new UsageGraphError( `Graph.copy: cannot create an incompatible copy from "${this.type}" type to "${options.type}" because this would mean losing information about the current graph.` ); if ( typeof options.multi === 'boolean' && options.multi !== this.multi && options.multi !== true ) throw new UsageGraphError( 'Graph.copy: cannot create an incompatible copy by downgrading a multi graph to a simple one because this would mean losing information about the current graph.' ); if ( typeof options.allowSelfLoops === 'boolean' && options.allowSelfLoops !== this.allowSelfLoops && options.allowSelfLoops !== true ) throw new UsageGraphError( 'Graph.copy: cannot create an incompatible copy from a graph allowing self loops to one that does not because this would mean losing information about the current graph.' ); const graph = this.emptyCopy(options); const iterator = this._edges.values(); let step, edgeData; while (((step = iterator.next()), step.done !== true)) { edgeData = step.value; // NOTE: no need to emit events since user cannot access the instance yet addEdge( graph, 'copy', false, edgeData.undirected, edgeData.key, edgeData.source.key, edgeData.target.key, assign({}, edgeData.attributes) ); } return graph; } /**--------------------------------------------------------------------------- * Known methods **--------------------------------------------------------------------------- */ /** * Method used by JavaScript to perform JSON serialization. * * @return {object} - The serialized graph. */ toJSON() { return this.export(); } /** * Method returning [object Graph]. */ toString() { return '[object Graph]'; } /** * Method used internally by node's console to display a custom object. * * @return {object} - Formatted object representation of the graph. */ inspect() { const nodes = {}; this._nodes.forEach((data, key) => { nodes[key] = data.attributes; }); const edges = {}, multiIndex = {}; this._edges.forEach((data, key) => { const direction = data.undirected ? '--' : '->'; let label = ''; let source = data.source.key; let target = data.target.key; let tmp; if (data.undirected && source > target) { tmp = source; source = target; target = tmp; } const desc = `(${source})${direction}(${target})`; if (!key.startsWith('geid_')) { label += `[${key}]: `; } else if (this.multi) { if (typeof multiIndex[desc] === 'undefined') { multiIndex[desc] = 0; } else { multiIndex[desc]++; } label += `${multiIndex[desc]}. `; } label += desc; edges[label] = data.attributes; }); const dummy = {}; for (const k in this) { if ( this.hasOwnProperty(k) && !EMITTER_PROPS.has(k) && typeof this[k] !== 'function' && typeof k !== 'symbol' ) dummy[k] = this[k]; } dummy.attributes = this._attributes; dummy.nodes = nodes; dummy.edges = edges; privateProperty(dummy, 'constructor', this.constructor); return dummy; } } /** * Attaching methods to the prototype. * * Here, we are attaching a wide variety of methods to the Graph class' * prototype when those are very numerous and when their creation is * abstracted. */ /** * Attaching custom inspect method for node >= 10. */ if (typeof Symbol !== 'undefined') Graph.prototype[Symbol.for('nodejs.util.inspect.custom')] = Graph.prototype.inspect; /** * Related to edge addition. */ EDGE_ADD_METHODS.forEach(method => { ['add', 'merge', 'update'].forEach(verb => { const name = method.name(verb); const fn = verb === 'add' ? addEdge : mergeEdge; if (method.generateKey) { Graph.prototype[name] = function (source, target, attributes) { return fn( this, name, true, (method.type || this.type) === 'undirected', null, source, target, attributes, verb === 'update' ); }; } else { Graph.prototype[name] = function (edge, source, target, attributes) { return fn( this, name, false, (method.type || this.type) === 'undirected', edge, source, target, attributes, verb === 'update' ); }; } }); }); /** * Attributes-related. */ attachNodeAttributesMethods(Graph); attachEdgeAttributesMethods(Graph); /** * Edge iteration-related. */ attachEdgeIterationMethods(Graph); /** * Neighbor iteration-related. */ attachNeighborIterationMethods(Graph); /** * Graphology Helper Classes * ========================== * * Building some higher-order classes instantiating the graph with * predefinite options. */ /** * Alternative constructors. */ class DirectedGraph extends Graph { constructor(options) { const finalOptions = assign({type: 'directed'}, options); if ('multi' in finalOptions && finalOptions.multi !== false) throw new InvalidArgumentsGraphError( 'DirectedGraph.from: inconsistent indication that the graph should be multi in given options!' ); if (finalOptions.type !== 'directed') throw new InvalidArgumentsGraphError( 'DirectedGraph.from: inconsistent "' + finalOptions.type + '" type in given options!' ); super(finalOptions); } } class UndirectedGraph extends Graph { constructor(options) { const finalOptions = assign({type: 'undirected'}, options); if ('multi' in finalOptions && finalOptions.multi !== false) throw new InvalidArgumentsGraphError( 'UndirectedGraph.from: inconsistent indication that the graph should be multi in given options!' ); if (finalOptions.type !== 'undirected') throw new InvalidArgumentsGraphError( 'UndirectedGraph.from: inconsistent "' + finalOptions.type + '" type in given options!' ); super(finalOptions); } } class MultiGraph extends Graph { constructor(options) { const finalOptions = assign({multi: true}, options); if ('multi' in finalOptions && finalOptions.multi !== true) throw new InvalidArgumentsGraphError( 'MultiGraph.from: inconsistent indication that the graph should be simple in given options!' ); super(finalOptions); } } class MultiDirectedGraph extends Graph { constructor(options) { const finalOptions = assign({type: 'directed', multi: true}, options); if ('multi' in finalOptions && finalOptions.multi !== true) throw new InvalidArgumentsGraphError( 'MultiDirectedGraph.from: inconsistent indication that the graph should be simple in given options!' ); if (finalOptions.type !== 'directed') throw new InvalidArgumentsGraphError( 'MultiDirectedGraph.from: inconsistent "' + finalOptions.type + '" type in given options!' ); super(finalOptions); } } class MultiUndirectedGraph extends Graph { constructor(options) { const finalOptions = assign({type: 'undirected', multi: true}, options); if ('multi' in finalOptions && finalOptions.multi !== true) throw new InvalidArgumentsGraphError( 'MultiUndirectedGraph.from: inconsistent indication that the graph should be simple in given options!' ); if (finalOptions.type !== 'undirected') throw new InvalidArgumentsGraphError( 'MultiUndirectedGraph.from: inconsistent "' + finalOptions.type + '" type in given options!' ); super(finalOptions); } } /** * Attaching static #.from method to each of the constructors. */ function attachStaticFromMethod(Class) { /** * Builds a graph from serialized data or another graph's data. * * @param {Graph|SerializedGraph} data - Hydratation data. * @param {object} [options] - Options. * @return {Class} */ Class.from = function (data, options) { // Merging given options with serialized ones const finalOptions = assign({}, data.options, options); const instance = new Class(finalOptions); instance.import(data); return instance; }; } attachStaticFromMethod(Graph); attachStaticFromMethod(DirectedGraph); attachStaticFromMethod(UndirectedGraph); attachStaticFromMethod(MultiGraph); attachStaticFromMethod(MultiDirectedGraph); attachStaticFromMethod(MultiUndirectedGraph); Graph.Graph = Graph; Graph.DirectedGraph = DirectedGraph; Graph.UndirectedGraph = UndirectedGraph; Graph.MultiGraph = MultiGraph; Graph.MultiDirectedGraph = MultiDirectedGraph; Graph.MultiUndirectedGraph = MultiUndirectedGraph; Graph.InvalidArgumentsGraphError = InvalidArgumentsGraphError; Graph.NotFoundGraphError = NotFoundGraphError; Graph.UsageGraphError = UsageGraphError; /** * Graphology ESM Endoint * ======================= * * Endpoint for ESM modules consumers. */ export { DirectedGraph, Graph, InvalidArgumentsGraphError, MultiDirectedGraph, MultiGraph, MultiUndirectedGraph, NotFoundGraphError, UndirectedGraph, UsageGraphError, Graph as default }; //# sourceMappingURL=graphology.mjs.map