noflo
Version:
Flow-Based Programming environment for JavaScript
1,060 lines (978 loc) • 42.7 kB
text/coffeescript
# NoFlo - Flow-Based Programming for JavaScript
# (c) 2014-2017 Flowhub UG
# NoFlo may be freely distributed under the MIT license
StreamSender = require('./Streams').StreamSender
StreamReceiver = require('./Streams').StreamReceiver
InternalSocket = require './InternalSocket'
IP = require './IP'
platform = require './Platform'
utils = require './Utils'
debug = require('debug') 'noflo:helpers'
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) ->
platform.deprecated 'noflo.helpers.MapComponent is deprecated. Please port to Process API'
config = {} unless config
config.inPort = 'in' unless config.inPort
config.outPort = 'out' unless config.outPort
# Set up bracket forwarding
component.forwardBrackets = {} unless component.forwardBrackets
component.forwardBrackets[config.inPort] = [config.outPort]
component.process (input, output) ->
return unless input.hasData config.inPort
data = input.getData config.inPort
groups = getGroupContext component, config.inPort, input
outProxy = getOutputProxy [config.outPort], output
func data, groups, outProxy
output.done()
# 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.
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
if config.forwardGroups
if typeof config.forwardGroups is 'string'
# Collect groups from one and only port?
config.forwardGroups = [config.forwardGroups]
if typeof config.forwardGroups is 'boolean'
# Forward groups from each port?
config.forwardGroups = inPorts
# Receive streams feature
config.receiveStreams = false unless 'receiveStreams' of config
if config.receiveStreams
throw new Error 'WirePattern receiveStreams is deprecated'
# if typeof config.receiveStreams is 'string'
# config.receiveStreams = [ config.receiveStreams ]
# Send streams feature
config.sendStreams = false unless 'sendStreams' of config
if config.sendStreams
throw new Error 'WirePattern sendStreams is deprecated'
# 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'
config.inPorts = inPorts
config.outPorts = outPorts
# Warn user of deprecated features
checkDeprecation config, proc
# Allow users to selectively fall back to legacy WirePattern implementation
if config.legacy or process?.env?.NOFLO_WIREPATTERN_LEGACY
platform.deprecated 'noflo.helpers.WirePattern legacy mode is deprecated'
setup = legacyWirePattern
else
setup = processApiWirePattern
return setup component, config, proc
# Takes WirePattern configuration of a component and sets up
# Process API to handle it.
processApiWirePattern = (component, config, func) ->
# Make param ports control ports
setupControlPorts component, config
# Set up sendDefaults function
setupSendDefaults component
# Set up bracket forwarding rules
setupBracketForwarding component, config
component.ordered = config.ordered
# Create the processing function
component.process (input, output, context) ->
# Abort unless WirePattern-style preconditions don't match
return unless checkWirePatternPreconditions config, input, output
# Populate component.params from control ports
component.params = populateParams config, input
# Read input data
data = getInputData config, input
# Read bracket context of first inport
groups = getGroupContext component, config.inPorts[0], input
# Produce proxy object wrapping output in legacy-style port API
outProxy = getOutputProxy config.outPorts, output
debug "WirePattern Process API call with", data, groups, component.params, context.scope
postpone = ->
throw new Error 'noflo.helpers.WirePattern postpone is deprecated'
resume = ->
throw new Error 'noflo.helpers.WirePattern resume is deprecated'
unless config.async
# Set up custom error handlers
errorHandler = setupErrorHandler component, config, output
# Synchronous WirePattern, call done here
func.call component, data, groups, outProxy, postpone, resume, input.scope
# No need to call done if component called fail
return if output.result.__resolved
# Let error handler send any remaining errors
do errorHandler
# Call done
output.done()
return
# Async WirePattern will call the output.done callback itself
errorHandler = setupErrorHandler component, config, output
func.call component, data, groups, outProxy, (err) ->
do errorHandler
output.done err
, postpone, resume, input.scope
# Provide deprecation warnings on certain more esoteric WirePattern features
checkDeprecation = (config, func) ->
# First check the conditions that force us to fall back on legacy WirePattern
if config.group
platform.deprecated 'noflo.helpers.WirePattern group option is deprecated. Please port to Process API'
if config.field
platform.deprecated 'noflo.helpers.WirePattern field option is deprecated. Please port to Process API'
# Then add deprecation warnings for other unwanted behaviors
if func.length > 4
platform.deprecated 'noflo.helpers.WirePattern postpone and resume are deprecated. Please port to Process API'
unless config.async
platform.deprecated 'noflo.helpers.WirePattern synchronous is deprecated. Please port to Process API'
return
# Updates component port definitions to control prots for WirePattern
# -style params array
setupControlPorts = (component, config) ->
for param in config.params
component.inPorts[param].options.control = true
# Sets up Process API bracket forwarding rules for WirePattern configuration
setupBracketForwarding = (component, config) ->
# Start with empty bracket forwarding config
component.forwardBrackets = {}
return unless config.forwardGroups
# By default we forward from all inports
inPorts = config.inPorts
if isArray config.forwardGroups
# Selective forwarding enabled
inPorts = config.forwardGroups
for inPort in inPorts
component.forwardBrackets[inPort] = []
# Forward to all declared outports
for outPort in config.outPorts
component.forwardBrackets[inPort].push outPort
# If component has an error outport, forward there too
if component.outPorts.error
component.forwardBrackets[inPort].push 'error'
return
setupErrorHandler = (component, config, output) ->
errors = []
errorHandler = (e, groups = []) ->
platform.deprecated 'noflo.helpers.WirePattern error method is deprecated. Please send error to callback instead'
errors.push
err: e
groups: groups
component.hasErrors = true
failHandler = (e = null, groups = []) ->
platform.deprecated 'noflo.helpers.WirePattern fail method is deprecated. Please send error to callback instead'
errorHandler e, groups if e
sendErrors()
output.done()
sendErrors = ->
return unless errors.length
output.sendIP 'error', new IP 'openBracket', config.name if config.name
errors.forEach (e) ->
output.sendIP 'error', new IP 'openBracket', grp for grp in e.groups
output.sendIP 'error', new IP 'data', e.err
output.sendIP 'error', new IP 'closeBracket', grp for grp in e.groups
output.sendIP 'error', new IP 'closeBracket', config.name if config.name
component.hasErrors = false
errors = []
component.hasErrors = false
component.error = errorHandler
component.fail = failHandler
sendErrors
setupSendDefaults = (component) ->
portsWithDefaults = Object.keys(component.inPorts.ports).filter (p) ->
return false unless component.inPorts[p].options.control
return false unless component.inPorts[p].hasDefault()
true
component.sendDefaults = ->
platform.deprecated 'noflo.helpers.WirePattern sendDefaults method is deprecated. Please start with a Network'
portsWithDefaults.forEach (port) ->
tempSocket = InternalSocket.createSocket()
component.inPorts[port].attach tempSocket
tempSocket.send()
tempSocket.disconnect()
component.inPorts[port].detach tempSocket
populateParams = (config, input) ->
return unless config.params.length
params = {}
for paramPort in config.params
if input.ports[paramPort].isAddressable()
params[paramPort] = {}
for idx in input.attached paramPort
continue unless input.hasData [paramPort, idx]
params[paramPort][idx] = input.getData [paramPort, idx]
continue
params[paramPort] = input.getData paramPort
return params
reorderBuffer = (buffer, matcher) ->
# Move matching IP packet to be first in buffer
#
# Note: the collation mechanism as shown below is not a
# very nice way to deal with inputs as it messes with
# input buffer order. Much better to handle collation
# in a specialized component or to separate flows by
# scope.
#
# The trick here is to order the input in a way that
# still allows bracket forwarding to work. So if we
# want to first process packet B in stream like:
#
# < 1
# < 2
# A
# > 2
# < 3
# B
# > 3
# > 1
#
# We need to change the stream to be like:
#
# < 1
# < 3
# B
# > 3
# < 2
# A
# > 2
# > 1
substream = null
brackets = []
substreamBrackets = []
for ip, idx in buffer
if ip.type is 'openBracket'
brackets.push ip.data
substreamBrackets.push ip
continue
if ip.type is 'closeBracket'
brackets.pop()
substream.push ip if substream
substreamBrackets.pop() if substreamBrackets.length
break if substream and not substreamBrackets.length
continue
unless matcher ip, brackets
# Reset substream bracket tracking when we hit data
substreamBrackets = []
continue
# Match found, start tracking the actual substream
substream = substreamBrackets.slice 0
substream.push ip
# See where in the buffer the matching substream begins
substreamIdx = buffer.indexOf substream[0]
# No need to reorder if matching packet is already first
return if substreamIdx is 0
# Remove substream from its natural position
buffer.splice substreamIdx, substream.length
# Place the substream in the beginning
substream.reverse()
buffer.unshift ip for ip in substream
handleInputCollation = (data, config, input, port, idx) ->
return if not config.group and not config.field
if config.group
buf = input.ports[port].getBuffer input.scope, idx
reorderBuffer buf, (ip, brackets) ->
for grp, idx in input.collatedBy.brackets
return false unless brackets[idx] is grp
true
if config.field
data[config.field] = input.collatedBy.field
buf = input.ports[port].getBuffer input.scope, idx
reorderBuffer buf, (ip) ->
ip.data[config.field] is data[config.field]
getInputData = (config, input) ->
data = {}
for port in config.inPorts
if input.ports[port].isAddressable()
data[port] = {}
for idx in input.attached port
continue unless input.hasData [port, idx]
handleInputCollation data, config, input, port, idx
data[port][idx] = input.getData [port, idx]
continue
continue unless input.hasData port
handleInputCollation data, config, input, port
data[port] = input.getData port
if config.inPorts.length is 1
return data[config.inPorts[0]]
return data
getGroupContext = (component, port, input) ->
return [] unless input.result.__bracketContext?[port]?
return input.collatedBy.brackets if input.collatedBy?.brackets
input.result.__bracketContext[port].filter((c) ->
c.source is port
).map (c) -> c.ip.data
getOutputProxy = (ports, output) ->
outProxy = {}
ports.forEach (port) ->
outProxy[port] =
connect: ->
beginGroup: (group, idx) ->
ip = new IP 'openBracket', group
ip.index = idx
output.sendIP port, ip
send: (data, idx) ->
ip = new IP 'data', data
ip.index = idx
output.sendIP port, ip
endGroup: (group, idx) ->
ip = new IP 'closeBracket', group
ip.index = idx
output.sendIP port, ip
disconnect: ->
if ports.length is 1
return outProxy[ports[0]]
return outProxy
checkWirePatternPreconditions = (config, input, output) ->
# First check for required params
paramsOk = checkWirePatternPreconditionsParams config, input
# Then check actual input ports
inputsOk = checkWirePatternPreconditionsInput config, input
# If input port has data but param requirements are not met, and we're in dropInput
# mode, read the data and call done
if config.dropInput and not paramsOk
# Drop all received input packets since params are not available
packetsDropped = false
for port in config.inPorts
if input.ports[port].isAddressable()
attached = input.attached port
continue unless attached.length
for idx in attached
while input.has [port, idx]
packetsDropped = true
input.get([port, idx]).drop()
continue
while input.has port
packetsDropped = true
input.get(port).drop()
# If we ended up dropping inputs because of missing params, we need to
# deactivate here
output.done() if packetsDropped
# Pass precondition check only if both params and inputs are OK
return inputsOk and paramsOk
checkWirePatternPreconditionsParams = (config, input) ->
for param in config.params
continue unless input.ports[param].isRequired()
if input.ports[param].isAddressable()
attached = input.attached param
return false unless attached.length
withData = attached.filter (idx) -> input.hasData [param, idx]
if config.arrayPolicy.params is 'all'
return false unless withData.length is attached.length
continue
return false unless withData.length
continue
return false unless input.hasData param
true
checkWirePatternPreconditionsInput = (config, input) ->
if config.group
bracketsAtPorts = {}
input.collatedBy =
brackets: []
ready: false
checkBrackets = (left, right) ->
for bracket, idx in left
return false unless right[idx] is bracket
true
checkPacket = (ip, brackets) ->
# With data packets we validate bracket matching
bracketsToCheck = brackets.slice 0
if config.group instanceof RegExp
# Basic regexp validation for the brackets
bracketsToCheck = bracketsToCheck.slice 0, 1
return false unless bracketsToCheck.length
return false unless config.group.test bracketsToCheck[0]
if input.collatedBy.ready
# We already know what brackets we're looking for, match
return checkBrackets input.collatedBy.brackets, bracketsToCheck
bracketId = bracketsToCheck.join ':'
bracketsAtPorts[bracketId] = [] unless bracketsAtPorts[bracketId]
if bracketsAtPorts[bracketId].indexOf(port) is -1
# Register that this port had these brackets
bracketsAtPorts[bracketId].push port
# To prevent deadlocks we see all bracket sets, and validate if at least
# one of them matches. This means we return true until the last inport
# where we actually check.
return true unless config.inPorts.indexOf(port) is config.inPorts.length - 1
# Brackets that are not in every port are invalid
return false unless bracketsAtPorts[bracketId].length is config.inPorts.length
return false if input.collatedBy.ready
input.collatedBy.ready = true
input.collatedBy.brackets = bracketsToCheck
true
if config.field
input.collatedBy =
field: undefined
ready: false
checkPort = (port) ->
# Without collation rules any data packet is OK
return input.hasData port if not config.group and not config.field
# With collation rules set we need can only work when we have full
# streams
if config.group
portBrackets = []
dataBrackets = []
hasMatching = false
buf = input.ports[port].getBuffer input.scope
for ip in buf
if ip.type is 'openBracket'
portBrackets.push ip.data
continue
if ip.type is 'closeBracket'
portBrackets.pop()
continue if portBrackets.length
continue unless hasData
hasMatching = true
continue
hasData = checkPacket ip, portBrackets
continue
return hasMatching
if config.field
return input.hasStream port, (ip) ->
# Use first data packet to define what to collate by
unless input.collatedBy.ready
input.collatedBy.field = ip.data[config.field]
input.collatedBy.ready = true
return true
return ip.data[config.field] is input.collatedBy.field
for port in config.inPorts
if input.ports[port].isAddressable()
attached = input.attached port
return false unless attached.length
withData = attached.filter (idx) -> checkPort [port, idx]
if config.arrayPolicy['in'] is 'all'
return false unless withData.length is attached.length
continue
return false unless withData.length
continue
return false unless checkPort port
true
# Wraps OutPort in WirePattern to add transparent scope support
class OutPortWrapper
constructor: (@port, @scope) ->
connect: (socketId = null) ->
@port.openBracket null, scope: @scope, socketId
beginGroup: (group, socketId = null) ->
@port.openBracket group, scope: @scope, socketId
send: (data, socketId = null) ->
@port.sendIP 'data', data, scope: @scope, socketId, false
endGroup: (group, socketId = null) ->
@port.closeBracket group, scope: @scope, socketId
disconnect: (socketId = null) ->
@endGroup socketId
isConnected: -> @port.isConnected()
isAttached: -> @port.isAttached()
# Legacy WirePattern implementation. We fall back to this with
# some deprecated parameters.
legacyWirePattern = (component, config, proc) ->
# 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 any port, as we group by them
if collectGroups isnt false and config.group
collectGroups = true
for name in config.inPorts
unless component.inPorts[name]
throw new Error "no inPort named '#{name}'"
for name in config.outPorts
unless component.outPorts[name]
throw new Error "no outPort named '#{name}'"
disconnectOuts = ->
# Manual disconnect forwarding
for p in config.outPorts
component.outPorts[p].disconnect() if component.outPorts[p].isConnected()
sendGroupToOuts = (grp) ->
for p in config.outPorts
component.outPorts[p].beginGroup grp
closeGroupOnOuts = (grp) ->
for p in config.outPorts
component.outPorts[p].endGroup grp
# Declarations
component.requiredParams = []
component.defaultedParams = []
component.gcCounter = 0
component._wpData = {}
_wp = (scope) ->
unless scope of component._wpData
component._wpData[scope] = {}
# Input grouping
component._wpData[scope].groupedData = {}
component._wpData[scope].groupedGroups = {}
component._wpData[scope].groupedDisconnects = {}
# Params and queues
component._wpData[scope].outputQ = []
component._wpData[scope].taskQ = []
component._wpData[scope].params = {}
component._wpData[scope].completeParams = []
component._wpData[scope].receivedParams = []
component._wpData[scope].defaultsSent = false
# Disconnect event forwarding
component._wpData[scope].disconnectData = {}
component._wpData[scope].disconnectQ = []
# GC and rest
component._wpData[scope].groupBuffers = {}
component._wpData[scope].keyBuffers = {}
component._wpData[scope].gcTimestamps = {}
component._wpData[scope]
component.params = {}
setParamsScope = (scope) ->
component.params = _wp(scope).params
# For ordered output
processQueue = (scope) ->
while _wp(scope).outputQ.length > 0
streams = _wp(scope).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 config.outPorts.length is 1
tmp = {}
tmp[config.outPorts[0]] = streams
streams = tmp
for key, stream of streams
if stream.resolved
stream.flush()
flushed = true
_wp(scope).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 = (scope, outs) ->
_wp(scope).outputQ.push outs if config.ordered
component.load++
component.emit 'activate', component.load
if 'load' of component.outPorts and component.outPorts.load.isAttached()
component.outPorts.load.send component.load
component.outPorts.load.disconnect()
component.afterProcess = (scope, err, outs) ->
processQueue scope
component.load--
if 'load' of component.outPorts and component.outPorts.load.isAttached()
component.outPorts.load.send component.load
component.outPorts.load.disconnect()
component.emit 'deactivate', component.load
component.sendDefaults = (scope) ->
if component.defaultedParams.length > 0
for param in component.defaultedParams
if _wp(scope).receivedParams.indexOf(param) is -1
tempSocket = InternalSocket.createSocket()
component.inPorts[param].attach tempSocket
tempSocket.send()
tempSocket.disconnect()
component.inPorts[param].detach tempSocket
_wp(scope).defaultsSent = true
resumeTaskQ = (scope) ->
if _wp(scope).completeParams.length is component.requiredParams.length and
_wp(scope).taskQ.length > 0
# Avoid looping when feeding the queue inside the queue itself
temp = _wp(scope).taskQ.slice 0
_wp(scope).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.handle = (ip) ->
event = ip.type
payload = ip.data
scope = ip.scope
index = ip.index
# Param ports only react on data
return unless event is 'data'
if inPort.isAddressable()
_wp(scope).params[port] = {} unless port of _wp(scope).params
_wp(scope).params[port][index] = payload
if config.arrayPolicy.params is 'all' and
Object.keys(_wp(scope).params[port]).length < inPort.listAttached().length
return # Need data on all array indexes to proceed
else
_wp(scope).params[port] = payload
if _wp(scope).completeParams.indexOf(port) is -1 and
component.requiredParams.indexOf(port) > -1
_wp(scope).completeParams.push port
_wp(scope).receivedParams.push port
# Trigger pending procs if all params are complete
resumeTaskQ scope
# Garbage collector
component.dropRequest = (scope, key) ->
# Discard pending disconnect keys
delete _wp(scope).disconnectData[key] if key of _wp(scope).disconnectData
# Clean grouped data
delete _wp(scope).groupedData[key] if key of _wp(scope).groupedData
delete _wp(scope).groupedGroups[key] if key of _wp(scope).groupedGroups
gc = ->
component.gcCounter++
if component.gcCounter % config.gcFrequency is 0
for scope in Object.keys(component._wpData)
current = new Date().getTime()
for key, val of _wp(scope).gcTimestamps
if (current - val) > (config.gcTimeout * 1000)
component.dropRequest scope, key
delete _wp(scope).gcTimestamps[key]
# Grouped ports
for port in config.inPorts
do (port) ->
# Support for StreamReceiver ports
# if config.receiveStreams and config.receiveStreams.indexOf(port) isnt -1
# inPort = new StreamReceiver component.inPorts[port]
inPort = component.inPorts[port]
needPortGroups = collectGroups instanceof Array and collectGroups.indexOf(port) isnt -1
# Set processing callback
inPort.handle = (ip) ->
index = ip.index
payload = ip.data
scope = ip.scope
_wp(scope).groupBuffers[port] = [] unless port of _wp(scope).groupBuffers
_wp(scope).keyBuffers[port] = null unless port of _wp(scope).keyBuffers
switch ip.type
when 'openBracket'
return if payload is null
_wp(scope).groupBuffers[port].push payload
if config.forwardGroups and (collectGroups is true or needPortGroups) and not config.async
sendGroupToOuts payload
when 'closeBracket'
_wp(scope).groupBuffers[port] = _wp(scope).groupBuffers[port].slice 0, _wp(scope).groupBuffers[port].length - 1
if config.forwardGroups and (collectGroups is true or needPortGroups) and not config.async
# FIXME probably need to skip this if payload is null
closeGroupOnOuts payload
# Disconnect
if _wp(scope).groupBuffers[port].length is 0
if config.inPorts.length is 1
if config.async or config.StreamSender
if config.ordered
_wp(scope).outputQ.push null
processQueue scope
else
_wp(scope).disconnectQ.push true
else
disconnectOuts()
else
foundGroup = false
key = _wp(scope).keyBuffers[port]
_wp(scope).disconnectData[key] = [] unless key of _wp(scope).disconnectData
for i in [0..._wp(scope).disconnectData[key].length]
unless port of _wp(scope).disconnectData[key][i]
foundGroup = true
_wp(scope).disconnectData[key][i][port] = true
if Object.keys(_wp(scope).disconnectData[key][i]).length is config.inPorts.length
_wp(scope).disconnectData[key].shift()
if config.async or config.StreamSender
if config.ordered
_wp(scope).outputQ.push null
processQueue scope
else
_wp(scope).disconnectQ.push true
else
disconnectOuts()
delete _wp(scope).disconnectData[key] if _wp(scope).disconnectData[key].length is 0
break
unless foundGroup
obj = {}
obj[port] = true
_wp(scope).disconnectData[key].push obj
when 'data'
if config.inPorts.length is 1 and not inPort.isAddressable()
data = payload
groups = _wp(scope).groupBuffers[port]
else
key = ''
if config.group and _wp(scope).groupBuffers[port].length > 0
key = _wp(scope).groupBuffers[port].toString()
if config.group instanceof RegExp
reqId = null
for grp in _wp(scope).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]
_wp(scope).keyBuffers[port] = key
_wp(scope).groupedData[key] = [] unless key of _wp(scope).groupedData
_wp(scope).groupedGroups[key] = [] unless key of _wp(scope).groupedGroups
foundGroup = false
requiredLength = config.inPorts.length
++requiredLength if config.field
# Check buffered tuples awaiting completion
for i in [0..._wp(scope).groupedData[key].length]
# Check this buffered tuple if it's missing value for this port
if not (port of _wp(scope).groupedData[key][i]) or
(component.inPorts[port].isAddressable() and
config.arrayPolicy.in is 'all' and
not (index of _wp(scope).groupedData[key][i][port]))
foundGroup = true
if component.inPorts[port].isAddressable()
# Maintain indexes for addressable ports
unless port of _wp(scope).groupedData[key][i]
_wp(scope).groupedData[key][i][port] = {}
_wp(scope).groupedData[key][i][port][index] = payload
else
_wp(scope).groupedData[key][i][port] = payload
if needPortGroups
# Include port groups into the set of the unique ones
_wp(scope).groupedGroups[key][i] = utils.unique [_wp(scope).groupedGroups[key][i]..., _wp(scope).groupBuffers[port]...]
else if collectGroups is true
# All the groups we need are here in this port
_wp(scope).groupedGroups[key][i][port] = _wp(scope).groupBuffers[port]
# Addressable ports may require other indexes
if component.inPorts[port].isAddressable() and
config.arrayPolicy.in is 'all' and
Object.keys(_wp(scope).groupedData[key][i][port]).length <
component.inPorts[port].listAttached().length
return # Need data on other array port indexes to arrive
groupLength = Object.keys(_wp(scope).groupedData[key][i]).length
# Check if the tuple is complete
if groupLength is requiredLength
data = (_wp(scope).groupedData[key].splice i, 1)[0]
# Strip port name if there's only one inport
if config.inPorts.length is 1 and inPort.isAddressable()
data = data[port]
groups = (_wp(scope).groupedGroups[key].splice i, 1)[0]
if collectGroups is true
groups = utils.intersection.apply null, utils.getValues groups
delete _wp(scope).groupedData[key] if _wp(scope).groupedData[key].length is 0
delete _wp(scope).groupedGroups[key] if _wp(scope).groupedGroups[key].length is 0
if config.group and key
delete _wp(scope).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 config.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 = _wp(scope).groupBuffers[port]
else
_wp(scope).groupedData[key].push obj
if needPortGroups
_wp(scope).groupedGroups[key].push _wp(scope).groupBuffers[port]
else if collectGroups is true
tmp = {} ; tmp[port] = _wp(scope).groupBuffers[port]
_wp(scope).groupedGroups[key].push tmp
else
_wp(scope).groupedGroups[key].push []
if config.group and key
# Timestamp to garbage collect this request
_wp(scope).gcTimestamps[key] = new Date().getTime()
return # need more data to continue
# Drop premature data if configured to do so
return if config.dropInput and _wp(scope).completeParams.length isnt component.requiredParams.length
# Prepare outputs
outs = {}
for name in config.outPorts
wrp = new OutPortWrapper component.outPorts[name], scope
if config.async or config.sendStreams and
config.sendStreams.indexOf(name) isnt -1
wrp
outs[name] = new StreamSender wrp, config.ordered
else
outs[name] = wrp
outs = outs[config.outPorts[0]] if config.outPorts.length is 1 # for simplicity
groups = [] unless groups
# Filter empty connect/disconnect groups
groups = (g for g in groups when g isnt null)
whenDoneGroups = groups.slice 0
whenDone = (err) ->
if err
component.error err, whenDoneGroups, 'error', scope
# For use with MultiError trait
if typeof component.fail is 'function' and component.hasErrors
component.fail null, [], scope
# Disconnect outputs if still connected,
# this also indicates them as resolved if pending
outputs = outs
if config.outPorts.length is 1
outputs = {}
outputs[port] = outs
disconnect = false
if _wp(scope).disconnectQ.length > 0
_wp(scope).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 scope, err or component.hasErrors, outs
# Before hook
if typeof component.beforeProcess is 'function'
component.beforeProcess scope, outs
# Group forwarding
if config.forwardGroups and config.async
if config.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, scope
debug "WirePattern Legacy API call with", data, groups, component.params, scope
# Call the proc function
if config.async
postpone = ->
resume = ->
postponedToQ = false
task = ->
setParamsScope scope
proc.call component, data, groups, outs, whenDone, postpone, resume, scope
postpone = (backToQueue = true) ->
postponedToQ = backToQueue
if backToQueue
_wp(scope).taskQ.push task
resume = ->
if postponedToQ then resumeTaskQ() else task()
else
task = ->
setParamsScope scope
proc.call component, data, groups, outs, null, null, null, scope
whenDone()
_wp(scope).taskQ.push task
resumeTaskQ scope
# Call the garbage collector
gc()
# Overload tearDown method to clean WirePattern state
baseTearDown = component.tearDown
component.tearDown = (callback) ->
component.requiredParams = []
component.defaultedParams = []
component.gcCounter = 0
component._wpData = {}
component.params = {}
baseTearDown.call component, callback
# 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 = [], scope = null) ->
platform.deprecated 'noflo.helpers.MultiError is deprecated. Send errors to error port instead'
component.hasErrors = false
component.errors = []
group = component.name if component.name and not group
group = 'Component' unless group
# 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].openBracket group, scope: scope if group
for error in component.errors
component.outPorts[errorPort].openBracket grp, scope: scope for grp in error.groups
component.outPorts[errorPort].data error.err, scope: scope
component.outPorts[errorPort].closeBracket grp, scope: scope for grp in error.groups
component.outPorts[errorPort].closeBracket group, scope: scope if group
# component.outPorts[errorPort].disconnect()
# Clean the status for next activation
component.hasErrors = false
component.errors = []
# Overload shutdown method to clear errors
baseTearDown = component.tearDown
component.tearDown = (callback) ->
component.hasErrors = false
component.errors = []
baseTearDown.call component, callback
return component