noflo
Version:
Flow-Based Programming environment for JavaScript
181 lines (167 loc) • 6.12 kB
text/coffeescript
# NoFlo - Flow-Based Programming for JavaScript
# (c) 2017 Flowhub UG
# NoFlo may be freely distributed under the MIT license
#
# asCallback helper embedding NoFlo components or graphs in other JavaScript programs.
ComponentLoader = require('./ComponentLoader').ComponentLoader
Network = require('./Network').Network
IP = require('./IP')
internalSocket = require './InternalSocket'
Graph = require('fbp-graph').Graph
normalizeOptions = (options, component) ->
options = {} unless options
options.name = component unless options.name
if options.loader
options.baseDir = options.loader.baseDir
if not options.baseDir and process and process.cwd
options.baseDir = process.cwd()
unless options.loader
options.loader = new ComponentLoader options.baseDir
options.raw = false unless options.raw
options
prepareNetwork = (component, options, callback) ->
# Start by loading the component
options.loader.load component, (err, instance) ->
return callback err if err
# Prepare a graph wrapping the component
graph = new Graph options.name
nodeName = options.name
graph.addNode nodeName, component
# Expose ports
# FIXME: direct process.component.inPorts/outPorts access is only for legacy compat
inPorts = instance.inPorts.ports or instance.inPorts
outPorts = instance.outPorts.ports or instance.outPorts
for port, def of inPorts
graph.addInport port, nodeName, port
for port, def of outPorts
graph.addOutport port, nodeName, port
# Prepare network
graph.componentLoader = options.loader
network = new Network graph, options
# Wire the network up and start execution
network.connect (err) ->
return callback err if err
callback null, network
runNetwork = (network, inputs, options, callback) ->
process = network.getNode options.name
# Prepare inports
inPorts = Object.keys network.graph.inports
inSockets = {}
inPorts.forEach (inport) ->
inSockets[inport] = internalSocket.createSocket()
process.component.inPorts[inport].attach inSockets[inport]
# Subscribe outports
received = []
outPorts = Object.keys network.graph.outports
outSockets = {}
outPorts.forEach (outport) ->
outSockets[outport] = internalSocket.createSocket()
process.component.outPorts[outport].attach outSockets[outport]
outSockets[outport].on 'ip', (ip) ->
res = {}
res[outport] = ip
received.push res
# Subscribe network finish
network.once 'end', ->
# Clear listeners
for port, socket of outSockets
process.component.outPorts[port].detach socket
outSockets = {}
inSockets = {}
callback null, received
# Start network
network.start (err) ->
return callback err if err
# Send inputs
for inputMap in inputs
for port, value of inputMap
if IP.isIP value
inSockets[port].post value
continue
inSockets[port].post new IP 'data', value
getType = (inputs, network) ->
# Scalar values are always simple inputs
return 'simple' unless typeof inputs is 'object'
if Array.isArray inputs
maps = inputs.filter (entry) ->
getType(entry, network) is 'map'
# If each member if the array is an input map, this is a sequence
return 'sequence' if maps.length is inputs.length
# Otherwise arrays must be simple inputs
return 'simple'
# Empty objects can't be maps
return 'simple' unless Object.keys(inputs).length
for key, value of inputs
return 'simple' unless network.graph.inports[key]
return 'map'
prepareInputMap = (inputs, inputType, network) ->
# Sequence we can use as-is
return inputs if inputType is 'sequence'
# We can turn a map to a sequence by wrapping it in an array
return [inputs] if inputType is 'map'
# Simple inputs need to be converted to a sequence
inPort = Object.keys(network.graph.inports)[0]
# If we have a port named "IN", send to that
inPort = 'in' if network.graph.inports.in
map = {}
map[inPort] = inputs
return [map]
normalizeOutput = (values, options) ->
return values if options.raw
result = []
previous = null
current = result
for packet in values
if packet.type is 'openBracket'
previous = current
current = []
previous.push current
if packet.type is 'data'
current.push packet.data
if packet.type is 'closeBracket'
current = previous
if result.length is 1
return result[0]
return result
sendOutputMap = (outputs, resultType, options, callback) ->
# First check if the output sequence contains errors
errors = outputs.filter((map) -> map.error?).map (map) -> map.error
return callback normalizeOutput errors, options if errors.length
if resultType is 'sequence'
return callback null, outputs.map (map) ->
res = {}
for key, val of map
if options.raw
res[key] = val
continue
res[key] = normalizeOutput [val], options
return res
# Flatten the sequence
mappedOutputs = {}
for map in outputs
for key, val of map
mappedOutputs[key] = [] unless mappedOutputs[key]
mappedOutputs[key].push val
outputKeys = Object.keys mappedOutputs
withValue = outputKeys.filter (outport) ->
mappedOutputs[outport].length > 0
if withValue.length is 0
# No output
return callback null
if withValue.length is 1 and resultType is 'simple'
# Single outport
return callback null, normalizeOutput mappedOutputs[withValue[0]], options
result = {}
for port, packets of mappedOutputs
result[port] = normalizeOutput packets, options
callback null, result
exports.asCallback = (component, options) ->
options = normalizeOptions options, component
return (inputs, callback) ->
prepareNetwork component, options, (err, network) ->
return callback err if err
resultType = getType inputs, network
inputMap = prepareInputMap inputs, resultType, network
runNetwork network, inputMap, options, (err, outputMap) ->
return callback err if err
sendOutputMap outputMap, resultType, options, callback