UNPKG

msgflo

Version:

Polyglot FBP runtime based on message queues

183 lines (149 loc) 5.79 kB
common = require './common' transport = require './transport' path = require 'path' fs = require 'fs' debug = require('debug')('msgflo:participant') chance = require 'chance' async = require 'async' EventEmitter = require('events').EventEmitter uuid = require 'uuid' fbp = require 'fbp' random = new chance.Chance 10202 findPort = (def, type, portName) -> ports = if type == 'inport' then def.inports else def.outports for port in ports return port if port.id == portName return null definitionToFbp = (d) -> def = common.clone d portsWithQueue = (ports) -> # these cannot be wired, so should not show. For Sources/Sinks return ports.filter (p) -> return p.queue? def.inports = portsWithQueue def.inports def.outports = portsWithQueue def.outports return def addQueues = (ports, role) -> for p in ports p.hidden = false if not p.hidden? name = role+'.'+p.id.toUpperCase() p.queue = name if not p.queue and not p.hidden return ports instantiateDefinition = (d, role) -> def = common.clone d id = uuid.v4() def.role = role def.id = "#{def.role}-#{id}" def.inports = addQueues def.inports, def.role def.outports = addQueues def.outports, def.role return def class Participant extends EventEmitter # @func gets called with inport, , and should return outport, outdata constructor: (@messaging, def, @func, role) -> @messaging = transport.getClient @messaging if typeof messaging == 'string' role = 'unknown' if not role @definition = instantiateDefinition def, role @running = false start: (callback) -> @messaging.connect (err) => debug 'connected', err return callback err if err @setupPorts (err) => @running = true return callback err if err @register (err) -> debug 'start completed' return callback err stop: (callback) -> @running = false @messaging.disconnect callback # Send data on inport # Normally only used directly for Source type participants # For Transform or Sink type, is called on data from input queue send: (inport, data) -> debug 'got msg from send()', inport @func inport, data, (outport, err, data) => if not err @onResult outport, data, () -> # Emit data on outport emitData: (outport, data) -> @emit 'data', outport, data onResult: (outport, data, callback) => port = findPort @definition, 'outport', outport @emitData port.id, data if port.queue @messaging.sendToQueue port.queue, data, callback else return callback null setupPorts: (callback) -> setupPort = (def, callback) => return callback null if not def.queue @messaging.createQueue 'TODO:distinquish', def.queue, callback subscribePort = (def, callback) => return callback null if not def.queue callFunc = (msg) => debug 'got msg from queue', def.queue @func def.id, msg.data, (outport, err, data) => return @messaging.nackMessage msg if err @onResult outport, data, (err) => return @messaging.nackMessage msg if err @messaging.ackMessage msg if msg debug 'subscribed to', def.queue @messaging.subscribeToQueue def.queue, callFunc, callback allports = @definition.outports.concat @definition.inports async.map allports, setupPort, (err) => return callback err if err async.map @definition.inports, subscribePort, (err) => return callback err if err return callback null register: (callback) -> # Send discovery package to broker on 'fbp' queue debug 'register' definition = definitionToFbp @definition @messaging.registerParticipant definition, (err) => debug 'registered', err return callback err # Sets up queues to match those defined in graph connectGraphEdges: (graph) -> processName = @definition.role # TODO: support also 'role' # If there are outbound connections # Set the output queue to equal to the input queue of the target for conn in graph.connections if conn.src?.process == processName # WARN: assumes the queue naming convention, # as we don't have access to the definition of target participant # altenative would be to instantiate whole network, # and in worker case only run one participant? # using exchanges for the outports and binding to inports might also solve it tgtQueue = "#{conn.tgt.process}.#{conn.tgt.port.toUpperCase()}" ports = @definition.outports.filter (p) -> return p.id == conn.src.port ports[0].queue = tgtQueue connectGraphEdgesFile: (filepath, callback) -> ext = path.extname filepath fs.readFile filepath, { encoding: 'utf-8' }, (err, contents) => return callback err if err try if ext == '.fbp' graph = fbp.parse contents else graph = JSON.parse contents @connectGraphEdges graph catch e return callback e return callback null # TODO: consider making component api a bit more like NoFlo.WirePattern # # inputs = { portA: { data: dataA1, groups: ['A', '1'] }, portB: { data: B1 } } # outfunc = (type, outputs) -> # type can be 'data', 'end' # process(inputs, outfunc) # # Core ideas: # groups attached to the packet, avoids separate lifetime handling, but still allows modification # should one enforce use of promises? calling process returns a promise? startParticipant = (library, client, componentName, id, callback) -> debug 'starting', componentName, id component = library[componentName] part = component client, id part.start (err) -> return callback err, part exports.Participant = Participant exports.startParticipant = startParticipant