noflo
Version:
Flow-Based Programming environment for JavaScript
1,678 lines (1,528 loc) • 53.2 kB
text/coffeescript
if typeof process isnt 'undefined' and process.execPath and process.execPath.match /node|iojs/
chai = require 'chai' unless chai
helpers = require '../src/lib/Helpers'
component = require '../src/lib/Component'
socket = require '../src/lib/InternalSocket'
Substream = require('../src/lib/Streams').Substream
else
helpers = require 'noflo/src/lib/Helpers'
component = require 'noflo/src/lib/Component'
socket = require 'noflo/src/lib/InternalSocket'
Substream = require('noflo/src/lib/Streams').Substream
describe 'Component traits', ->
describe 'MapComponent', ->
c = null
it 'should pass data to the callback', ->
c = new component.Component
c.inPorts.add 'in'
c.outPorts.add 'out',
required: false
helpers.MapComponent c, (data) ->
chai.expect(data).to.equal 1
s = new socket.createSocket()
c.inPorts.in.attach s
s.send 1
it 'should pass groups to the callback', ->
c = new component.Component
c.inPorts.add 'in'
c.outPorts.add 'out',
required: false
helpers.MapComponent c, (data, groups) ->
chai.expect(groups).to.eql [
'one'
'two'
]
chai.expect(data).to.equal 1
s = new socket.createSocket()
c.inPorts.in.attach s
s.beginGroup 'one'
s.beginGroup 'two'
s.send 1
it 'should send groups and disconnect through', (done) ->
c = new component.Component
c.inPorts.add 'in'
c.outPorts.add 'out',
required: false
helpers.MapComponent c, (data, groups, out) ->
out.send data * 2
s = new socket.createSocket()
c.inPorts.in.attach s
s2 = new socket.createSocket()
c.outPorts.out.attach s2
groups = []
s2.on 'begingroup', (group) ->
groups.push group
s2.on 'data', (data) ->
chai.expect(groups.length).to.equal 2
chai.expect(data).to.equal 6
s2.on 'endgroup', ->
groups.pop()
s2.on 'disconnect', ->
chai.expect(groups.length).to.equal 0
done()
s.beginGroup 'one'
s.beginGroup 'two'
s.send 3
s.endGroup()
s.endGroup()
s.disconnect()
describe 'WirePattern', ->
describe 'when grouping by packet groups', ->
c = new component.Component
c.inPorts.add 'x',
required: true
datatype: 'int'
.add 'y',
required: true
datatype: 'int'
.add 'z',
required: true
datatype: 'int'
c.outPorts.add 'point'
x = new socket.createSocket()
y = new socket.createSocket()
z = new socket.createSocket()
p = new socket.createSocket()
c.inPorts.x.attach x
c.inPorts.y.attach y
c.inPorts.z.attach z
c.outPorts.point.attach p
it 'should pass data and groups to the callback', (done) ->
src =
111: {x: 1, y: 2, z: 3}
222: {x: 4, y: 5, z: 6}
333: {x: 7, y: 8, z: 9}
helpers.WirePattern c,
in: ['x', 'y', 'z']
out: 'point'
group: true
forwardGroups: true
, (data, groups, out) ->
chai.expect(groups.length).to.be.above 0
chai.expect(data).to.deep.equal src[groups[0]]
out.send data
groups = []
count = 0
p.on 'begingroup', (grp) ->
groups.push grp
p.on 'endgroup', ->
groups.pop()
p.on 'data', (data) ->
count++
p.on 'disconnect', ->
done() if count is 3 and groups.length is 0
for key, grp of src
x.beginGroup key
y.beginGroup key
z.beginGroup key
x.send grp.x
y.send grp.y
z.send grp.z
x.endGroup()
y.endGroup()
z.endGroup()
x.disconnect()
y.disconnect()
z.disconnect()
it 'should work without a group provided', (done) ->
p.removeAllListeners()
helpers.WirePattern c,
in: ['x', 'y', 'z']
out: 'point'
, (data, groups, out) ->
chai.expect(groups.length).to.equal 0
out.send {x: data.x, y: data.y, z: data.z}
p.once 'data', (data) ->
chai.expect(data).to.deep.equal {x: 123, y: 456, z: 789}
done()
x.send 123
x.disconnect()
y.send 456
y.disconnect()
z.send 789
z.disconnect()
it 'should process inputs for different groups independently with group: true', (done) ->
src =
1: {x: 1, y: 2, z: 3}
2: {x: 4, y: 5, z: 6}
3: {x: 7, y: 8, z: 9}
inOrder = [
[ 1, 'x' ]
[ 3, 'z' ]
[ 2, 'y' ]
[ 2, 'x' ]
[ 1, 'z' ]
[ 2, 'z' ]
[ 3, 'x' ]
[ 1, 'y' ]
[ 3, 'y' ]
]
outOrder = [ 2, 1, 3 ]
helpers.WirePattern c,
in: ['x', 'y', 'z']
out: 'point'
group: true
forwardGroups: true
, (data, groups, out) ->
out.send {x: data.x, y: data.y, z: data.z}
groups = []
p.on 'begingroup', (grp) ->
groups.push grp
p.on 'endgroup', (grp) ->
groups.pop()
p.on 'data', (data) ->
chai.expect(groups.length).to.equal 1
chai.expect(groups[0]).to.equal outOrder[0]
chai.expect(data).to.deep.equal src[outOrder[0]]
outOrder.shift()
done() unless outOrder.length
for tuple in inOrder
input = null
switch tuple[1]
when 'x'
input = x
when 'y'
input = y
when 'z'
input = z
input.beginGroup tuple[0]
input.send src[tuple[0]][tuple[1]]
input.endGroup()
input.disconnect()
it 'should support asynchronous handlers', (done) ->
point =
x: 123
y: 456
z: 789
helpers.WirePattern c,
in: ['x', 'y', 'z']
out: 'point'
async: true
group: true
forwardGroups: true
, (data, groups, out, callback) ->
setTimeout ->
out.send {x: data.x, y: data.y, z: data.z}
callback()
, 100
p.removeAllListeners()
counter = 0
hadData = false
p.on 'begingroup', (grp) ->
counter++
p.on 'endgroup', ->
counter--
p.once 'data', (data) ->
chai.expect(data).to.deep.equal point
hadData = true
p.once 'disconnect', ->
chai.expect(counter).to.equal 0
chai.expect(hadData).to.be.true
done()
x.beginGroup 'async'
y.beginGroup 'async'
z.beginGroup 'async'
x.send point.x
y.send point.y
z.send point.z
x.endGroup()
y.endGroup()
z.endGroup()
x.disconnect()
y.disconnect()
z.disconnect()
it 'should not forward groups if forwarding is off', (done) ->
point =
x: 123
y: 456
helpers.WirePattern c,
in: ['x', 'y']
out: 'point'
, (data, groups, out) ->
out.send { x: data.x, y: data.y }
p.removeAllListeners()
counter = 0
hadData = false
p.on 'begingroup', (grp) ->
counter++
p.on 'data', (data) ->
chai.expect(data).to.deep.equal point
hadData = true
p.once 'disconnect', ->
chai.expect(counter).to.equal 0
chai.expect(hadData).to.be.true
done()
x.beginGroup 'doNotForwardMe'
y.beginGroup 'doNotForwardMe'
x.send point.x
y.send point.y
x.endGroup()
y.endGroup()
x.disconnect()
y.disconnect()
it 'should forward groups from a specific port only', (done) ->
point =
x: 123
y: 456
z: 789
refGroups = ['boo']
helpers.WirePattern c,
in: ['x', 'y', 'z']
out: 'point'
forwardGroups: 'y'
, (data, groups, out) ->
out.send { x: data.x, y: data.y, z: data.z }
p.removeAllListeners()
groups = []
p.on 'begingroup', (grp) ->
groups.push grp
p.on 'data', (data) ->
chai.expect(data).to.deep.equal point
p.once 'disconnect', ->
chai.expect(groups).to.deep.equal refGroups
done()
x.beginGroup 'foo'
y.beginGroup 'boo'
z.beginGroup 'bar'
x.send point.x
y.send point.y
z.send point.z
x.endGroup()
y.endGroup()
z.endGroup()
x.disconnect()
y.disconnect()
z.disconnect()
it 'should forward groups from selected ports only', (done) ->
point =
x: 123
y: 456
z: 789
refGroups = ['foo', 'bar']
helpers.WirePattern c,
in: ['x', 'y', 'z']
out: 'point'
forwardGroups: [ 'x', 'z' ]
, (data, groups, out) ->
out.send { x: data.x, y: data.y, z: data.z }
p.removeAllListeners()
groups = []
p.on 'begingroup', (grp) ->
groups.push grp
p.on 'data', (data) ->
chai.expect(data).to.deep.equal point
p.once 'disconnect', ->
chai.expect(groups).to.deep.equal refGroups
done()
x.beginGroup 'foo'
y.beginGroup 'boo'
z.beginGroup 'bar'
x.send point.x
y.send point.y
z.send point.z
x.endGroup()
y.endGroup()
z.endGroup()
x.disconnect()
y.disconnect()
z.disconnect()
describe 'when `this` context is important', ->
c = new component.Component
c.inPorts.add 'x',
required: true
datatype: 'int'
.add 'y',
required: true
datatype: 'int'
.add 'z',
required: true
datatype: 'int'
c.outPorts.add 'point'
x = new socket.createSocket()
y = new socket.createSocket()
z = new socket.createSocket()
p = new socket.createSocket()
c.inPorts.x.attach x
c.inPorts.y.attach y
c.inPorts.z.attach z
c.outPorts.point.attach p
it 'should correctly bind component to `this` context', (done) ->
p.removeAllListeners()
helpers.WirePattern c,
in: ['x', 'y', 'z']
out: 'point'
, (data, groups, out) ->
chai.expect(this).to.deep.equal c
out.send {x: data.x, y: data.y, z: data.z}
p.once 'data', (data) ->
chai.expect(data).to.deep.equal {x: 123, y: 456, z: 789}
done()
x.send 123
x.disconnect()
y.send 456
y.disconnect()
z.send 789
z.disconnect()
it 'should correctly bind component to `this` context in async mode', (done) ->
p.removeAllListeners()
helpers.WirePattern c,
in: ['x', 'y', 'z']
async: true
out: 'point'
, (data, groups, out, callback) ->
chai.expect(this).to.deep.equal c
out.send {x: data.x, y: data.y, z: data.z}
callback()
p.once 'data', (data) ->
done()
x.send 123
x.disconnect()
y.send 456
y.disconnect()
z.send 789
z.disconnect()
describe 'when in async mode and packet order matters', ->
c = new component.Component
c.inPorts.add 'delay', datatype: 'int'
.add 'msg', datatype: 'string'
c.outPorts.add 'out', datatype: 'object'
.add 'load', datatype: 'int'
delay = new socket.createSocket()
msg = new socket.createSocket()
out = new socket.createSocket()
load = new socket.createSocket()
c.inPorts.delay.attach delay
c.inPorts.msg.attach msg
c.outPorts.out.attach out
c.outPorts.load.attach load
it 'should preserve input order at the output', (done) ->
helpers.WirePattern c,
in: ['delay', 'msg']
async: true
ordered: true
group: false
, (data, groups, res, callback) ->
setTimeout ->
res.send { delay: data.delay, msg: data.msg }
callback()
, data.delay
sample = [
{ delay: 30, msg: "one" }
{ delay: 0, msg: "two" }
{ delay: 20, msg: "three" }
{ delay: 10, msg: "four" }
]
out.on 'data', (data) ->
chai.expect(data).to.deep.equal sample.shift()
out.on 'disconnect', ->
done() if sample.length is 0
expected = [1, 2, 3, 4, 3, 2, 1, 0]
load.on 'data', (data) ->
chai.expect(data).to.equal expected.shift()
idx = 0
for ip in sample
delay.beginGroup idx
delay.send ip.delay
delay.endGroup()
msg.beginGroup idx
msg.send ip.msg
msg.endGroup()
delay.disconnect()
msg.disconnect()
idx++
it 'should support complex substreams', (done) ->
out.removeAllListeners()
load.removeAllListeners()
c.cntr = 0
helpers.WirePattern c,
in: ['delay', 'msg']
async: true
ordered: true
group: false
receiveStreams: ['delay', 'msg']
, (data, groups, res, callback) ->
# Substream to object conversion validation
# (the hard way)
chai.expect(data.delay instanceof Substream).to.be.true
chai.expect(data.msg instanceof Substream).to.be.true
delayObj = data.delay.toObject()
msgObj = data.msg.toObject()
index0 = this.cntr.toString()
chai.expect(Object.keys(delayObj)[0]).to.equal index0
chai.expect(Object.keys(msgObj)[0]).to.equal index0
subDelay = delayObj[index0]
subMsg = msgObj[index0]
index1 = (10 + this.cntr).toString()
chai.expect(Object.keys(subDelay)[0]).to.equal index1
chai.expect(Object.keys(subMsg)[0]).to.equal index1
delayData = subDelay[index1]
msgData = subMsg[index1]
chai.expect(delayData).to.equal sample[c.cntr].delay
chai.expect(msgData).to.equal sample[c.cntr].msg
this.cntr++
setTimeout ->
# Substream tree traversal (the easy way)
for k0, v0 of msgObj
res.beginGroup k0
res.send k0
for k1, v1 of v0
res.beginGroup k1
res.send
delay: delayObj[k0][k1]
msg: msgObj[k0][k1]
res.endGroup()
res.send k1
res.endGroup()
callback()
, data.delay
sample = [
{ delay: 30, msg: "one" }
{ delay: 0, msg: "two" }
{ delay: 20, msg: "three" }
{ delay: 10, msg: "four" }
]
expected = [
'0', '0', '10', sample[0], '10'
'1', '1', '11', sample[1], '11'
'2', '2', '12', sample[2], '12'
'3', '3', '13', sample[3], '13'
]
out.on 'begingroup', (grp) ->
chai.expect(grp).to.equal expected.shift()
out.on 'data', (data) ->
chai.expect(data).to.deep.equal expected.shift()
out.on 'disconnect', ->
done() if expected.length is 0
for i in [0..3]
delay.beginGroup i
delay.beginGroup 10 + i
delay.send sample[i].delay
delay.endGroup()
delay.endGroup()
msg.beginGroup i
msg.beginGroup 10 + i
msg.send sample[i].msg
msg.endGroup()
msg.endGroup()
delay.disconnect()
msg.disconnect()
describe 'when grouping by field', ->
c = new component.Component
c.inPorts.add 'user', datatype: 'object'
.add 'message', datatype: 'object'
c.outPorts.add 'signedmessage'
usr = new socket.createSocket()
msg = new socket.createSocket()
umsg = new socket.createSocket()
c.inPorts.user.attach usr
c.inPorts.message.attach msg
c.outPorts.signedmessage.attach umsg
it 'should match objects by specific field', (done) ->
helpers.WirePattern c,
in: ['user', 'message']
out: 'signedmessage'
async: true
field: 'request'
, (data, groups, out, callback) ->
setTimeout ->
out.send
request: data.request
user: data.user.name
text: data.message.text
callback()
, 10
users =
14: {request: 14, id: 21, name: 'Josh'}
12: {request: 12, id: 25, name: 'Leo'}
34: {request: 34, id: 84, name: 'Anica'}
messages =
34: {request: 34, id: 234, text: 'Hello world'}
12: {request: 12, id: 82, text: 'Aloha amigos'}
14: {request: 14, id: 249, text: 'Node.js ftw'}
counter = 0
umsg.on 'data', (data) ->
chai.expect(data).to.be.an 'object'
chai.expect(data.request).to.be.ok
chai.expect(data.user).to.equal users[data.request].name
chai.expect(data.text).to.equal messages[data.request].text
counter++
done() if counter is 3
# Send input asynchronously with mixed delays
for req, user of users
do (req, user) ->
setTimeout ->
usr.send user
usr.disconnect()
, req
for req, mesg of messages
do (req, mesg) ->
setTimeout ->
msg.send mesg
msg.disconnect()
, req
describe 'when there are multiple output routes', ->
c = new component.Component
c.inPorts.add 'num', datatype: 'int'
.add 'str', datatype: 'string'
c.outPorts.add 'odd', datatype: 'object'
.add 'even', datatype: 'object'
num = new socket.createSocket()
str = new socket.createSocket()
odd = new socket.createSocket()
even = new socket.createSocket()
c.inPorts.num.attach num
c.inPorts.str.attach str
c.outPorts.odd.attach odd
c.outPorts.even.attach even
it 'should send output to one or more of them', (done) ->
numbers = ['cero', 'uno', 'dos', 'tres', 'cuatro', 'cinco', 'seis', 'siete', 'ocho', 'nueve']
helpers.WirePattern c,
in: ['num', 'str']
out: ['odd', 'even']
async: true
ordered: true
, (data, groups, outs, callback) ->
setTimeout ->
if data.num % 2 is 1
outs.odd.beginGroup grp for grp in groups
outs.odd.send data
outs.odd.endGroup() for grp in groups
else
outs.even.beginGroup grp for grp in groups
outs.even.send data
outs.even.endGroup() for grp in groups
callback()
, 0
grpCounter = 0
dataCounter = 0
odd.on 'begingroup', (grp) ->
grpCounter++
odd.on 'data', (data) ->
chai.expect(data.num % 2).to.equal 1
chai.expect(data.str).to.equal numbers[data.num]
dataCounter++
odd.on 'disconnect', ->
done() if dataCounter is 10 and grpCounter is 10
even.on 'begingroup', (grp) ->
grpCounter++
even.on 'data', (data) ->
chai.expect(data.num % 2).to.equal 0
chai.expect(data.str).to.equal numbers[data.num]
dataCounter++
even.on 'disconnect', ->
done() if dataCounter is 10 and grpCounter is 10
for i in [0...10]
num.beginGroup i
num.send i
num.endGroup i
num.disconnect()
str.beginGroup i
str.send numbers[i]
str.endGroup i
str.disconnect()
describe 'when there are parameter ports', ->
c = null
p1 = p2 = p3 = d1 = d2 = out = err = 0
beforeEach ->
c = new component.Component
c.inPorts.add 'param1',
datatype: 'string'
required: true
.add 'param2',
datatype: 'int'
required: false
.add 'param3',
datatype: 'int'
required: true
default: 0
.add 'data1',
datatype: 'string'
.add 'data2',
datatype: 'int'
c.outPorts.add 'out',
datatype: 'object'
.add 'error',
datatype: 'object'
p1 = new socket.createSocket()
p2 = new socket.createSocket()
p3 = new socket.createSocket()
d1 = new socket.createSocket()
d2 = new socket.createSocket()
out = new socket.createSocket()
err = new socket.createSocket()
c.inPorts.param1.attach p1
c.inPorts.param2.attach p2
c.inPorts.param3.attach p3
c.inPorts.data1.attach d1
c.inPorts.data2.attach d2
c.outPorts.out.attach out
c.outPorts.error.attach err
it 'should wait for required params without default value', (done) ->
helpers.WirePattern c,
in: ['data1', 'data2']
out: 'out'
params: ['param1', 'param2', 'param3']
, (input, groups, out) ->
res =
p1: c.params.param1
p2: c.params.param2
p3: c.params.param3
d1: input.data1
d2: input.data2
out.send res
out.once 'data', (data) ->
chai.expect(data).to.be.an 'object'
chai.expect(data.p1).to.equal 'req'
chai.expect(data.p2).to.be.undefined
chai.expect(data.p3).to.equal 0
chai.expect(data.d1).to.equal 'foo'
chai.expect(data.d2).to.equal 123
# And later when second param arrives
out.once 'data', (data) ->
chai.expect(data).to.be.an 'object'
chai.expect(data.p1).to.equal 'req'
chai.expect(data.p2).to.equal 568
chai.expect(data.p3).to.equal 800
chai.expect(data.d1).to.equal 'bar'
chai.expect(data.d2).to.equal 456
done()
d1.send 'foo'
d1.disconnect()
d2.send 123
d2.disconnect()
c.sendDefaults()
p1.send 'req'
p1.disconnect()
# the handler should be triggered here
setTimeout ->
p2.send 568
p2.disconnect()
p3.send 800
p3.disconnect()
d1.send 'bar'
d1.disconnect()
d2.send 456
d2.disconnect()
, 10
it 'should work for async procs too', (done) ->
helpers.WirePattern c,
in: ['data1', 'data2']
out: 'out'
params: ['param1', 'param2', 'param3']
, (input, groups, out) ->
delay = if c.params.param2 then c.params.param2 else 10
setTimeout ->
res =
p1: c.params.param1
p2: c.params.param2
p3: c.params.param3
d1: input.data1
d2: input.data2
out.send res
, delay
out.once 'data', (data) ->
chai.expect(data).to.be.an 'object'
chai.expect(data.p1).to.equal 'req'
chai.expect(data.p2).to.equal 56
chai.expect(data.p3).to.equal 0
chai.expect(data.d1).to.equal 'foo'
chai.expect(data.d2).to.equal 123
done()
p2.send 56
p2.disconnect()
d1.send 'foo'
d1.disconnect()
d2.send 123
d2.disconnect()
c.sendDefaults()
p1.send 'req'
p1.disconnect()
# the handler should be triggered here
it 'should reset state if shutdown() is called', (done) ->
helpers.WirePattern c,
in: ['data1', 'data2']
out: 'out'
params: ['param1', 'param2', 'param3']
, (input, groups, out) ->
out.send
p1: c.params.param1
p2: c.params.param2
p3: c.params.param3
d1: input.data1
d2: input.data2
d1.send 'boo'
d1.disconnect()
p2.send 73
p2.disconnect()
chai.expect(Object.keys(c.groupedData)).to.have.length.above 0
chai.expect(Object.keys(c.params)).to.have.length.above 0
c.shutdown()
chai.expect(c.groupedData).to.deep.equal {}
chai.expect(c.params).to.deep.equal {}
chai.expect(c.taskQ).to.deep.equal []
done()
it 'should drop premature data if configured to do so', (done) ->
helpers.WirePattern c,
in: ['data1', 'data2']
out: 'out'
params: ['param1', 'param2', 'param3']
dropInput: true
, (input, groups, out) ->
res =
p1: c.params.param1
p2: c.params.param2
p3: c.params.param3
d1: input.data1
d2: input.data2
out.send res
out.once 'data', (data) ->
chai.expect(data).to.be.an 'object'
chai.expect(data.p1).to.equal 'req'
chai.expect(data.p2).to.equal 568
chai.expect(data.p3).to.equal 800
chai.expect(data.d1).to.equal 'bar'
chai.expect(data.d2).to.equal 456
done()
c.sendDefaults()
p2.send 568
p2.disconnect()
p3.send 800
p3.disconnect()
d1.send 'foo'
d1.disconnect()
d2.send 123
d2.disconnect()
# Data is dropped at this point
setTimeout ->
p1.send 'req'
p1.disconnect()
d1.send 'bar'
d1.disconnect()
d2.send 456
d2.disconnect()
, 10
describe 'without output ports', ->
c = new component.Component
c.inPorts.add 'foo'
foo = socket.createSocket()
sig = socket.createSocket()
c.inPorts.foo.attach foo
helpers.WirePattern c,
in: 'foo'
out: []
async: true
, (foo, grp, out, callback) ->
setTimeout ->
sig.send foo
callback()
, 20
it 'should be fine still', (done) ->
sig.on 'data', (data) ->
chai.expect(data).to.equal 'foo'
done()
foo.send 'foo'
foo.disconnect()
describe 'when data processing is not possible at the moment', ->
c = new component.Component
c.inPorts.add 'line', datatype: 'string'
.add 'repeat', datatype: 'int'
.add 'when',
datatype: 'string'
default: 'later'
c.outPorts.add 'res', datatype: 'string'
.add 'error', datatype: 'object'
line = socket.createSocket()
rpt = socket.createSocket()
whn = socket.createSocket()
res = socket.createSocket()
err = socket.createSocket()
c.inPorts.line.attach line
c.inPorts.repeat.attach rpt
c.inPorts.when.attach whn
c.outPorts.res.attach res
c.outPorts.error.attach err
c.invCount = 0
tryAgain = null
helpers.WirePattern c,
in: ['line', 'repeat']
params: 'when'
out: 'res'
async: true
, (input, groups, out, completed, postpone, resume) ->
this.invCount++
return if this.invCount > 100 # avoid deadlocks just in case
switch this.params.when
when 'now'
repeated = ''
repeated += input.line for i in [0...input.repeat]
out.send repeated
completed()
when 'later'
postpone()
when 'afterTimeout'
postpone false
this.params.when = 'now' # don't recurse forever
setTimeout ->
resume()
, 10
when 'whenItell'
postpone false
this.params.when = 'now' # don't recurse forever
tryAgain = resume
it 'should be able to postpone it until next tuple of data', (done) ->
res.once 'data', (data) ->
chai.expect(data).to.equal 'opopopopopopopopopop'
chai.expect(c.invCount).to.equal 2
res.once 'data', (data) ->
chai.expect(data).to.equal 'gogogo'
chai.expect(c.invCount).to.equal 3
done()
c.sendDefaults()
line.send 'op'
rpt.send 10
line.disconnect()
rpt.disconnect()
# no output expected at this point
whn.send 'now'
whn.disconnect()
line.send 'go'
rpt.send 3
line.disconnect()
rpt.disconnect()
# this flushes the earlier stuff
it 'should be able to postpone and retry after timeout', (done) ->
c.invCount = 0
res.once 'data', (data) ->
chai.expect(data).to.equal 'dododo'
chai.expect(c.invCount).to.equal 2
done()
whn.send 'afterTimeout'
whn.disconnect()
line.send 'do'
rpt.send 3
line.disconnect()
rpt.disconnect()
it 'should be able to postpone it and resume when needed', (done) ->
c.invCount = 0
res.once 'data', (data) ->
chai.expect(data).to.equal 'yoyo'
chai.expect(c.invCount).to.equal 2
done()
whn.send 'whenItell'
whn.disconnect()
line.send 'yo'
rpt.send 2
line.disconnect()
rpt.disconnect()
# Here tryAgain got the resume callback
setTimeout ->
tryAgain()
, 30
describe 'with many inputs and groups', ->
c = new component.Component
c.token = null
c.inPorts.add 'in', datatype: 'string'
.add 'message', datatype: 'string'
.add 'repository', datatype: 'string'
.add 'path', datatype: 'string'
.add 'token', datatype: 'string', (event, payload) ->
c.token = payload if event is 'data'
c.outPorts.add 'out', datatype: 'string'
.add 'error', datatype: 'object'
helpers.WirePattern c,
in: ['in', 'message', 'repository', 'path']
out: 'out'
async: true
forwardGroups: true
, (data, groups, out, callback) ->
setTimeout ->
out.beginGroup data.path
out.send data.message
out.endGroup()
do callback
, 300
ins = socket.createSocket()
msg = socket.createSocket()
rep = socket.createSocket()
pth = socket.createSocket()
tkn = socket.createSocket()
out = socket.createSocket()
err = socket.createSocket()
c.inPorts.in.attach ins
c.inPorts.message.attach msg
c.inPorts.repository.attach rep
c.inPorts.path.attach pth
c.inPorts.token.attach tkn
c.outPorts.out.attach out
c.outPorts.error.attach err
it 'should handle mixed flow well', (done) ->
groups = []
refGroups = [
'foo'
'http://techcrunch.com/2013/03/26/embedly-now/'
'path data'
]
ends = 0
packets = []
refData = ['message data']
out.on 'begingroup', (grp) ->
groups.push grp
out.on 'endgroup', ->
ends++
out.on 'data', (data) ->
packets.push data
out.on 'disconnect', ->
chai.expect(groups).to.deep.equal refGroups
chai.expect(ends).to.equal 3
chai.expect(packets).to.deep.equal refData
done()
err.on 'data', (data) ->
done data
rep.beginGroup 'foo'
rep.beginGroup 'http://techcrunch.com/2013/03/26/embedly-now/'
rep.send 'repo data'
rep.endGroup()
rep.endGroup()
ins.beginGroup 'foo'
ins.beginGroup 'http://techcrunch.com/2013/03/26/embedly-now/'
ins.send 'ins data'
msg.beginGroup 'foo'
msg.beginGroup 'http://techcrunch.com/2013/03/26/embedly-now/'
msg.send 'message data'
msg.endGroup()
msg.endGroup()
ins.endGroup()
ins.endGroup()
ins.disconnect()
msg.disconnect()
pth.beginGroup 'foo'
pth.beginGroup 'http://techcrunch.com/2013/03/26/embedly-now/'
pth.send 'path data'
pth.endGroup()
pth.endGroup()
pth.disconnect()
rep.disconnect()
describe 'for batch processing', ->
# Component constructors
newGenerator = (name) ->
generator = new component.Component
generator.inPorts.add 'count', datatype: 'int'
generator.outPorts.add 'seq', datatype: 'int'
helpers.WirePattern generator,
in: 'count'
out: 'seq'
async: true
forwardGroups: true
ordered: true
, (count, groups, seq, callback) ->
sentCount = 0
for i in [1..count]
do (i) ->
delay = if i > 10 then i % 10 else i
setTimeout ->
seq.send i
sentCount++
if sentCount is count
callback()
, delay
newDoubler = (name) ->
doubler = new component.Component
doubler.inPorts.add 'num', datatype: 'int'
doubler.outPorts.add 'out', datatype: 'int'
helpers.WirePattern doubler,
in: 'num'
out: 'out'
forwardGroups: true
, (num, groups, out) ->
dbl = 2*num
out.send dbl
newAdder = ->
adder = new component.Component
adder.inPorts.add 'num1', datatype: 'int'
adder.inPorts.add 'num2', datatype: 'int'
adder.outPorts.add 'sum', datatype: 'int'
helpers.WirePattern adder,
in: ['num1', 'num2']
out: 'sum'
forwardGroups: true
async: true
ordered: true
, (args, groups, out, callback) ->
sum = args.num1 + args.num2
# out.send sum
setTimeout ->
out.send sum
callback()
, sum % 10
newSeqsum = ->
seqsum = new component.Component
seqsum.sum = 0
seqsum.inPorts.add 'seq', datatype: 'int', (event, payload) ->
switch event
when 'data'
seqsum.sum += payload
when 'disconnect'
seqsum.outPorts.sum.send seqsum.sum
seqsum.sum = 0
seqsum.outPorts.sum.disconnect()
seqsum.outPorts.add 'sum', datatype: 'int'
return seqsum
# Wires
genA = newGenerator 'A'
genB = newGenerator 'B'
dblA = newDoubler 'A'
dblB = newDoubler 'B'
addr = newAdder()
sumr = newSeqsum()
cntA = socket.createSocket()
cntB = socket.createSocket()
gen2dblA = socket.createSocket()
gen2dblB = socket.createSocket()
dblA2add = socket.createSocket()
dblB2add = socket.createSocket()
addr2sum = socket.createSocket()
sum = socket.createSocket()
genA.inPorts.count.attach cntA
genB.inPorts.count.attach cntB
genA.outPorts.seq.attach gen2dblA
genB.outPorts.seq.attach gen2dblB
dblA.inPorts.num.attach gen2dblA
dblB.inPorts.num.attach gen2dblB
dblA.outPorts.out.attach dblA2add
dblB.outPorts.out.attach dblB2add
addr.inPorts.num1.attach dblA2add
addr.inPorts.num2.attach dblB2add
addr.outPorts.sum.attach addr2sum
sumr.inPorts.seq.attach addr2sum
sumr.outPorts.sum.attach sum
it 'should process sequences of packets separated by disconnects', (done) ->
expected = [ 24, 40 ]
actual = []
sum.on 'data', (data) ->
actual.push data
sum.on 'disconnect', ->
chai.expect(actual).to.have.length.above 0
chai.expect(expected).to.have.length.above 0
act = actual.shift()
exp = expected.shift()
chai.expect(act).to.equal exp
done() if expected.length is 0
cntA.send 3
cntA.disconnect()
cntB.send 3
cntB.disconnect()
cntA.send 4
cntB.send 4
cntA.disconnect()
cntB.disconnect()
describe 'for batch processing with groups', ->
c1 = new component.Component
c1.inPorts.add 'count', datatype: 'int'
c1.outPorts.add 'seq', datatype: 'int'
c2 = new component.Component
c2.inPorts.add 'num', datatype: 'int'
c2.outPorts.add 'out', datatype: 'int'
cnt = socket.createSocket()
c1c2 = socket.createSocket()
out = socket.createSocket()
c1.inPorts.count.attach cnt
c1.outPorts.seq.attach c1c2
c2.inPorts.num.attach c1c2
c2.outPorts.out.attach out
it 'should wrap entire sequence with groups', (done) ->
helpers.WirePattern c1,
in: 'count'
out: 'seq'
async: true
forwardGroups: true
, (count, groups, out, callback) ->
for i in [0...count]
do (i) ->
setTimeout ->
out.send i
, 0
setTimeout ->
callback()
, 3
helpers.WirePattern c2,
in: 'num'
out: 'out'
forwardGroups: true
, (num, groups, out) ->
chai.expect(groups).to.deep.equal ['foo', 'bar']
out.send num
expected = ['<foo>', '<bar>', 0, 1, 2, '</bar>', '</foo>']
actual = []
out.on 'begingroup', (grp) ->
actual.push "<#{grp}>"
out.on 'endgroup', (grp) ->
actual.push "</#{grp}>"
out.on 'data', (data) ->
actual.push data
out.on 'disconnect', ->
chai.expect(actual).to.deep.equal expected
done()
cnt.beginGroup 'foo'
cnt.beginGroup 'bar'
cnt.send 3
cnt.endGroup()
cnt.endGroup()
cnt.disconnect()
describe 'with addressable ports', ->
c = new component.Component
c.inPorts.add 'p1',
datatype: 'int'
addressable: true
required: true
.add 'd1',
datatype: 'int'
addressable: true
.add 'd2',
datatype: 'string'
c.outPorts.add 'out',
datatype: 'object'
.add 'error',
datatype: 'object'
p11 = socket.createSocket()
p12 = socket.createSocket()
p13 = socket.createSocket()
d11 = socket.createSocket()
d12 = socket.createSocket()
d13 = socket.createSocket()
d2 = socket.createSocket()
out = socket.createSocket()
err = socket.createSocket()
c.inPorts.p1.attach p11
c.inPorts.p1.attach p12
c.inPorts.p1.attach p13
c.inPorts.d1.attach d11
c.inPorts.d1.attach d12
c.inPorts.d1.attach d13
c.inPorts.d2.attach d2
c.outPorts.out.attach out
c.outPorts.error.attach err
it 'should wait for all param and any data port values (default)', (done) ->
helpers.WirePattern c,
in: ['d1', 'd2']
params: 'p1'
out: 'out'
arrayPolicy: # default values
in: 'any'
params: 'all'
, (input, groups, out) ->
chai.expect(c.params.p1).to.deep.equal { 0: 1, 1: 2, 2: 3 }
chai.expect(input.d1).to.deep.equal {0: 1}
chai.expect(input.d2).to.equal 'foo'
done()
d2.send 'foo'
d2.disconnect()
d11.send 1
d11.disconnect()
p11.send 1
p11.disconnect()
p12.send 2
p12.disconnect()
p13.send 3
p13.disconnect()
it 'should wait for any param and all data values', (done) ->
helpers.WirePattern c,
in: ['d1', 'd2']
params: 'p1'
out: 'out'
arrayPolicy: # inversed
in: 'all'
params: 'any'
, (input, groups, out) ->
chai.expect(c.params.p1).to.deep.equal {0: 1}
chai.expect(input.d1).to.deep.equal { 0: 1, 1: 2, 2: 3 }
chai.expect(input.d2).to.equal 'foo'
done()
out.on 'disconnect', ->
console.log 'disc'
d2.send 'foo'
d2.disconnect()
p11.send 1
p11.disconnect()
d11.send 1
d11.disconnect()
d12.send 2
d12.disconnect()
d13.send 3
d13.disconnect()
p12.send 2
p12.disconnect()
p13.send 3
p13.disconnect()
it 'should wait for all indexes of a single input', (done) ->
helpers.WirePattern c,
in: 'd1'
out: 'out'
arrayPolicy:
in: 'all'
, (input, groups, out) ->
chai.expect(input).to.deep.equal { 0: 1, 1: 2, 2: 3 }
done()
d11.send 1
d11.disconnect()
d12.send 2
d12.disconnect()
d13.send 3
d13.disconnect()
it 'should behave normally with string output from another component', (done) ->
c = new component.Component
c.inPorts.add 'd1',
datatype: 'string'
addressable: true
c.outPorts.add 'out',
datatype: 'object'
d11 = socket.createSocket()
d12 = socket.createSocket()
d13 = socket.createSocket()
out = socket.createSocket()
c.inPorts.d1.attach d11
c.inPorts.d1.attach d12
c.inPorts.d1.attach d13
c.outPorts.out.attach out
c2 = new component.Component
c2.inPorts.add 'in', datatype: 'string'
c2.outPorts.add 'out', datatype: 'string'
helpers.WirePattern c2,
in: 'in'
out: 'out'
forwardGroups: true
, (input, groups, out) ->
out.send input
d3 = socket.createSocket()
c2.inPorts.in.attach d3
c2.outPorts.out.attach d11
helpers.WirePattern c,
in: 'd1'
out: 'out'
, (input, groups, out) ->
chai.expect(input).to.deep.equal {0: 'My string'}
done()
d3.send 'My string'
d3.disconnect()
describe 'when grouping requests', ->
c = new component.Component
c.inPorts.add 'x', datatype: 'int'
.add 'y', datatype: 'int'
c.outPorts.add 'out', datatype: 'object'
x = socket.createSocket()
y = socket.createSocket()
out = socket.createSocket()
c.inPorts.x.attach x
c.inPorts.y.attach y
c.outPorts.out.attach out
getUuid = ->
'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace /[xy]/g, (c) ->
r = Math.random()*16|0
v = if c is 'x' then r else r&0x3|0x8
v.toString 16
isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
generateRequests = (num) ->
reqs = {}
for i in [1..num]
req =
id: getUuid()
num: i
if i % 3 is 0
req.x = i
else if i % 7 is 0
req.y = i
else
req.x = i
req.y = 2*i
reqs[req.id] = req
reqs
sendRequests = (reqs, delay) ->
for id, req of reqs
do (req) ->
setTimeout ->
if 'x' of req
x.beginGroup req.id
x.beginGroup 'x'
x.beginGroup req.num
x.send req.x
x.endGroup()
x.endGroup()
x.endGroup()
x.disconnect()
if 'y' of req
y.beginGroup req.id
y.beginGroup 'y'
y.beginGroup req.num
y.send req.y
y.endGroup()
y.endGroup()
y.endGroup()
y.disconnect()
, delay*req.num
helpers.WirePattern c,
in: ['x', 'y']
out: 'out'
async: true
forwardGroups: true
group: isUuid
gcFrequency: 2 # every 2 requests
gcTimeout: 0.02 # older than 20ms
, (input, groups, out, done) ->
setTimeout ->
out.send
id: groups[0]
x: input.x
y: input.y
done()
, 3
it 'should group requests by outer UUID group', (done) ->
reqs = generateRequests 10
count = 0
out.on 'data', (data) ->
count++
chai.expect(data.x).to.equal reqs[data.id].x
chai.expect(data.y).to.equal reqs[data.id].y
done() if count is 6 # 6 complete requests processed
sendRequests reqs, 10
it 'should collect garbage every N requests', (done) ->
# GC dropped 3 timed out packets, 1 should be left
chai.expect(Object.keys(c.groupedData)).to.have.lengthOf 1
chai.expect(Object.keys(c.groupedGroups)).to.have.lengthOf 1
chai.expect(Object.keys(c.disconnectData)).to.have.lengthOf 1
done()
it 'should be able to drop a request explicitly', (done) ->
for key in Object.keys(c.groupedData)
c.dropRequest key
chai.expect(c.groupedData).to.deep.equal {}
chai.expect(c.groupedGroups).to.deep.equal {}
chai.expect(c.disconnectData).to.deep.equal {}
done()
describe 'MultiError', ->
describe 'with simple sync processes', ->
c = new component.Component
c.inPorts.add 'form', datatype: 'object', (event, payload) ->
return unless event is 'data'
# Validate form
unless payload.name and payload.name.match /^\w{3,16}$/
c.error helpers.CustomError 'Incorrect name',
kind: 'form_error'
code: 'invalid_name'
param: 'name'
unless payload.email and payload.email.match /^\w+@\w+\.\w+$/
c.error helpers.CustomError 'Incorrect email',
kind: 'form_error'
code: 'invalid_email'
param: 'email'
unless payload.accept
c.error helpers.CustomError 'Terms have to be accepted',
kind: 'form_error'
code: 'terms_not_accepted'
param: 'accept'
# Finish validation
return c.fail() if c.hasErrors
# Emulating some processing logic here
if payload.name is 'DelayLama'
# oops
c.outPorts.saved.send false
c.outPorts.saved.disconnect()
return c.fail helpers.CustomError 'Suspended for a meditation',
kind: 'runtime_error'
code: 'delay_lama_detected'
else
c.outPorts.saved.send true
c.outPorts.saved.disconnect()
c.outPorts.add 'saved', datatype: 'boolean'
c.outPorts.add 'error', datatype: 'object'
form = new socket.createSocket()
saved = new socket.createSocket()
err = new socket.createSocket()
c.inPorts.form.attach form
c.outPorts.saved.attach saved
c.outPorts.error.attach err
helpers.MultiError c
it 'should support multiple customized error messages', (done) ->
errCount = 0
err.on 'data', (data) ->
chai.expect(data instanceof Error).to.be.true
chai.expect(data.kind).to.equal 'form_error'
errCount++
err.on 'disconnect', ->
chai.expect(errCount).to.equal 3
done()
form.send
name: 'Bo'
email: 'missing'
form.disconnect()
it 'should pass if everything is correct', (done) ->
hadData = false
saved.removeAllListeners()
saved.once 'data', (data) ->
chai.expect(data).to.be.true
hadData = true
saved.once 'disconnect', ->
chai.expect(hadData).to.be.true
done()
err.removeAllListeners()
err.on 'data', (data) ->
done data
form.send
name: 'Josh'
email: 'josh@example.com'
accept: true
form.disconnect()
it 'should handle fatals and runtimes normally', (done) ->
saved.once 'data', (data) ->
chai.expect(data).to.be.false
err.removeAllListeners()
errCount = 0
err.once 'data', (data) ->
chai.expect(data instanceof Error).to.be.true
chai.expect(data.kind).to.equal 'runtime_error'
errCount++
err.once 'disconnect', ->
chai.expect(errCount).to.equal 1
done()
form.send
name: 'DelayLama'
email: 'delay@lama.ti'
accept: true
form.disconnect()
describe 'with async processes and groups', ->
c = new component.Component
c.inPorts.add 'form', datatype: 'object'
c.outPorts.add 'saved', datatype: 'boolean'
c.outPorts.add 'error', datatype: 'object'
form = new socket.createSocket()
saved = new socket.createSocket()
err = new socket.createSocket()
c.inPorts.form.attach form
c.outPorts.saved.attach saved
c.outPorts.error.attach err
helpers.WirePattern c,
in: 'form'
out: 'saved'
async: true
forwardGroups: true
name: 'Registration'
, (payload, groups, out, callback) ->
# Validate form
unless payload.name and payload.name.match /^\w{3,16}$/
this.error helpers.CustomError('Incorrect name',
kind: 'form_error'
code: 'invalid_name'
param: 'name'
), ['e1']
unless payload.email and payload.email.match /^\w+@\w+\.\w+$/
this.error helpers.CustomError('Incorrect email',
kind: 'form_error'
code: 'invalid_email'
param: 'email'
), ['e2']
unless payload.accept
this.error helpers.CustomError('Terms have to be accepted',
kind: 'form_error'
code: 'terms_not_accepted'
param: 'accept'
), ['e3']
# Finish validation
return callback no if this.hasErrors
setT