├── .gitignore ├── .jshintrc ├── .travis.yml ├── README.md ├── examples ├── async.js ├── buffered.js ├── close.js ├── errors.js ├── interval.js ├── passing.js ├── requests.js ├── select.js ├── stream.js └── timeout.js ├── index.js ├── lib ├── async.js ├── channel.js ├── interval.js ├── make.js ├── receiver.js ├── select.js └── timeout.js ├── package.json └── test ├── async.js ├── buffered.js ├── chan.js ├── close.js ├── interval.js ├── select.js └── timeout.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | .DS_Store 3 | node_modules 4 | examples/node_modules 5 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "forin": true, 5 | "freeze": true, 6 | "indent": 2, 7 | "latedef": "nofunc", 8 | "noarg": true, 9 | "noempty": true, 10 | "quotmark": "single", 11 | "undef": true, 12 | "strict": false, 13 | "trailing": true, 14 | "maxparams": 5, 15 | "maxdepth": 5, 16 | "maxstatements": 15, 17 | "maxcomplexity": 7, 18 | "maxlen": 80, 19 | "asi": true, 20 | "boss": true, 21 | "eqnull": true, 22 | "browser": true, 23 | "node": true 24 | } 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.11" 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chan 2 | 3 | A [golang](http://golang.org) like channel implementation for JavaScript that 4 | works well with [co](https://github.com/visionmedia/co). 5 | 6 | [![Build Status](https://travis-ci.org/brentburg/chan.png)](https://travis-ci.org/brentburg/chan) 7 | [![Code Climate](https://codeclimate.com/github/brentburgoyne/chan.png)](https://codeclimate.com/github/brentburgoyne/chan) 8 | [![Dependency Status](https://gemnasium.com/brentburgoyne/chan.png)](https://gemnasium.com/brentburgoyne/chan) 9 | 10 | ## Features 11 | 12 | - CSP Style channels in JavaScript 13 | - Buffered or Unbuffered channels 14 | - Channels can be closed 15 | - API designed to work well with generators and co 16 | - Can be used without generators 17 | - Channels can be selected similar to Go's select statement 18 | 19 | ## Installation 20 | 21 | ```bash 22 | $ npm install chan --save 23 | ``` 24 | 25 | ## The Basics 26 | 27 | Chan is inspired by golang's channels. It is implemented as a function that 28 | represents an asynchronous first in first out queue. 29 | 30 | ```js 31 | var makeChan = require('chan') 32 | // make a new unbuffered channel 33 | var ch = makeChan() 34 | typeof ch // -> 'function' 35 | ``` 36 | 37 | ### Sending values to the channel 38 | 39 | Values are added to the 40 | channel by calling the function with either `(value)` or `(error, value)`. The 41 | return value is a thunk (a function that take a node-style callback as its only 42 | argument). The callback given to the thunk is called once the value is added. 43 | 44 | ```js 45 | ch('foo')(function (err) { 46 | if (err) { 47 | // There was an error putting the value on the channel 48 | } else { 49 | // The value was successfully put on the channel 50 | } 51 | }) 52 | ``` 53 | 54 | ### Receiving values from the channel 55 | 56 | Values are removed from the channel by calling it with a node-style callback as 57 | this first argument. When a value is available on the channel the callback is 58 | called with the value or error. In this case the channel itself can also be a 59 | thunk. 60 | 61 | ```js 62 | ch(function (err, val) { 63 | // called when there is a value or error on the channel 64 | }) 65 | ``` 66 | 67 | ### Generators 68 | 69 | Because thunks are yield-able in a co generator, chan works very well when 70 | combined with co. Using them together makes chan feel very similar to go 71 | channels. 72 | 73 | ```js 74 | var co = require('co') 75 | 76 | co(function *() { 77 | var val = yield ch 78 | }) 79 | 80 | co(function *() { 81 | yield ch('foo') 82 | }) 83 | ``` 84 | 85 | ## Buffer 86 | 87 | Docs coming soon... 88 | 89 | ## Close 90 | 91 | Docs coming soon... 92 | 93 | ## Select 94 | 95 | Docs coming soon... 96 | -------------------------------------------------------------------------------- /examples/async.js: -------------------------------------------------------------------------------- 1 | // jshint esnext:true 2 | 3 | var request = require('superagent') 4 | var chan = require('..') 5 | var co = require('co') 6 | 7 | var urls = [ 8 | 'http://google.com', 9 | 'http://medium.com', 10 | 'http://segment.io', 11 | 'http://cloudup.com', 12 | 'http://github.com' 13 | ] 14 | 15 | var ch = chan(3) // buffer size 3 16 | 17 | co(function *() { 18 | for (var i = 0, l = urls.length; i < l; i++) { 19 | yield ch.async(request.get, urls[i]) 20 | console.log('Response added to channel for: ' + urls[i]) 21 | } 22 | ch.close() 23 | })() 24 | 25 | co(function *() { 26 | while (!ch.done()) { 27 | yield ch 28 | yield chan.timeout(1000) 29 | console.log('Channel yielded') 30 | } 31 | })() 32 | -------------------------------------------------------------------------------- /examples/buffered.js: -------------------------------------------------------------------------------- 1 | // jshint esnext:true 2 | 3 | var chan = require('..') 4 | var co = require('co') 5 | var ch = chan(5) 6 | 7 | co(function *() { 8 | var n 9 | while (!ch.done()) { 10 | yield chan.timeout(100) 11 | console.log('<-- ' + (yield ch)) 12 | } 13 | })() 14 | 15 | 16 | co(function *() { 17 | var n = 10 18 | while (n-- > 0) { 19 | yield ch(n) 20 | console.log(n + ' -->') 21 | } 22 | ch.close() 23 | })() 24 | -------------------------------------------------------------------------------- /examples/close.js: -------------------------------------------------------------------------------- 1 | // jshint esnext:true 2 | 3 | var chan = require('..') 4 | var co = require('co') 5 | var ch = chan() 6 | 7 | co(function *() { 8 | var val 9 | while (!ch.done()) { 10 | val = yield ch 11 | if (val !== ch.empty) { 12 | console.log(val) 13 | } 14 | } 15 | console.log('Done!') 16 | })() 17 | 18 | co(function *() { 19 | var n = 10 20 | 21 | while (n-- > 0) { 22 | yield chan.timeout(100) 23 | try { 24 | ch(n) 25 | } catch(err) { 26 | console.log(err.message) 27 | } 28 | 29 | if (n === 5) { 30 | ch.close() 31 | } 32 | } 33 | })() 34 | -------------------------------------------------------------------------------- /examples/errors.js: -------------------------------------------------------------------------------- 1 | // jshint esnext:true 2 | 3 | var chan = require('..') 4 | var co = require('co') 5 | var fs = require('fs') 6 | 7 | co(function *() { 8 | var ch = chan() 9 | 10 | fs.readFile('something', ch) 11 | 12 | try { 13 | yield ch 14 | } catch (err) { 15 | console.log('failed: %s', err.message) 16 | } 17 | })() 18 | -------------------------------------------------------------------------------- /examples/interval.js: -------------------------------------------------------------------------------- 1 | // jshint esnext:true 2 | 3 | var chan = require('..') 4 | var co = require('co') 5 | 6 | co(function *() { 7 | var int = chan.interval(40) 8 | while (true) { 9 | console.log('a: ' + (yield int)) 10 | } 11 | })() 12 | 13 | co(function *() { 14 | var int = chan.interval(30) 15 | while (true) { 16 | console.log('b: ' + (yield int)) 17 | } 18 | })() 19 | -------------------------------------------------------------------------------- /examples/passing.js: -------------------------------------------------------------------------------- 1 | // jshint esnext:true 2 | 3 | var chan = require('..') 4 | var co = require('co') 5 | var ch = chan() 6 | 7 | co(function *() { 8 | var n 9 | 10 | while ((n = yield ch)) { 11 | console.log(n) 12 | } 13 | })() 14 | 15 | co(function *() { 16 | var n = 50 17 | 18 | while (n-- > 0) { 19 | yield chan.timeout(100) 20 | ch(n) 21 | } 22 | })() 23 | -------------------------------------------------------------------------------- /examples/requests.js: -------------------------------------------------------------------------------- 1 | // jshint esnext:true 2 | 3 | var request = require('superagent') 4 | var chan = require('..') 5 | var co = require('co') 6 | 7 | var urls = [ 8 | 'http://google.com', 9 | 'http://medium.com', 10 | 'http://segment.io', 11 | 'http://cloudup.com' 12 | ] 13 | 14 | co(function *() { 15 | var ch = chan() 16 | var res 17 | 18 | urls.forEach(function (url) { 19 | request.get(url, ch) 20 | }) 21 | 22 | while ((res = yield ch)) { 23 | console.log(res.status) 24 | } 25 | })() 26 | -------------------------------------------------------------------------------- /examples/select.js: -------------------------------------------------------------------------------- 1 | // jshint esnext: true, loopfunc: true 2 | 3 | var chan = require('..') 4 | var co = require('co') 5 | 6 | co(function *() { 7 | var count = 10 8 | , ch1 9 | , ch2 10 | 11 | while (count-- > 0) { 12 | // macke new channels 13 | ch1 = chan() 14 | ch2 = chan() 15 | 16 | // add a value on each channel after a random amout of time 17 | setTimeout(function () { ch1('ch1') }, Math.random() * 100 | 0) 18 | setTimeout(function () { ch2('ch2') }, Math.random() * 100 | 0) 19 | 20 | // will block until there is data on either ch1 or ch2, 21 | // and will return the channel with data 22 | // if data is on both channels, a channel will be selected at random 23 | switch (yield chan.select(ch1, ch2)) { 24 | 25 | // channel 1 received data 26 | case ch1: 27 | // retrieve the message from the channel 28 | console.log(yield ch1.selected) 29 | break 30 | 31 | // channel 2 received data 32 | case ch2: 33 | // retrieve the message from the channel 34 | console.log(yield ch2.selected) 35 | break 36 | 37 | } 38 | } 39 | 40 | })() 41 | -------------------------------------------------------------------------------- /examples/stream.js: -------------------------------------------------------------------------------- 1 | // jshint esnext:true 2 | 3 | var fs = require('fs') 4 | var chan = require('..') 5 | var co = require('co') 6 | var split = require('split') 7 | 8 | co(function *() { 9 | var ch = chan() 10 | 11 | fs.createReadStream(__dirname + '/../README.md') 12 | .pipe(split()) 13 | .on('data', ch) 14 | .on('error', ch) 15 | .on('end', ch.close) 16 | 17 | while (!ch.done()) { 18 | var val = yield ch 19 | if (val !== ch.empty) { 20 | console.log('Stream yielded: ' + String(yield ch)) 21 | } 22 | } 23 | 24 | console.log('Stream ended') 25 | })() 26 | -------------------------------------------------------------------------------- /examples/timeout.js: -------------------------------------------------------------------------------- 1 | // jshint esnext:true 2 | 3 | var request = require('superagent') 4 | var chan = require('..') 5 | var co = require('co') 6 | 7 | co(function *() { 8 | var ch = chan() 9 | request.get('http://google.com', ch) 10 | 11 | switch (yield chan.select(ch, chan.timeout(1000))) { 12 | case ch: 13 | console.log('Google loaded.') 14 | break 15 | default: 16 | console.log('Timeout of 1 second reached.') 17 | } 18 | })() 19 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | var make = require('./lib/make') 5 | var select = require('./lib/select') 6 | var timeout = require('./lib/timeout') 7 | var interval = require('./lib/interval') 8 | 9 | /** 10 | * Expose `make`. 11 | */ 12 | module.exports = make 13 | 14 | /** 15 | * Expose `select`. 16 | */ 17 | module.exports.select = select 18 | 19 | /** 20 | * Expose `interval`. 21 | */ 22 | module.exports.interval = interval 23 | 24 | /** 25 | * Expose `timeout`. 26 | */ 27 | module.exports.timeout = timeout 28 | -------------------------------------------------------------------------------- /lib/async.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | var Receiver = require('./receiver') 5 | 6 | /** 7 | * Expose `async`. 8 | */ 9 | module.exports = async 10 | 11 | /** 12 | * Add value to channel via node-style async function. 13 | * 14 | * @param {Function} channel 15 | * @param {Function|Object} fn async function or object with async method 16 | * @param {String} method name only if fn is an object 17 | * @param {mixed} args async function arguments without callback 18 | * @return {Function} thunk 19 | */ 20 | function async(ch, fn/*, args...*/) { 21 | var args = [].slice.call(arguments, 2) 22 | var receiver = new Receiver() 23 | var context = null 24 | 25 | if (typeof fn === 'object') { 26 | context = fn 27 | fn = fn[args.shift()] 28 | } 29 | 30 | args.push(function (err, val) { 31 | if (arguments.length > 2) { 32 | val = [].slice.call(arguments, 1) 33 | } 34 | ch(err, val)(function (err) { 35 | receiver[err ? 'error' : 'add'](err) 36 | }) 37 | }) 38 | 39 | fn.apply(context, args) 40 | 41 | return function (cb) { 42 | receiver.callback(cb) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/channel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | var Receiver = require('./receiver') 5 | 6 | /** 7 | * Expose `Channel`. 8 | */ 9 | module.exports = Channel 10 | 11 | /** 12 | * Constants. 13 | */ 14 | var CLOSED_ERROR_MSG = 'Cannot add to closed channel' 15 | 16 | /** 17 | * Initialize a `Channel`. 18 | * 19 | * @param {Function|Object} [empty=Object] 20 | * @api private 21 | */ 22 | function Channel(bufferSize) { 23 | this.pendingAdds = [] 24 | this.pendingGets = [] 25 | this.items = [] 26 | this.bufferSize = parseInt(bufferSize, 10) || 0 27 | this.isClosed = false 28 | this.isDone = false 29 | this.empty = {} 30 | } 31 | 32 | /** 33 | * Static reference to the most recently called callback 34 | */ 35 | Channel.lastCalled = null 36 | 37 | /** 38 | * Get an item with `cb`. 39 | * 40 | * @param {Function} cb 41 | * @api private 42 | */ 43 | Channel.prototype.get = function (cb){ 44 | if (this.done()) { 45 | this.callEmpty(cb) 46 | } else if (this.items.length > 0 || this.pendingAdds.length > 0) { 47 | this.call(cb, this.nextItem()) 48 | } else { 49 | this.pendingGets.push(cb) 50 | } 51 | } 52 | 53 | /** 54 | * Remove `cb` from the queue. 55 | * 56 | * @param {Function} cb 57 | * @api private 58 | */ 59 | Channel.prototype.removeGet = function (cb) { 60 | var idx = this.pendingGets.indexOf(cb) 61 | if (idx > -1) { 62 | this.pendingGets.splice(idx, 1) 63 | } 64 | } 65 | 66 | /** 67 | * Get the next item and pull from pendingAdds to fill the buffer. 68 | * 69 | * @return {Mixed} 70 | * @api private 71 | */ 72 | Channel.prototype.nextItem = function () { 73 | if (this.pendingAdds.length > 0) { 74 | this.items.push(this.pendingAdds.shift().add()) 75 | } 76 | return this.items.shift() 77 | } 78 | 79 | /** 80 | * Add `val` to the channel. 81 | * 82 | * @param {Mixed} val 83 | * @return {Function} thunk 84 | * @api private 85 | */ 86 | Channel.prototype.add = function (val){ 87 | var receiver = new Receiver(val) 88 | 89 | if (this.isClosed) { 90 | receiver.error(Error(CLOSED_ERROR_MSG)) 91 | } else if (this.pendingGets.length > 0) { 92 | this.call(this.pendingGets.shift(), receiver.add()) 93 | } else if (this.items.length < this.bufferSize) { 94 | this.items.push(receiver.add()) 95 | } else { 96 | this.pendingAdds.push(receiver) 97 | } 98 | 99 | return function (cb) { 100 | receiver.callback(cb) 101 | } 102 | } 103 | 104 | /** 105 | * Invoke `cb` with `val` facilitate both 106 | * `chan(value)` and the `chan(error, value)` 107 | * use-cases. 108 | * 109 | * @param {Function} cb 110 | * @param {Mixed} val 111 | * @api private 112 | */ 113 | Channel.prototype.call = function (cb, val) { 114 | Channel.lastCalled = this.func 115 | if (val instanceof Error) { 116 | cb(val) 117 | } else { 118 | cb(null, val) 119 | } 120 | this.done() 121 | } 122 | 123 | /** 124 | * Invoke `cb` callback with the empty value. 125 | * 126 | * @param {Function} cb 127 | * @api private 128 | */ 129 | Channel.prototype.callEmpty = function (cb) { 130 | this.call(cb, this.empty) 131 | } 132 | 133 | /** 134 | * Prevennt future values from being added to 135 | * the channel. 136 | * 137 | * @return {Boolean} 138 | * @api public 139 | */ 140 | Channel.prototype.close = function () { 141 | this.isClosed = true 142 | var receiver 143 | while (receiver = this.pendingAdds.shift()) { 144 | receiver.error(Error(CLOSED_ERROR_MSG)) 145 | } 146 | return this.done() 147 | } 148 | 149 | /** 150 | * Check to see if the channel is done and 151 | * call pending callbacks if necessary. 152 | * 153 | * @return {Boolean} 154 | * @api private 155 | */ 156 | Channel.prototype.done = function () { 157 | if (!this.isDone && this.isClosed && this.items.length === 0) { 158 | this.isDone = true 159 | // call each pending callback with the empty value 160 | this.pendingGets.forEach(function (cb) { this.callEmpty(cb) }, this) 161 | } 162 | return this.isDone 163 | } 164 | -------------------------------------------------------------------------------- /lib/interval.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | var make = require('./make') 5 | 6 | /** 7 | * Expose `intervalChan`. 8 | */ 9 | module.exports = intervalChan 10 | 11 | /** 12 | * Make a interval channel that receives a count every number of milliseconds. 13 | * 14 | * @param {Number} ms 15 | * @returns {Function} channel 16 | * @api public 17 | */ 18 | function intervalChan(ms) { 19 | var ch = make() 20 | var count = 0; 21 | 22 | var int = setInterval(function () { 23 | try { 24 | ch(++count) 25 | } catch (err) { 26 | clearInterval(int) 27 | } 28 | }, ms) 29 | 30 | return ch 31 | } 32 | -------------------------------------------------------------------------------- /lib/make.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | var Channel = require('./channel') 5 | var async = require('./async') 6 | 7 | /** 8 | * Expose `make`. 9 | */ 10 | module.exports = make 11 | 12 | /** 13 | * Make a channel. 14 | * 15 | * @param {Number} bufferSize optional default=0 16 | * @return {Function} 17 | * @api public 18 | */ 19 | function make(bufferSize) { 20 | var chan = new Channel(bufferSize) 21 | 22 | var func = function (a, b) { 23 | // yielded 24 | if (typeof a === 'function') { 25 | return chan.get(a) 26 | } 27 | 28 | // (err, res) 29 | if (a === null && typeof b !== 'undefined') { 30 | a = b 31 | } 32 | 33 | // value 34 | return chan.add(a) 35 | } 36 | 37 | // expose public channel methods 38 | func.close = chan.close.bind(chan) 39 | func.done = chan.done.bind(chan) 40 | 41 | // bind async helper 42 | func.async = async.bind(null, func) 43 | 44 | // expose empty value 45 | func.empty = chan.empty 46 | 47 | // cross reference the channel object and function for internal use 48 | func.__chan = chan 49 | chan.func = func 50 | 51 | return func 52 | } 53 | -------------------------------------------------------------------------------- /lib/receiver.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Expose `Receiver`. 3 | */ 4 | module.exports = Receiver 5 | 6 | /** 7 | * Initialize a `Receiver`. 8 | * 9 | * @param {Mixed} val 10 | * @api private 11 | */ 12 | function Receiver(val) { 13 | this.val = val 14 | this.isAdded = false 15 | this.err = null 16 | this.cb = null 17 | this.isDone = false 18 | } 19 | 20 | /** 21 | * Call the callback if the pending add is complete. 22 | * 23 | * @api private 24 | */ 25 | Receiver.prototype.attemptNotify = function () { 26 | if ((this.isAdded || this.err) && this.cb && !this.isDone) { 27 | this.isDone = true 28 | setImmediate(function () { this.cb(this.err) }.bind(this)) 29 | } 30 | } 31 | 32 | /** 33 | * Reject the pending add with an error. 34 | * 35 | * @param {Error} err 36 | * @api private 37 | */ 38 | Receiver.prototype.error = function (err) { 39 | this.err = err 40 | this.attemptNotify() 41 | } 42 | 43 | /** 44 | * Get the `val` and set the state of the value to added 45 | * 46 | * @return {Mixed} val 47 | * @api private 48 | */ 49 | Receiver.prototype.add = function () { 50 | this.isAdded = true 51 | this.attemptNotify() 52 | return this.val 53 | } 54 | 55 | /** 56 | * Register the callback. 57 | * 58 | * @api private 59 | */ 60 | Receiver.prototype.callback = function (cb) { 61 | this.cb = cb 62 | this.attemptNotify() 63 | } 64 | -------------------------------------------------------------------------------- /lib/select.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | var make = require('./make') 5 | var Channel = require('./channel') 6 | 7 | /** 8 | * Expose `select`. 9 | */ 10 | module.exports = select 11 | 12 | /** 13 | * Return the first of the given channels with a value. 14 | * 15 | * @param {Function} channels... 16 | * @return {Function} 17 | * @api public 18 | */ 19 | function select(/*channels...*/) { 20 | var selectCh = make(arguments.length) 21 | var chans = [].slice.call(arguments, 0) 22 | var remaining = chans.length 23 | 24 | // get all channels with values waiting 25 | var full = chans.filter(function (ch) { 26 | return ch.__chan.items.length + ch.__chan.pendingAdds.length > 0 27 | }) 28 | 29 | // define get callback 30 | var get = function (err, value) { 31 | var args = arguments 32 | var ch = Channel.lastCalled 33 | 34 | // don't select an channel returning an empty value, unless it is last 35 | if (value === ch.empty && --remaining > 0) { 36 | return 37 | } 38 | 39 | // remove get callback from all selected channels 40 | chans.forEach(function (ch) { ch.__chan.removeGet(get) }) 41 | 42 | // add temporary selected yieldable function 43 | ch.selected = function (cb) { 44 | delete ch.selected 45 | cb.apply(null, args) 46 | } 47 | 48 | // added the selected channel to the select channel 49 | selectCh(null, ch) 50 | selectCh.close() 51 | } 52 | 53 | if (full.length > 1) { 54 | // multiple channels with waiting values, pick one at random 55 | full[Math.floor(Math.random() * full.length)](get) 56 | } else { 57 | // add get callback to all channels 58 | chans.forEach(function (ch) { ch(get) }) 59 | } 60 | 61 | return selectCh 62 | } 63 | -------------------------------------------------------------------------------- /lib/timeout.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | var make = require('./make') 5 | 6 | /** 7 | * Expose `timeoutChan`. 8 | */ 9 | module.exports = timeoutChan 10 | 11 | /** 12 | * Make a timeout channel that receives `true` after a number of milliseconds. 13 | * 14 | * @param {Number} ms 15 | * @returns {Function} channel 16 | * @api public 17 | */ 18 | function timeoutChan(ms) { 19 | var ch = make() 20 | 21 | setTimeout(function () { 22 | try { 23 | ch(true) 24 | ch.close() 25 | } catch(err) {} 26 | }, ms) 27 | 28 | return ch 29 | } 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chan", 3 | "version": "0.6.1", 4 | "description": "A go style channel implementation that works nicely with co", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "http://github.com/brentburgoyne/chan" 12 | }, 13 | "keywords": [ 14 | "async", 15 | "go", 16 | "channel", 17 | "co", 18 | "generator" 19 | ], 20 | "author": "Brent Burgoyne", 21 | "contributors": [ 22 | { 23 | "name": "Brent Burgoyne" 24 | }, 25 | { 26 | "name": "TJ Holowaychuk", 27 | "email": "tj@vision-media.ca" 28 | }, 29 | { 30 | "name": "Eugene Ware", 31 | "email": "eugene@noblesamurai.com" 32 | } 33 | ], 34 | "license": "MIT", 35 | "bugs": { 36 | "url": "https://github.com/brentburgoyne/chan/issues" 37 | }, 38 | "homepage": "https://github.com/brentburgoyne/chan", 39 | "devDependencies": { 40 | "co": "^3.0.6", 41 | "expect.js": "^0.3.1", 42 | "mocha": "^1.20.1", 43 | "should": "^4.0.4", 44 | "sinon": "^1.10.3", 45 | "split": "^0.3.0", 46 | "superagent": "^0.18.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/async.js: -------------------------------------------------------------------------------- 1 | /* jshint expr:true */ 2 | /* global describe:true, beforeEach:true, afterEach:true, it:true */ 3 | 4 | var async = require('../lib/async') 5 | var should = require('should') 6 | var sinon = require('sinon') 7 | 8 | describe('Async helper', function () { 9 | 10 | var err = {} 11 | var val = {} 12 | var ch 13 | var fn 14 | 15 | beforeEach(function () { 16 | ch = sinon.stub().returns(function (cb) { cb() }) 17 | fn = sinon.stub().yields(err, val) 18 | }) 19 | 20 | it( 21 | 'should return a function with an arity of 1', 22 | function () { 23 | var thunk = async(ch, fn) 24 | thunk.should.be.a.Function 25 | thunk.length.should.be.exactly(1) 26 | } 27 | ) 28 | 29 | it( 30 | 'should call fn with args plus a callback', 31 | function () { 32 | async(ch, fn, 1, 2, 3, 'foo') 33 | var argsWithoutCb = fn.firstCall.args.slice(0, -1) 34 | argsWithoutCb.should.eql([1, 2, 3, 'foo']) 35 | } 36 | ) 37 | 38 | it( 39 | 'should call a method of an object with the third argument as the name', 40 | function () { 41 | var ob = { foo: fn } 42 | async(ch, ob, 'foo', 1, 2, 3) 43 | var argsWithoutCb = fn.firstCall.args.slice(0, -1) 44 | argsWithoutCb.should.eql([1, 2, 3]) 45 | fn.firstCall.calledOn(ob).should.be.true 46 | } 47 | ) 48 | 49 | it( 50 | 'should call channel with arguments of the async function callback', 51 | function () { 52 | async(ch, fn) 53 | ch.firstCall.args.length.should.be.exactly(2) 54 | ch.firstCall.args[0].should.be.exactly(err) 55 | ch.firstCall.args[1].should.be.exactly(val) 56 | } 57 | ) 58 | 59 | it( 60 | 'should call callback given to returned function', 61 | function (done) { 62 | var cb = sinon.spy() 63 | async(ch, fn)(cb) 64 | setImmediate(function () { 65 | cb.callCount.should.be.exactly(1) 66 | done() 67 | }) 68 | } 69 | ) 70 | 71 | }) 72 | -------------------------------------------------------------------------------- /test/buffered.js: -------------------------------------------------------------------------------- 1 | /* jshint loopfunc: true */ 2 | /* global describe:true, beforeEach:true, it:true */ 3 | 4 | var chan = require('..') 5 | var expect = require('expect.js') 6 | 7 | describe('A unbuffered channel', function () { 8 | 9 | it( 10 | 'should not call the added callback until the value is removed', 11 | function (done) { 12 | var ch = chan(0) // unbuffered 13 | var cbCalled = false 14 | ch('foo')(function () { 15 | cbCalled = true 16 | }) 17 | setImmediate(function () { 18 | expect(cbCalled).to.not.be.ok() 19 | ch(function (err, val) { 20 | setImmediate(function () { 21 | expect(cbCalled).to.be.ok() 22 | done() 23 | }) 24 | }) 25 | }) 26 | } 27 | ) 28 | 29 | }) 30 | 31 | describe('A buffered channel', function () { 32 | 33 | it( 34 | 'should pull values from the buffer when yielded', 35 | function (done) { 36 | var ch = chan(1) 37 | var cbCalled = false 38 | var testValue = 'foo' 39 | ch(testValue) 40 | ch(function (err, val) { 41 | cbCalled = true 42 | expect(val).to.be(testValue) 43 | }) 44 | setImmediate(function () { 45 | expect(cbCalled).to.be.ok() 46 | done() 47 | }) 48 | } 49 | ) 50 | 51 | describe('with a non-full buffer', function () { 52 | 53 | it( 54 | 'should call added callback as soon as it is given to the returned thunk', 55 | function (done) { 56 | var buffer = 3 57 | var ch = chan(buffer) 58 | var called = 0 59 | var added = 0 60 | while (++added <= buffer + 10) { 61 | ch(added)(function (err) { 62 | called++ 63 | }) 64 | } 65 | setImmediate(function () { 66 | expect(called).to.be(buffer) 67 | done() 68 | }) 69 | } 70 | ) 71 | 72 | }) 73 | 74 | describe('with a full buffer', function () { 75 | 76 | it( 77 | 'should not add another value untill a value has been removed', 78 | function (done) { 79 | var ch = chan(1) 80 | var cbCalled = false 81 | ch('foo') 82 | ch('bar')(function () { 83 | cbCalled = true 84 | }) 85 | setImmediate(function () { 86 | expect(cbCalled).to.not.be.ok() 87 | ch(function (err, val) { 88 | setImmediate(function () { 89 | expect(cbCalled).to.be.ok() 90 | done() 91 | }) 92 | }) 93 | }) 94 | } 95 | ) 96 | 97 | it( 98 | 'should call cb with an error when the channel is closed before adding', 99 | function (done) { 100 | var ch = chan(0) 101 | var cbCalled = false 102 | ch('foo')(function (err) { 103 | cbCalled = true 104 | expect(err).to.be.an(Error) 105 | }) 106 | ch.close() 107 | setImmediate(function () { 108 | expect(cbCalled).to.be.ok() 109 | done() 110 | }) 111 | } 112 | ) 113 | 114 | }) 115 | 116 | }) 117 | -------------------------------------------------------------------------------- /test/chan.js: -------------------------------------------------------------------------------- 1 | /* global describe:true, beforeEach:true, it:true */ 2 | 3 | var chan = require('..') 4 | var expect = require('expect.js') 5 | var fs = require('fs') 6 | 7 | describe('Channel make', function () { 8 | 9 | it( 10 | 'should return a channel function', 11 | function () { 12 | var ch = chan() 13 | expect(ch).to.be.a(Function) 14 | } 15 | ) 16 | 17 | }) 18 | 19 | describe('A channel', function () { 20 | 21 | var ch 22 | 23 | beforeEach(function () { 24 | ch = chan() 25 | }) 26 | 27 | it( 28 | 'should receive a value of any non-function type as the first argument', 29 | function () { 30 | var typeCases = [ 31 | 1, 32 | 'foo', 33 | [1, 2 , 3], 34 | {foo: 'bar'}, 35 | true, 36 | false, 37 | null, 38 | void 0 39 | ] 40 | typeCases.forEach(function (val) { 41 | ch(val) 42 | ch(function (err, result) { 43 | expect(result).to.be(val) 44 | }) 45 | }) 46 | } 47 | ) 48 | 49 | it( 50 | 'should receive a function value as a second argument if the first is null', 51 | function () { 52 | ch(null, function () {}) 53 | ch(function (err, result) { 54 | expect(result).to.be.a(Function) 55 | }) 56 | } 57 | ) 58 | 59 | it( 60 | 'should queue values until they are yielded/removed', 61 | function () { 62 | var values = [1, 2, 3, 4, 5] 63 | values.forEach(function (value) { 64 | ch(value) 65 | }) 66 | values.forEach(function (value) { 67 | ch(function (err, result) { 68 | expect(result).to.be(value) 69 | }) 70 | }) 71 | } 72 | ) 73 | 74 | it( 75 | 'should queue callbacks until values are added', 76 | function () { 77 | var values = [1, 2, 3, 4, 5] 78 | values.forEach(function (value) { 79 | ch(function (err, result) { 80 | expect(result).to.be(value) 81 | }) 82 | }) 83 | values.forEach(function (value) { 84 | ch(value) 85 | }) 86 | } 87 | ) 88 | 89 | it( 90 | 'should pass errors as the first argument to callbacks', 91 | function () { 92 | var e = new Error('Foo') 93 | ch(e) 94 | ch(function (err) { 95 | expect(err).to.be(e) 96 | }) 97 | } 98 | ) 99 | 100 | it( 101 | 'should be useable directly as a callback for node style async functions', 102 | function (done) { 103 | ch(function (err, contents) { 104 | expect(err).to.be(null) 105 | expect(contents).to.be.a(Buffer) 106 | done() 107 | }) 108 | fs.readFile(__filename, ch) 109 | } 110 | ) 111 | 112 | }) 113 | -------------------------------------------------------------------------------- /test/close.js: -------------------------------------------------------------------------------- 1 | /* global describe:true, beforeEach:true, it:true */ 2 | 3 | var chan = require('..') 4 | var expect = require('expect.js') 5 | 6 | describe('A closed channel', function () { 7 | 8 | it( 9 | 'should yield an error when attempting to add a value', 10 | function () { 11 | var ch = chan() 12 | ch.close() 13 | ch('foo')(function (err) { 14 | expect(err).to.be.an(Error) 15 | }) 16 | } 17 | ) 18 | 19 | describe('that is has items in the buffer', function () { 20 | 21 | it( 22 | 'should return `false` when the `done()` method is called', 23 | function () { 24 | var ch = chan(1) 25 | ch('foo') 26 | ch.close() 27 | expect(ch.done()).to.be(false) 28 | } 29 | ) 30 | 31 | }) 32 | 33 | describe('that is empty', function () { 34 | 35 | it( 36 | 'should invoke peding callbacks with empty value', 37 | function () { 38 | var ch = chan() 39 | ch(function (err, value) { 40 | expect(value).to.be(ch.empty) 41 | }) 42 | ch.close() 43 | } 44 | ) 45 | 46 | it( 47 | 'should return `true` when the `done()` method is called', 48 | function () { 49 | var ch = chan() 50 | ch.close() 51 | expect(ch.done()).to.be(true) 52 | } 53 | ) 54 | 55 | it( 56 | 'should immediately invoke any callback added with the empty value', 57 | function () { 58 | var ch = chan() 59 | ch.close() 60 | ch(function (err, value) { 61 | expect(value).to.be(ch.empty) 62 | }) 63 | } 64 | ) 65 | 66 | }) 67 | 68 | }) 69 | -------------------------------------------------------------------------------- /test/interval.js: -------------------------------------------------------------------------------- 1 | /* jshint expr:true */ 2 | /* global describe:true, beforeEach:true, afterEach:true, it:true */ 3 | 4 | var interval = require('../lib/interval') 5 | var should = require('should') 6 | var sinon = require('sinon') 7 | 8 | describe('Interval channel make', function () { 9 | 10 | it('should return a function', function () { 11 | var int = interval(500) 12 | int.should.be.a.Function 13 | }) 14 | 15 | it('should should call the callback after a number of ms', function () { 16 | var clock = sinon.useFakeTimers() 17 | var cb = sinon.spy() 18 | var ms = 500 19 | var int = interval(ms) 20 | int(cb) 21 | clock.tick(ms - 1) 22 | cb.called.should.be.false 23 | clock.tick(1) 24 | cb.called.should.be.true 25 | }) 26 | 27 | it('should call the callback after number of ms', function () { 28 | var clock = sinon.useFakeTimers() 29 | var cb = sinon.spy() 30 | var ms = 500 31 | var int = interval(ms) 32 | int(cb) 33 | clock.tick(ms - 1) 34 | cb.called.should.be.false 35 | clock.tick(1) 36 | cb.called.should.be.true 37 | }) 38 | 39 | }) 40 | 41 | -------------------------------------------------------------------------------- /test/select.js: -------------------------------------------------------------------------------- 1 | /* jshint loopfunc:true */ 2 | /* global describe:true, beforeEach:true, afterEach:true, it:true */ 3 | 4 | var chan = require('..') 5 | var expect = require('expect.js') 6 | 7 | describe('Channel select', function () { 8 | var random 9 | beforeEach(function (done) { 10 | // save Math.random 11 | random = Math.random 12 | done() 13 | }) 14 | 15 | afterEach(function (done) { 16 | // restore Math.random 17 | Math.random = random 18 | done() 19 | }) 20 | 21 | it( 22 | 'should be able to select on channels', 23 | function (done) { 24 | var ch1 = chan() 25 | var ch2 = chan() 26 | chan.select(ch1, ch2)(function (err, ch) { 27 | expect(ch).to.equal(ch2) 28 | ch2.selected(function (err, val) { 29 | expect(val).to.equal(42) 30 | done() 31 | }) 32 | }) 33 | ch2(42) 34 | } 35 | ) 36 | 37 | it( 38 | 'should be able to select on multiple channels', 39 | function (done) { 40 | var chs = [chan(), chan()] 41 | var remaining = chs.length 42 | chs.forEach(function (needle, i) { 43 | chan.select.apply(null, chs)(function (err, ch) { 44 | expect(ch).to.equal(needle) 45 | ch.selected(function (err, val) { 46 | expect(val).to.equal(i*10) 47 | if (--remaining === 0) { 48 | done() 49 | } 50 | }) 51 | }) 52 | }) 53 | chs.forEach(function (ch, i) { 54 | ch(i*10) 55 | }) 56 | } 57 | ) 58 | 59 | it( 60 | 'should be able to select with queued messages', 61 | function (done) { 62 | var chs = [chan(), chan()] 63 | var remaining = chs.length 64 | var i = -1 65 | while (++i < 10) { 66 | (function (i) { 67 | chan.select.apply(null, chs)(function (err, ch) { 68 | expect(ch).to.equal(chs[0]) 69 | ch.selected(function (err, val) { 70 | expect(val).to.equal(i * 10) 71 | if (--remaining === 0) { 72 | done() 73 | } 74 | }) 75 | }) 76 | })(i) 77 | } 78 | var j = -1 79 | while (++j < 10) { 80 | chs[0](j * 10) 81 | } 82 | } 83 | ) 84 | 85 | it( 86 | 'should be able to select with existing messages on the channels', 87 | function (done) { 88 | var ch1 = chan() 89 | var ch2 = chan() 90 | ch2(42) 91 | chan.select(ch1, ch2)(function (err, ch) { 92 | expect(ch).to.equal(ch2) 93 | ch2.selected(function (err, val) { 94 | expect(val).to.equal(42) 95 | done() 96 | }) 97 | }) 98 | } 99 | ) 100 | 101 | it( 102 | 'should randomly choose a channel to return with multiple full channels', 103 | function (done) { 104 | var ch1 = chan() 105 | var ch2 = chan() 106 | 107 | // force the random selection to be the second channel 108 | Math.random = function () { return 0.5 } 109 | 110 | // fill up both the channels 111 | ch1(21) 112 | ch2(42) 113 | 114 | // random selection should choose the second channel "randomly" 115 | chan.select(ch1, ch2)(function (err, ch) { 116 | expect(ch).to.equal(ch2) 117 | ch2.selected(function (err, val) { 118 | expect(val).to.equal(42) 119 | done() 120 | }) 121 | }) 122 | } 123 | ) 124 | 125 | it ( 126 | 'should wait for previously queued callbacks before selecting', 127 | function (done) { 128 | var ch1 = chan() 129 | var ch2 = chan() 130 | 131 | // queue a callback for ch1 132 | ch1(function () {}) 133 | 134 | chan.select(ch1, ch2)(function (err, ch) { 135 | expect(ch).to.be(ch2) 136 | done() 137 | }) 138 | 139 | ch1(74) 140 | ch2(47) 141 | } 142 | ) 143 | }) 144 | -------------------------------------------------------------------------------- /test/timeout.js: -------------------------------------------------------------------------------- 1 | /* jshint expr:true */ 2 | /* global describe:true, beforeEach:true, afterEach:true, it:true */ 3 | 4 | var timeout = require('../lib/timeout') 5 | var should = require('should') 6 | var sinon = require('sinon') 7 | 8 | describe('Timeout channel make', function () { 9 | 10 | it('should return a function', function () { 11 | var to = timeout(500) 12 | to.should.be.a.Function 13 | }) 14 | 15 | it('should should call the callback after a number of ms', function () { 16 | var clock = sinon.useFakeTimers() 17 | var cb = sinon.spy() 18 | var ms = 500 19 | var to = timeout(ms) 20 | to(cb) 21 | clock.tick(ms - 1) 22 | cb.called.should.be.false 23 | clock.tick(1) 24 | cb.called.should.be.true 25 | }) 26 | 27 | }) 28 | --------------------------------------------------------------------------------