UNPKG

noflo

Version:

Flow-Based Programming environment for JavaScript

595 lines (549 loc) 24.7 kB
# NoFlo - Flow-Based Programming for JavaScript # (c) 2014-2015 TheGrid (Rituwall Inc.) # NoFlo may be freely distributed under the MIT license _ = require 'underscore' StreamSender = require('./Streams').StreamSender StreamReceiver = require('./Streams').StreamReceiver InternalSocket = require './InternalSocket' isArray = (obj) -> return Array.isArray(obj) if Array.isArray return Object.prototype.toString.call(arg) == '[object Array]' # MapComponent maps a single inport to a single outport, forwarding all # groups from in to out and calling `func` on each incoming packet exports.MapComponent = (component, func, config) -> config = {} unless config config.inPort = 'in' unless config.inPort config.outPort = 'out' unless config.outPort inPort = component.inPorts[config.inPort] outPort = component.outPorts[config.outPort] groups = [] inPort.process = (event, payload) -> switch event when 'connect' then outPort.connect() when 'begingroup' groups.push payload outPort.beginGroup payload when 'data' func payload, groups, outPort when 'endgroup' groups.pop() outPort.endGroup() when 'disconnect' groups = [] outPort.disconnect() # WirePattern makes your component collect data from several inports # and activates a handler `proc` only when a tuple from all of these # ports is complete. The signature of handler function is: # ``` # proc = (combinedInputData, inputGroups, outputPorts, asyncCallback) -> # ``` # # With `config.group = true` it checks incoming group IPs and collates # data with matching group IPs. By default this kind of grouping is `false`. # Set `config.group` to a RegExp object to correlate inputs only if the # group matches the expression (e.g. `^req_`). For non-matching groups # the component will act normally. # # With `config.field = 'fieldName' it collates incoming data by specified # field. The component's proc function is passed a combined object with # port names used as keys. This kind of grouping is disabled by default. # # With `config.forwardGroups = true` it would forward group IPs from # inputs to the output sending them along with the data. This option also # accepts string or array values, if you want to forward groups from specific # port(s) only. By default group forwarding is `false`. # # `config.receiveStreams = [portNames]` feature makes the component expect # substreams on specific inports instead of separate IPs (brackets and data). # It makes select inports emit `Substream` objects on `data` event # and silences `beginGroup` and `endGroup` events. # # `config.sendStreams = [portNames]` feature makes the component emit entire # substreams of packets atomically to the outport. Atomically means that a # substream cannot be interrupted by other packets, which is important when # doing asynchronous processing. In fact, `sendStreams` is enabled by default # on all outports when `config.async` is `true`. # # WirePattern supports both sync and async `proc` handlers. In latter case # pass `config.async = true` and make sure that `proc` accepts callback as # 4th parameter and calls it when async operation completes or fails. # # WirePattern sends group packets, sends data packets emitted by `proc` # via its `outputPort` argument, then closes groups and disconnects # automatically. exports.WirePattern = (component, config, proc) -> # In ports inPorts = if 'in' of config then config.in else 'in' inPorts = [ inPorts ] unless isArray inPorts # Out ports outPorts = if 'out' of config then config.out else 'out' outPorts = [ outPorts ] unless isArray outPorts # Error port config.error = 'error' unless 'error' of config # For async process config.async = false unless 'async' of config # Keep correct output order for async mode config.ordered = true unless 'ordered' of config # Group requests by group ID config.group = false unless 'group' of config # Group requests by object field config.field = null unless 'field' of config # Forward group events from specific inputs to the output: # - false: don't forward anything # - true: forward unique groups of all inputs # - string: forward groups of a specific port only # - array: forward unique groups of inports in the list config.forwardGroups = false unless 'forwardGroups' of config # Receive streams feature config.receiveStreams = false unless 'receiveStreams' of config if typeof config.receiveStreams is 'string' config.receiveStreams = [ config.receiveStreams ] # Send streams feature config.sendStreams = false unless 'sendStreams' of config if typeof config.sendStreams is 'string' config.sendStreams = [ config.sendStreams ] config.sendStreams = outPorts if config.async # Parameter ports config.params = [] unless 'params' of config config.params = [ config.params ] if typeof config.params is 'string' # Node name config.name = '' unless 'name' of config # Drop premature input before all params are received config.dropInput = false unless 'dropInput' of config # Firing policy for addressable ports unless 'arrayPolicy' of config config.arrayPolicy = in: 'any' params: 'all' # Garbage collector frequency: execute every N packets config.gcFrequency = 100 unless 'gcFrequency' of config # Garbage collector timeout: drop packets older than N seconds config.gcTimeout = 300 unless 'gcTimeout' of config collectGroups = config.forwardGroups # Collect groups from each port? if typeof collectGroups is 'boolean' and not config.group collectGroups = inPorts # Collect groups from one and only port? if typeof collectGroups is 'string' and not config.group collectGroups = [collectGroups] # Collect groups from any port, as we group by them if collectGroups isnt false and config.group collectGroups = true for name in inPorts unless component.inPorts[name] throw new Error "no inPort named '#{name}'" for name in outPorts unless component.outPorts[name] throw new Error "no outPort named '#{name}'" component.groupedData = {} component.groupedGroups = {} component.groupedDisconnects = {} disconnectOuts = -> # Manual disconnect forwarding for p in outPorts component.outPorts[p].disconnect() if component.outPorts[p].isConnected() sendGroupToOuts = (grp) -> for p in outPorts component.outPorts[p].beginGroup grp closeGroupOnOuts = (grp) -> for p in outPorts component.outPorts[p].endGroup grp # For ordered output component.outputQ = [] processQueue = -> while component.outputQ.length > 0 streams = component.outputQ[0] flushed = false # Null in the queue means "disconnect all" if streams is null disconnectOuts() flushed = true else # At least one of the outputs has to be resolved # for output streams to be flushed. if outPorts.length is 1 tmp = {} tmp[outPorts[0]] = streams streams = tmp for key, stream of streams if stream.resolved stream.flush() flushed = true component.outputQ.shift() if flushed return unless flushed if config.async component.load = 0 if 'load' of component.outPorts # Create before and after hooks component.beforeProcess = (outs) -> component.outputQ.push outs if config.ordered component.load++ if 'load' of component.outPorts and component.outPorts.load.isAttached() component.outPorts.load.send component.load component.outPorts.load.disconnect() component.afterProcess = (err, outs) -> processQueue() component.load-- if 'load' of component.outPorts and component.outPorts.load.isAttached() component.outPorts.load.send component.load component.outPorts.load.disconnect() # Parameter ports component.taskQ = [] component.params = {} component.requiredParams = [] component.completeParams = [] component.receivedParams = [] component.defaultedParams = [] component.defaultsSent = false component.sendDefaults = -> if component.defaultedParams.length > 0 for param in component.defaultedParams if component.receivedParams.indexOf(param) is -1 tempSocket = InternalSocket.createSocket() component.inPorts[param].attach tempSocket tempSocket.send() tempSocket.disconnect() component.inPorts[param].detach tempSocket component.defaultsSent = true resumeTaskQ = -> if component.completeParams.length is component.requiredParams.length and component.taskQ.length > 0 # Avoid looping when feeding the queue inside the queue itself temp = component.taskQ.slice 0 component.taskQ = [] while temp.length > 0 task = temp.shift() task() for port in config.params unless component.inPorts[port] throw new Error "no inPort named '#{port}'" component.requiredParams.push port if component.inPorts[port].isRequired() component.defaultedParams.push port if component.inPorts[port].hasDefault() for port in config.params do (port) -> inPort = component.inPorts[port] inPort.process = (event, payload, index) -> # Param ports only react on data return unless event is 'data' if inPort.isAddressable() component.params[port] = {} unless port of component.params component.params[port][index] = payload if config.arrayPolicy.params is 'all' and Object.keys(component.params[port]).length < inPort.listAttached().length return # Need data on all array indexes to proceed else component.params[port] = payload if component.completeParams.indexOf(port) is -1 and component.requiredParams.indexOf(port) > -1 component.completeParams.push port component.receivedParams.push port # Trigger pending procs if all params are complete resumeTaskQ() # Disconnect event forwarding component.disconnectData = {} component.disconnectQ = [] component.groupBuffers = {} component.keyBuffers = {} component.gcTimestamps = {} # Garbage collector component.dropRequest = (key) -> # Discard pending disconnect keys delete component.disconnectData[key] if key of component.disconnectData # Clean grouped data delete component.groupedData[key] if key of component.groupedData delete component.groupedGroups[key] if key of component.groupedGroups component.gcCounter = 0 gc = -> component.gcCounter++ if component.gcCounter % config.gcFrequency is 0 current = new Date().getTime() for key, val of component.gcTimestamps if (current - val) > (config.gcTimeout * 1000) component.dropRequest key delete component.gcTimestamps[key] # Grouped ports for port in inPorts do (port) -> component.groupBuffers[port] = [] component.keyBuffers[port] = null # Support for StreamReceiver ports if config.receiveStreams and config.receiveStreams.indexOf(port) isnt -1 inPort = new StreamReceiver component.inPorts[port] else inPort = component.inPorts[port] needPortGroups = collectGroups instanceof Array and collectGroups.indexOf(port) isnt -1 # Set processing callback inPort.process = (event, payload, index) -> component.groupBuffers[port] = [] unless component.groupBuffers[port] switch event when 'begingroup' component.groupBuffers[port].push payload if config.forwardGroups and (collectGroups is true or needPortGroups) and not config.async sendGroupToOuts payload when 'endgroup' component.groupBuffers[port] = component.groupBuffers[port].slice 0, component.groupBuffers[port].length - 1 if config.forwardGroups and (collectGroups is true or needPortGroups) and not config.async closeGroupOnOuts payload when 'disconnect' if inPorts.length is 1 if config.async or config.StreamSender if config.ordered component.outputQ.push null processQueue() else component.disconnectQ.push true else disconnectOuts() else foundGroup = false key = component.keyBuffers[port] component.disconnectData[key] = [] unless key of component.disconnectData for i in [0...component.disconnectData[key].length] unless port of component.disconnectData[key][i] foundGroup = true component.disconnectData[key][i][port] = true if Object.keys(component.disconnectData[key][i]).length is inPorts.length component.disconnectData[key].shift() if config.async or config.StreamSender if config.ordered component.outputQ.push null processQueue() else component.disconnectQ.push true else disconnectOuts() delete component.disconnectData[key] if component.disconnectData[key].length is 0 break unless foundGroup obj = {} obj[port] = true component.disconnectData[key].push obj when 'data' if inPorts.length is 1 and not inPort.isAddressable() data = payload groups = component.groupBuffers[port] else key = '' if config.group and component.groupBuffers[port].length > 0 key = component.groupBuffers[port].toString() if config.group instanceof RegExp reqId = null for grp in component.groupBuffers[port] if config.group.test grp reqId = grp break key = if reqId then reqId else '' else if config.field and typeof(payload) is 'object' and config.field of payload key = payload[config.field] component.keyBuffers[port] = key component.groupedData[key] = [] unless key of component.groupedData component.groupedGroups[key] = [] unless key of component.groupedGroups foundGroup = false requiredLength = inPorts.length ++requiredLength if config.field # Check buffered tuples awaiting completion for i in [0...component.groupedData[key].length] # Check this buffered tuple if it's missing value for this port if not (port of component.groupedData[key][i]) or (component.inPorts[port].isAddressable() and config.arrayPolicy.in is 'all' and not (index of component.groupedData[key][i][port])) foundGroup = true if component.inPorts[port].isAddressable() # Maintain indexes for addressable ports unless port of component.groupedData[key][i] component.groupedData[key][i][port] = {} component.groupedData[key][i][port][index] = payload else component.groupedData[key][i][port] = payload if needPortGroups # Include port groups into the set of the unique ones component.groupedGroups[key][i] = _.union component.groupedGroups[key][i], component.groupBuffers[port] else if collectGroups is true # All the groups we need are here in this port component.groupedGroups[key][i][port] = component.groupBuffers[port] # Addressable ports may require other indexes if component.inPorts[port].isAddressable() and config.arrayPolicy.in is 'all' and Object.keys(component.groupedData[key][i][port]).length < component.inPorts[port].listAttached().length return # Need data on other array port indexes to arrive groupLength = Object.keys(component.groupedData[key][i]).length # Check if the tuple is complete if groupLength is requiredLength data = (component.groupedData[key].splice i, 1)[0] # Strip port name if there's only one inport if inPorts.length is 1 and inPort.isAddressable() data = data[port] groups = (component.groupedGroups[key].splice i, 1)[0] if collectGroups is true groups = _.intersection.apply null, _.values groups delete component.groupedData[key] if component.groupedData[key].length is 0 delete component.groupedGroups[key] if component.groupedGroups[key].length is 0 if config.group and key delete component.gcTimestamps[key] break else return # need more data to continue unless foundGroup # Create a new tuple obj = {} obj[config.field] = key if config.field if component.inPorts[port].isAddressable() obj[port] = {} ; obj[port][index] = payload else obj[port] = payload if inPorts.length is 1 and component.inPorts[port].isAddressable() and (config.arrayPolicy.in is 'any' or component.inPorts[port].listAttached().length is 1) # This packet is all we need data = obj[port] groups = component.groupBuffers[port] else component.groupedData[key].push obj if needPortGroups component.groupedGroups[key].push component.groupBuffers[port] else if collectGroups is true tmp = {} ; tmp[port] = component.groupBuffers[port] component.groupedGroups[key].push tmp else component.groupedGroups[key].push [] if config.group and key # Timestamp to garbage collect this request component.gcTimestamps[key] = new Date().getTime() return # need more data to continue # Drop premature data if configured to do so return if config.dropInput and component.completeParams.length isnt component.requiredParams.length # Prepare outputs outs = {} for name in outPorts if config.async or config.sendStreams and config.sendStreams.indexOf(name) isnt -1 outs[name] = new StreamSender component.outPorts[name], config.ordered else outs[name] = component.outPorts[name] outs = outs[outPorts[0]] if outPorts.length is 1 # for simplicity groups = [] unless groups whenDoneGroups = groups.slice 0 whenDone = (err) -> if err component.error err, whenDoneGroups # For use with MultiError trait if typeof component.fail is 'function' and component.hasErrors component.fail() # Disconnect outputs if still connected, # this also indicates them as resolved if pending outputs = if outPorts.length is 1 then port: outs else outs disconnect = false if component.disconnectQ.length > 0 component.disconnectQ.shift() disconnect = true for name, out of outputs out.endGroup() for i in whenDoneGroups if config.forwardGroups and config.async out.disconnect() if disconnect out.done() if config.async or config.StreamSender if typeof component.afterProcess is 'function' component.afterProcess err or component.hasErrors, outs # Before hook if typeof component.beforeProcess is 'function' component.beforeProcess outs # Group forwarding if config.forwardGroups and config.async if outPorts.length is 1 outs.beginGroup g for g in groups else for name, out of outs out.beginGroup g for g in groups # Enforce MultiError with WirePattern (for group forwarding) exports.MultiError component, config.name, config.error, groups # Call the proc function if config.async postpone = -> resume = -> postponedToQ = false task = -> proc.call component, data, groups, outs, whenDone, postpone, resume postpone = (backToQueue = true) -> postponedToQ = backToQueue if backToQueue component.taskQ.push task resume = -> if postponedToQ then resumeTaskQ() else task() else task = -> proc.call component, data, groups, outs whenDone() component.taskQ.push task resumeTaskQ() # Call the garbage collector gc() # Overload shutdown method to clean WirePattern state baseShutdown = component.shutdown component.shutdown = -> baseShutdown.call component component.groupedData = {} component.groupedGroups = {} component.outputQ = [] component.disconnectData = {} component.disconnectQ = [] component.taskQ = [] component.params = {} component.completeParams = [] component.receivedParams = [] component.defaultsSent = false component.groupBuffers = {} component.keyBuffers = {} component.gcTimestamps = {} component.gcCounter = 0 # Make it chainable or usable at the end of getComponent() return component # Alias for compatibility with 0.5.3 exports.GroupedInput = exports.WirePattern # `CustomError` returns an `Error` object carrying additional properties. exports.CustomError = (message, options) -> err = new Error message return exports.CustomizeError err, options # `CustomizeError` sets additional options for an `Error` object. exports.CustomizeError = (err, options) -> for own key, val of options err[key] = val return err # `MultiError` simplifies throwing and handling multiple error objects # during a single component activation. # # `group` is an optional group ID which will be used to wrap all error # packets emitted by the component. exports.MultiError = (component, group = '', errorPort = 'error', forwardedGroups = []) -> component.hasErrors = false component.errors = [] # Override component.error to support group information component.error = (e, groups = []) -> component.errors.push err: e groups: forwardedGroups.concat groups component.hasErrors = true # Fail method should be called to terminate process immediately # or to flush error packets. component.fail = (e = null, groups = []) -> component.error e, groups if e return unless component.hasErrors return unless errorPort of component.outPorts return unless component.outPorts[errorPort].isAttached() component.outPorts[errorPort].beginGroup group if group for error in component.errors component.outPorts[errorPort].beginGroup grp for grp in error.groups component.outPorts[errorPort].send error.err component.outPorts[errorPort].endGroup() for grp in error.groups component.outPorts[errorPort].endGroup() if group component.outPorts[errorPort].disconnect() # Clean the status for next activation component.hasErrors = false component.errors = [] # Overload shutdown method to clear errors baseShutdown = component.shutdown component.shutdown = -> baseShutdown.call component component.hasErrors = false component.errors = [] return component