import { EventEmitter } from 'events' ; import * as clone from 'clone' ; import { writeFile , readFile } from 'fs' ; import { isBrowser } from './Platform' ; import { JournalMetadata , GraphOptions , PropertyMap , GraphNode , GraphNodeMetadata , GraphNodeID , GraphEdge , GraphEdgeMetadata , GraphJsonEdge , GraphIIP , GraphIIPMetadata , GraphExportedPort , GraphGroup , GraphGroupMetadata , GraphJson , } from './Types' ; class Graph extends EventEmitter { name : string ; properties : PropertyMap ; nodes : Array < GraphNode > ; edges : Array < GraphEdge > ; initializers : Array < GraphIIP > ; inports : { [ key ] : GraphExportedPort ; } ; outports : { [ key ] : GraphExportedPort ; } ; groups : Array < GraphGroup > ; transaction : { id : string | null ; depth : number ; } ; caseSensitive : boolean ; constructor ( name = '' , options = { } ) { super ( ) ; this . setMaxListeners ( 0 ) ; this . name = name ; this . properties = { } ; this . nodes = [ ] ; this . edges = [ ] ; this . initializers = [ ] ; this . inports = { } ; this . outports = { } ; this . groups = [ ] ; this . transaction = { id : null , depth : 0 , } ; this . caseSensitive = options . caseSensitive || false ; } getPortName ( port = '' ) { if ( this . caseSensitive ) { return port ; } return port . toLowerCase ( ) ; } startTransaction ( id , metadata = { } ) { if ( this . transaction . id ) { throw Error ( 'Nested transactions not supported' ) ; } this . transaction . id = id ; this . transaction . depth = 1 ; this . emit ( 'startTransaction' , id , metadata ) ; return this ; } endTransaction ( id , metadata = { } ) { if ( ! this . transaction . id ) { throw Error ( 'Attempted to end non-existing transaction' ) ; } this . transaction . id = null ; this . transaction . depth = 0 ; this . emit ( 'endTransaction' , id , metadata ) ; return this ; } checkTransactionStart ( ) { if ( ! this . transaction . id ) { this . startTransaction ( 'implicit' ) ; } else if ( this . transaction . id === 'implicit' ) { this . transaction . depth += 1 ; } return this ; } checkTransactionEnd ( ) { if ( this . transaction . id === 'implicit' ) { this . transaction . depth -= 1 ; } if ( this . transaction . depth === 0 ) { this . endTransaction ( 'implicit' ) ; } return this ; } setProperties ( properties ) { this . checkTransactionStart ( ) ; const before = clone ( this . properties ) ; Object . keys ( properties ) . forEach ( ( item ) => { const val = properties [ item ] ; this . properties [ item ] = val ; } ) ; this . emit ( 'changeProperties' , this . properties , before ) ; this . checkTransactionEnd ( ) ; return this ; } addInport ( publicPort , nodeKey , portKey , metadata = { } ) { if ( ! this . getNode ( nodeKey ) ) { return this ; } const portName = this . getPortName ( publicPort ) ; this . checkTransactionStart ( ) ; this . inports [ portName ] = { process : nodeKey , port : this . getPortName ( portKey ) , metadata , } ; this . emit ( 'addInport' , portName , this . inports [ portName ] ) ; this . checkTransactionEnd ( ) ; return this ; } removeInport ( publicPort ) { const portName = this . getPortName ( publicPort ) ; if ( ! this . inports [ portName ] ) { return this ; } this . checkTransactionStart ( ) ; const port = this . inports [ portName ] ; this . setInportMetadata ( portName , { } ) ; delete this . inports [ portName ] ; this . emit ( 'removeInport' , portName , port ) ; this . checkTransactionEnd ( ) ; return this ; } renameInport ( oldPort , newPort ) { const oldPortName = this . getPortName ( oldPort ) ; const newPortName = this . getPortName ( newPort ) ; if ( ! this . inports [ oldPortName ] ) { return this ; } if ( newPortName === oldPortName ) { return this ; } this . checkTransactionStart ( ) ; this . inports [ newPortName ] = this . inports [ oldPortName ] ; delete this . inports [ oldPortName ] ; this . emit ( 'renameInport' , oldPortName , newPortName ) ; this . checkTransactionEnd ( ) ; return this ; } setInportMetadata ( publicPort , metadata ) { const portName = this . getPortName ( publicPort ) ; if ( ! this . inports [ portName ] ) { return this ; } this . checkTransactionStart ( ) ; if ( ! this . inports [ portName ] . metadata ) { this . inports [ portName ] . metadata = { } ; } const before = clone ( this . inports [ portName ] . metadata ) ; Object . keys ( metadata ) . forEach ( ( item ) => { const val = metadata [ item ] ; const existingMeta = this . inports [ portName ] . metadata ; if ( ! existingMeta ) { return ; } if ( val != null ) { existingMeta [ item ] = val ; } else { delete existingMeta [ item ] ; } } ) ; this . emit ( 'changeInport' , portName , this . inports [ portName ] , before , metadata ) ; this . checkTransactionEnd ( ) ; return this ; } addOutport ( publicPort , nodeKey , portKey , metadata = { } ) { if ( ! this . getNode ( nodeKey ) ) { return this ; } const portName = this . getPortName ( publicPort ) ; this . checkTransactionStart ( ) ; this . outports [ portName ] = { process : nodeKey , port : this . getPortName ( portKey ) , metadata , } ; this . emit ( 'addOutport' , portName , this . outports [ portName ] ) ; this . checkTransactionEnd ( ) ; return this ; } removeOutport ( publicPort ) { const portName = this . getPortName ( publicPort ) ; if ( ! this . outports [ portName ] ) { return this ; } this . checkTransactionStart ( ) ; const port = this . outports [ portName ] ; this . setOutportMetadata ( portName , { } ) ; delete this . outports [ portName ] ; this . emit ( 'removeOutport' , portName , port ) ; this . checkTransactionEnd ( ) ; return this ; } renameOutport ( oldPort , newPort ) { const oldPortName = this . getPortName ( oldPort ) ; const newPortName = this . getPortName ( newPort ) ; if ( ! this . outports [ oldPortName ] ) { return this ; } this . checkTransactionStart ( ) ; this . outports [ newPortName ] = this . outports [ oldPortName ] ; delete this . outports [ oldPortName ] ; this . emit ( 'renameOutport' , oldPortName , newPortName ) ; this . checkTransactionEnd ( ) ; return this ; } setOutportMetadata ( publicPort , metadata ) { const portName = this . getPortName ( publicPort ) ; if ( ! this . outports [ portName ] ) { return this ; } this . checkTransactionStart ( ) ; const before = clone ( this . outports [ portName ] . metadata ) ; if ( ! this . outports [ portName ] . metadata ) { this . outports [ portName ] . metadata = { } ; } Object . keys ( metadata ) . forEach ( ( item ) => { const val = metadata [ item ] ; const existingMeta = this . outports [ portName ] . metadata ; if ( ! existingMeta ) { return ; } if ( val != null ) { existingMeta [ item ] = val ; } else { delete existingMeta [ item ] ; } } ) ; this . emit ( 'changeOutport' , portName , this . outports [ portName ] , before , metadata ) ; this . checkTransactionEnd ( ) ; return this ; } addGroup ( group , nodes , metadata ) { this . checkTransactionStart ( ) ; const g = { name : group , nodes , metadata , } ; this . groups . push ( g ) ; this . emit ( 'addGroup' , g ) ; this . checkTransactionEnd ( ) ; return this ; } renameGroup ( oldName , newName ) { this . checkTransactionStart ( ) ; this . groups . forEach ( ( group ) => { if ( ! group ) { return ; } if ( group . name !== oldName ) { return ; } const g = group ; g . name = newName ; this . emit ( 'renameGroup' , oldName , newName ) ; } ) ; this . checkTransactionEnd ( ) ; return this ; } removeGroup ( groupName ) { this . checkTransactionStart ( ) ; this . groups = this . groups . filter ( ( group ) => { if ( ! group ) { return false ; } if ( group . name !== groupName ) { return true ; } this . setGroupMetadata ( group . name , { } ) ; this . emit ( 'removeGroup' , group ) ; return false ; } ) ; this . checkTransactionEnd ( ) ; return this ; } setGroupMetadata ( groupName , metadata ) { this . checkTransactionStart ( ) ; this . groups . forEach ( ( group ) => { if ( ! group ) { return ; } if ( group . name !== groupName ) { return ; } const before = clone ( group . metadata ) ; Object . keys ( metadata ) . forEach ( ( item ) => { const val = metadata [ item ] ; const g = group ; if ( ! g . metadata ) { return ; } if ( val != null ) { g . metadata [ item ] = val ; } else { delete g . metadata [ item ] ; } } ) ; this . emit ( 'changeGroup' , group , before , metadata ) ; } ) ; this . checkTransactionEnd ( ) ; return this ; } addNode ( id , component , metadata = { } ) { this . checkTransactionStart ( ) ; const node = { id , component , metadata , } ; this . nodes . push ( node ) ; this . emit ( 'addNode' , node ) ; this . checkTransactionEnd ( ) ; return this ; } removeNode ( id ) { const node = this . getNode ( id ) ; if ( ! node ) { return this ; } this . checkTransactionStart ( ) ; this . edges . forEach ( ( edge ) => { if ( ( edge . from . node === node . id ) || ( edge . to . node === node . id ) ) { this . removeEdge ( edge . from . node , edge . from . port , edge . to . node , edge . to . port ) ; } } ) ; this . initializers . forEach ( ( initializer ) => { if ( initializer . to . node === node . id ) { this . removeInitial ( initializer . to . node , initializer . to . port ) ; } } ) ; Object . keys ( this . inports ) . forEach ( ( pub ) => { const priv = this . inports [ pub ] ; if ( priv . process === id ) { this . removeInport ( pub ) ; } } ) ; Object . keys ( this . outports ) . forEach ( ( pub ) => { const priv = this . outports [ pub ] ; if ( priv . process === id ) { this . removeOutport ( pub ) ; } } ) ; this . groups . forEach ( ( group ) => { if ( ! group ) { return ; } const index = group . nodes . indexOf ( id ) ; if ( index === - 1 ) { return ; } group . nodes . splice ( index , 1 ) ; if ( group . nodes . length === 0 ) { this . removeGroup ( group . name ) ; } } ) ; this . setNodeMetadata ( id , { } ) ; this . nodes = this . nodes . filter ( ( n ) => n !== node ) ; this . emit ( 'removeNode' , node ) ; this . checkTransactionEnd ( ) ; return this ; } getNode ( id ) { const node = this . nodes . find ( ( node ) => node && node . id === id ) ; if ( ! node ) { return null ; } return node ; } renameNode ( oldId , newId ) { this . checkTransactionStart ( ) ; const node = this . getNode ( oldId ) ; if ( ! node ) { return this ; } node . id = newId ; this . edges . forEach ( ( e ) => { const edge = e ; if ( ! edge ) { return ; } if ( edge . from . node === oldId ) { edge . from . node = newId ; } if ( edge . to . node === oldId ) { edge . to . node = newId ; } } ) ; this . initializers . forEach ( ( i ) => { const iip = i ; if ( ! iip ) { return ; } if ( iip . to . node === oldId ) { iip . to . node = newId ; } } ) ; Object . keys ( this . inports ) . forEach ( ( pub ) => { const priv = this . inports [ pub ] ; if ( priv . process === oldId ) { priv . process = newId ; } } ) ; Object . keys ( this . outports ) . forEach ( ( pub ) => { const priv = this . outports [ pub ] ; if ( priv . process === oldId ) { priv . process = newId ; } } ) ; this . groups . forEach ( ( group ) => { if ( ! group ) { return ; } const index = group . nodes . indexOf ( oldId ) ; if ( index === - 1 ) { return ; } const g = group ; g . nodes [ index ] = newId ; } ) ; this . emit ( 'renameNode' , oldId , newId ) ; this . checkTransactionEnd ( ) ; return this ; } setNodeMetadata ( id , metadata ) { const node = this . getNode ( id ) ; if ( ! node ) { return this ; } this . checkTransactionStart ( ) ; if ( ! node . metadata ) { node . metadata = { } ; } const before = clone ( node . metadata ) ; Object . keys ( metadata ) . forEach ( ( item ) => { if ( ! node . metadata ) { return ; } const val = metadata [ item ] ; if ( val != null ) { node . metadata [ item ] = val ; } else { delete node . metadata [ item ] ; } } ) ; this . emit ( 'changeNode' , node , before , metadata ) ; this . checkTransactionEnd ( ) ; return this ; } addEdge ( outNode , outPort , inNode , inPort , metadata = { } ) { const outPortName = this . getPortName ( outPort ) ; const inPortName = this . getPortName ( inPort ) ; if ( this . edges . some ( ( edge ) => { if ( ( edge . from . node === outNode ) && ( edge . from . port === outPortName ) && ( edge . to . node === inNode ) && ( edge . to . port === inPortName ) ) { return true ; } return false ; } ) ) { return this ; } if ( ! this . getNode ( outNode ) ) { return this ; } if ( ! this . getNode ( inNode ) ) { return this ; } this . checkTransactionStart ( ) ; const edge = { from : { node : outNode , port : outPortName , } , to : { node : inNode , port : inPortName , } , metadata , } ; this . edges . push ( edge ) ; this . emit ( 'addEdge' , edge ) ; this . checkTransactionEnd ( ) ; return this ; } addEdgeIndex ( outNode , outPort , outIndex , inNode , inPort , inIndex , metadata = { } ) { const outPortName = this . getPortName ( outPort ) ; const inPortName = this . getPortName ( inPort ) ; const inIndexVal = ( inIndex === null ) ? undefined : inIndex ; const outIndexVal = ( outIndex === null ) ? undefined : outIndex ; if ( this . edges . some ( ( edge ) => { if ( ( edge . from . node === outNode ) && ( edge . from . port === outPortName ) && ( edge . from . index === outIndexVal ) && ( edge . to . node === inNode ) && ( edge . to . port === inPortName ) && ( edge . to . index === inIndexVal ) ) { return true ; } return false ; } ) ) { return this ; } if ( ! this . getNode ( outNode ) ) { return this ; } if ( ! this . getNode ( inNode ) ) { return this ; } this . checkTransactionStart ( ) ; const edge = { from : { node : outNode , port : outPortName , index : outIndexVal , } , to : { node : inNode , port : inPortName , index : inIndexVal , } , metadata , } ; this . edges . push ( edge ) ; this . emit ( 'addEdge' , edge ) ; this . checkTransactionEnd ( ) ; return this ; } removeEdge ( node , port , node2 , port2 ) { if ( ! this . getEdge ( node , port , node2 , port2 ) ) { return this ; } this . checkTransactionStart ( ) ; const outPort = this . getPortName ( port ) ; const inPort = this . getPortName ( port2 ) ; this . edges = this . edges . filter ( ( edge ) => { if ( node2 && inPort ) { if ( ( edge . from . node === node ) && ( edge . from . port === outPort ) && ( edge . to . node === node2 ) && ( edge . to . port === inPort ) ) { this . setEdgeMetadata ( edge . from . node , edge . from . port , edge . to . node , edge . to . port , { } ) ; this . emit ( 'removeEdge' , edge ) ; return false ; } } else if ( ( ( edge . from . node === node ) && ( edge . from . port === outPort ) ) || ( ( edge . to . node === node ) && ( edge . to . port === outPort ) ) ) { this . setEdgeMetadata ( edge . from . node , edge . from . port , edge . to . node , edge . to . port , { } ) ; this . emit ( 'removeEdge' , edge ) ; return false ; } return true ; } ) ; this . checkTransactionEnd ( ) ; return this ; } getEdge ( node , port , node2 , port2 ) { const outPort = this . getPortName ( port ) ; const inPort = this . getPortName ( port2 ) ; const edge = this . edges . find ( ( edge ) => { if ( ! edge ) { return false ; } if ( edge . from . node === node && edge . from . port === outPort && edge . to . node === node2 && edge . to . port === inPort ) { return true ; } return false ; } ) ; if ( ! edge ) { return null ; } return edge ; } setEdgeMetadata ( node , port , node2 , port2 , metadata ) { const edge = this . getEdge ( node , port , node2 , port2 ) ; if ( ! edge ) { return this ; } this . checkTransactionStart ( ) ; if ( ! edge . metadata ) { edge . metadata = { } ; } const before = clone ( edge . metadata ) ; Object . keys ( metadata ) . forEach ( ( item ) => { const val = metadata [ item ] ; if ( ! edge . metadata ) { edge . metadata = { } ; } if ( val !== null ) { edge . metadata [ item ] = val ; } else { delete edge . metadata [ item ] ; } } ) ; this . emit ( 'changeEdge' , edge , before , metadata ) ; this . checkTransactionEnd ( ) ; return this ; } addInitial ( data , node , port , metadata = { } ) { if ( ! this . getNode ( node ) ) { return this ; } const portName = this . getPortName ( port ) ; this . checkTransactionStart ( ) ; const initializer = { from : { data , } , to : { node , port : portName , } , metadata , } ; this . initializers . push ( initializer ) ; this . emit ( 'addInitial' , initializer ) ; this . checkTransactionEnd ( ) ; return this ; } addInitialIndex ( data , node , port , index , metadata = { } ) { if ( ! this . getNode ( node ) ) { return this ; } const indexVal = ( index === null ) ? undefined : index ; const portName = this . getPortName ( port ) ; this . checkTransactionStart ( ) ; const initializer = { from : { data , } , to : { node , port : portName , index : indexVal , } , metadata , } ; this . initializers . push ( initializer ) ; this . emit ( 'addInitial' , initializer ) ; this . checkTransactionEnd ( ) ; return this ; } addGraphInitial ( data , node , metadata = { } ) { const inport = this . inports [ node ] ; if ( ! inport ) { return this ; } return this . addInitial ( data , inport . process , inport . port , metadata ) ; } addGraphInitialIndex ( data , node , index , metadata = { } ) { const inport = this . inports [ node ] ; if ( ! inport ) { return this ; } return this . addInitialIndex ( data , inport . process , inport . port , index , metadata ) ; } removeInitial ( node , port ) { const portName = this . getPortName ( port ) ; this . checkTransactionStart ( ) ; this . initializers = this . initializers . filter ( ( iip ) => { if ( iip . to . node === node && iip . to . port === portName ) { this . emit ( 'removeInitial' , iip ) ; return false ; } return true ; } ) ; this . checkTransactionEnd ( ) ; return this ; } removeGraphInitial ( node ) { const inport = this . inports [ node ] ; if ( ! inport ) { return this ; } this . removeInitial ( inport . process , inport . port ) ; return this ; } toDOT ( ) { const cleanId = ( id ) : string => id . replace ( /"/g , '\\"' ) ; const cleanPort = ( port ) : string => port . replace ( /\./g , '' ) ; const wrapQuotes = ( id ) : string => `"${ cleanId ( id ) }"` ; let dot = 'digraph {\n' ; this . nodes . forEach ( ( node ) => { dot += `    ${ wrapQuotes ( node . id ) } [label=${ wrapQuotes ( node . id ) } shape=box]\n` ; } ) ; this . initializers . forEach ( ( initializer , id ) => { let data ; if ( typeof initializer . from . data === 'function' ) { data = 'Function' ; } else { data = JSON . stringify ( initializer . from . data ) ; } dot += `    data${ id } [label=${ wrapQuotes ( data ) } shape=plaintext]\n` ; dot += `    data${ id } -> ${ wrapQuotes ( initializer . to . node ) }[headlabel=${ cleanPort ( initializer . to . port ) } labelfontcolor=blue labelfontsize=8.0]\n` ; } ) ; this . edges . forEach ( ( edge ) => { dot += `    ${ wrapQuotes ( edge . from . node ) } -> ${ wrapQuotes ( edge . to . node ) }[taillabel=${ cleanPort ( edge . from . port ) } headlabel=${ cleanPort ( edge . to . port ) } labelfontcolor=blue labelfontsize=8.0]\n` ; } ) ; dot += '}' ; return dot ; } toYUML ( ) { const yuml = [ ] ; this . initializers . forEach ( ( initializer ) => { yuml . push ( `(start)[${ initializer . to . port }]->(${ initializer . to . node })` ) ; } ) ; this . edges . forEach ( ( edge ) => { yuml . push ( `(${ edge . from . node })[${ edge . from . port }]->(${ edge . to . node })` ) ; } ) ; return yuml . join ( ',' ) ; } toJSON ( ) { const json = { caseSensitive : this . caseSensitive , properties : { name : this . name , ... this . properties , } , inports : { ... this . inports , } , outports : { ... this . outports , } , groups : this . groups . map ( ( group ) => { const groupData = { name : group . name , nodes : group . nodes , } ; if ( group . metadata && Object . keys ( group . metadata ) . length ) { groupData . metadata = { ... group . metadata , } ; } return groupData ; } ) , processes : { } , connections : [ ] , } ; if ( json && json . properties ) { delete json . properties . baseDir ; delete json . properties . componentLoader ; } this . nodes . forEach ( ( node ) => { if ( ! json . processes ) { json . processes = { } ; } json . processes [ node . id ] = { component : node . component , } ; if ( node . metadata ) { json . processes [ node . id ] . metadata = { ... node . metadata , } ; } } ) ; this . edges . forEach ( ( edge ) => { const connection = { src : { process : edge . from . node , port : edge . from . port , index : edge . from . index , } , tgt : { process : edge . to . node , port : edge . to . port , index : edge . to . index , } , } ; if ( edge . metadata && Object . keys ( edge . metadata ) . length ) { connection . metadata = { ... edge . metadata , } ; } if ( ! json . connections ) { json . connections = [ ] ; } json . connections . push ( connection ) ; } ) ; this . initializers . forEach ( ( initializer ) => { const iip = { data : initializer . from . data , tgt : { process : initializer . to . node , port : initializer . to . port , index : initializer . to . index , } , } ; if ( initializer . metadata && Object . keys ( initializer . metadata ) . length ) { iip . metadata = { ... initializer . metadata , } ; } if ( ! json . connections ) { json . connections = [ ] ; } json . connections . push ( iip ) ; } ) ; return json ; } save ( file ) ; save ( file , callback ) ; save ( file , callback ? ) { let promise ; if ( isBrowser ( ) ) { promise = Promise . reject ( new Error ( 'Saving graphs not supported on browser' ) ) ; } else { promise = new Promise < string > ( ( resolve , reject ) => { const json = JSON . stringify ( this . toJSON ( ) , null , 4 ) ; let filename = file ; if ( ! filename . match ( /\.json$/ ) ) { filename = `${ file }.json` ; } writeFile ( filename , json , 'utf-8' , ( err ) => { if ( err ) { reject ( err ) ; return ; } resolve ( filename ) ; } ) ; } ) ; } if ( callback ) { promise . then ( ( filename ) => { callback ( null , filename ) ; } , callback ) ; return ; } return promise ; } } function createGraph ( name , options ) { return new Graph ( name , options ) ; } ; export interface GraphLoadingCallback { ( err , graph ? ) : void ; } interface StringLoadingCallback { ( err , result ? ) : void ; } function loadJSON ( passedDefinition ) ; function loadJSON ( passedDefinition , callback , metadata ? ) ; function loadJSON ( passedDefinition , callback ? , metadata = { } ) { const promise = new Promise < Graph > ( ( resolve ) => { let definition ; if ( typeof passedDefinition === 'string' ) { definition = JSON . parse ( passedDefinition ) ; } else { definition = clone ( passedDefinition ) ; } if ( ! definition . properties ) { definition . properties = { } ; } if ( ! definition . processes ) { definition . processes = { } ; } if ( ! definition . connections ) { definition . connections = [ ] ; } const graph = new Graph ( definition . properties . name , { caseSensitive : definition . caseSensitive || false , } ) ; graph . startTransaction ( 'loadJSON' , metadata ) ; const properties = { } ; Object . keys ( definition . properties ) . forEach ( ( property ) => { if ( property === 'name' ) { return ; } if ( ! definition . properties ) { return ; } const value = definition . properties [ property ] ; properties [ property ] = value ; } ) ; graph . setProperties ( properties ) ; Object . keys ( definition . processes ) . forEach ( ( id ) => { if ( ! definition . processes ) { return ; } const def = definition . processes [ id ] ; if ( ! def . metadata ) { def . metadata = { } ; } graph . addNode ( id , def . component , def . metadata ) ; } ) ; definition . connections . forEach ( ( conn ) => { const meta = conn . metadata ? conn . metadata : { } ; if ( typeof conn . data !== 'undefined' ) { if ( typeof conn . tgt . index === 'number' ) { graph . addInitialIndex ( conn . data , conn . tgt . process , graph . getPortName ( conn . tgt . port ) , conn . tgt . index , meta ) ; } else { graph . addInitial ( conn . data , conn . tgt . process , graph . getPortName ( conn . tgt . port ) , meta ) ; } return ; } if ( typeof conn . src === 'undefined' ) { return ; } if ( ( typeof conn . src . index === 'number' ) || ( typeof conn . tgt . index === 'number' ) ) { graph . addEdgeIndex ( conn . src . process , graph . getPortName ( conn . src . port ) , conn . src . index , conn . tgt . process , graph . getPortName ( conn . tgt . port ) , conn . tgt . index , meta ) ; return ; } graph . addEdge ( conn . src . process , graph . getPortName ( conn . src . port ) , conn . tgt . process , graph . getPortName ( conn . tgt . port ) , meta ) ; } ) ; if ( definition . inports ) { Object . keys ( definition . inports ) . forEach ( ( pub ) => { if ( ! definition . inports || ! definition . inports [ pub ] ) { return ; } const priv = definition . inports [ pub ] ; graph . addInport ( pub , priv . process , graph . getPortName ( priv . port ) , priv . metadata || { } ) ; } ) ; } if ( definition . outports ) { Object . keys ( definition . outports ) . forEach ( ( pub ) => { if ( ! definition . outports || ! definition . outports [ pub ] ) { return ; } const priv = definition . outports [ pub ] ; graph . addOutport ( pub , priv . process , graph . getPortName ( priv . port ) , priv . metadata || { } ) ; } ) ; } if ( definition . groups ) { definition . groups . forEach ( ( group ) => { graph . addGroup ( group . name , group . nodes , group . metadata || { } ) ; } ) ; } graph . endTransaction ( 'loadJSON' ) ; resolve ( graph ) ; } ) ; if ( callback ) { promise . then ( ( graph ) => { callback ( null , graph ) ; } , callback ) ; } return promise ; } function loadFBP ( fbpData ) ; function loadFBP ( fbpData , callback , metadata ? ) ; function loadFBP ( fbpData , callback ? , metadata = { } , caseSensitive = false ) { const promise = new Promise < GraphJson > ( ( resolve ) => { resolve ( require ( 'fbp' ) . parse ( fbpData , { caseSensitive } ) ) ; } ) . then ( ( def ) => loadJSON ( def ) ) ; if ( callback ) { promise . then ( ( graph ) => { callback ( null , graph ) ; } , callback ) ; } return promise ; } function loadHTTP ( url ) ; function loadHTTP ( url , callback ) ; function loadHTTP ( url , callback ? ) { const promise = new Promise < string > ( ( resolve , reject ) => { const req = new XMLHttpRequest ( ) ; req . onreadystatechange = ( ) => { if ( req . readyState !== 4 ) { return ; } if ( req . status !== 200 ) { reject ( new Error ( `Failed to load ${ url }: HTTP ${ req . status }` ) ) ; return ; } resolve ( req . responseText ) ; } ; req . open ( 'GET' , url , true ) ; req . send ( ) ; } ) ; if ( callback ) { promise . then ( ( content ) => { callback ( null , content ) ; } , callback ) ; } return promise ; } function loadFile ( file ) ; function loadFile ( file , callback , metadata ? , caseSensitive ? ) ; function loadFile ( file , callback ? , metadata = { } , caseSensitive = false ) { let ioPromise ; if ( isBrowser ( ) ) { ioPromise = loadHTTP ( file ) ; } else { ioPromise = new Promise ( ( resolve , reject ) => { readFile ( file , 'utf-8' , ( err , data ) => { if ( err ) { reject ( err ) ; return ; } resolve ( data ) ; } ) ; } ) ; } const promise = ioPromise . then ( ( content ) => { if ( file . split ( '.' ) . pop ( ) === 'fbp' ) { return loadFBP ( content ) ; } return loadJSON ( content ) ; } ) ; if ( callback ) { promise . then ( ( content ) => { callback ( null , content ) ; } , callback ) ; } return promise ; } function resetGraph ( graph ) { graph . groups . reverse ( ) ; graph . groups . forEach ( ( group ) => { if ( group != null ) { graph . removeGroup ( group . name ) ; } } ) ; Object . keys ( graph . outports ) . forEach ( ( port ) => { graph . removeOutport ( port ) ; } ) ; Object . keys ( graph . inports ) . forEach ( ( port ) => { graph . removeInport ( port ) ; } ) ; graph . setProperties ( { } ) ; graph . initializers . reverse ( ) ; graph . initializers . forEach ( ( iip ) => { graph . removeInitial ( iip . to . node , iip . to . port ) ; } ) ; graph . edges . reverse ( ) ; graph . edges . forEach ( ( edge ) => { graph . removeEdge ( edge . from . node , edge . from . port , edge . to . node , edge . to . port ) ; } ) ; graph . nodes . reverse ( ) ; graph . nodes . forEach ( ( node ) => { graph . removeNode ( node . id ) ; } ) ; } function mergeResolveTheirsNaive ( base , to ) { resetGraph ( base ) ; to . nodes . forEach ( ( node ) => { base . addNode ( node . id , node . component , node . metadata ) ; } ) ; to . edges . forEach ( ( edge ) => { base . addEdge ( edge . from . node , edge . from . port , edge . to . node , edge . to . port , edge . metadata ) ; } ) ; to . initializers . forEach ( ( iip ) => { if ( typeof iip . to . index === 'number' ) { base . addInitialIndex ( iip . from . data , iip . to . node , iip . to . port , iip . to . index , iip . metadata || { } ) ; return ; } base . addInitial ( iip . from . data , iip . to . node , iip . to . port , iip . metadata || { } ) ; } ) ; base . setProperties ( to . properties ) ; Object . keys ( to . inports ) . forEach ( ( pub ) => { const priv = to . inports [ pub ] ; base . addInport ( pub , priv . process , priv . port , priv . metadata || { } ) ; } ) ; Object . keys ( to . outports ) . forEach ( ( pub ) => { const priv = to . outports [ pub ] ; base . addOutport ( pub , priv . process , priv . port , priv . metadata || { } ) ; } ) ; to . groups . forEach ( ( group ) => { base . addGroup ( group . name , group . nodes , group . metadata || { } ) ; } ) ; } function equivalent ( a , b ) { return ( JSON . stringify ( a ) === JSON . stringify ( b ) ) ; } export { Graph , createGraph , loadJSON , loadFBP , loadFile , equivalent , mergeResolveTheirsNaive as mergeResolveTheirs , } ;