diff --git a/.codeclimate.yml b/.codeclimate.yml deleted file mode 100644 index afc5a1e..0000000 --- a/.codeclimate.yml +++ /dev/null @@ -1,9 +0,0 @@ -engines: - eslint: - enabled: true -ratings: - paths: - - "bin/eslint.js" -exclude_paths: -- "node_modules/**" -- "tests/**" diff --git a/.gitignore b/.gitignore index 6b1d8fe..01db8ad 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,5 @@ node_modules # Compiled api docs api_docs /log.txt +/bin/kalm.min.js +/bin/kalm.min.js.map diff --git a/.travis.yml b/.travis.yml index d848029..5929118 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ language: node_js node_js: - "5.0" -script: "npm run-script test" +script: "npm run-script test && node tests/benchmarks/index.js" diff --git a/README.md b/README.md index f043078..df21b13 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,22 @@ + # Kalm +*The Socket Optimizer* [![Kalm](https://img.shields.io/npm/v/kalm.svg)](https://www.npmjs.com/package/kalm) [![Build Status](https://travis-ci.org/fed135/Kalm.svg?branch=master)](https://travis-ci.org/fed135/Kalm) -[![Code Climate](https://codeclimate.com/github/fed135/Kalm/badges/gpa.svg)](https://codeclimate.com/github/fed135/Kalm) [![Dependencies Status](https://david-dm.org/fed135/Kalm.svg)](https://www.npmjs.com/package/kalm) -[![Current Stage](https://img.shields.io/badge/stage-alpha-blue.svg)](https://codeclimate.com/github/fed135/Kalm) +[![Current Stage](https://img.shields.io/badge/stage-beta-blue.svg)](https://codeclimate.com/github/fed135/Kalm) +--- -A library to simplify and optimize your Socket communications. +Simplify and optimize your Socket communications with: -- Packet bundling -- Packet minification +- Packet bundling and minification - Easy-to-use single syntax for all protocols -- Channels for all protocols -- Plug-and-play -- Ultra-flexible and extensible +- Event channels for all protocols +- Ultra-flexible and extensible adapters + +--- ## Installation @@ -24,92 +26,101 @@ A library to simplify and optimize your Socket communications. ## Usage +**Server** + +```node var Kalm = require('Kalm'); - var client = new Kalm.Client({ - hostname: '0.0.0.0', // Some ip - port: 3000, // Some port - adapter: 'tcp', + // Create a server, a listener for incomming connections + var server = new Kalm.Server({ + port: 6000, + adapter: 'udp', encoder: 'msg-pack', channels: { - 'myEvent': function(data) {} // Handler + messageEvent: function(data) { // Handler - new connections will register to these events + console.log('User sent message ' + data.body); + } } }); - client.send('myEvent', {foo: 'bar'}); // Can send Objects, Strings or Buffers - client.channel('someOtherEvent', function() {}); // Can add other handlers dynamically + // When a connection is received, send a message to all connected users + server.on('connection', function(client) { // Handler, where client is an instance of Kalm.Client + server.broadcast('userEvent', 'A new user has connected'); + }); + +``` - var server = new Kalm.Server({ - port: 6000, - adapter: 'udp', - encoder: 'json', +**Client** + +```node + // Create a connection to the server + var client = new Kalm.Client({ + hostname: '0.0.0.0', // Server's IP + port: 6000, // Server's port + adapter: 'udp', // Server's adapter + encoder: 'msg-pack', // Server's encoder channels: { - 'myEvent': function(data) {} // Handler - new connections will register to these events + 'userEvent': function(data) { + console.log('Server: ' + data); + } } }); - server.on('connection', function(client) {} // Handler, where client is an instance of Kalm.Client - server.broadcast('someOtherEvent', 'hello!'); + client.send('messageEvent', {body: 'This is an object!'}); // Can send Objects, Strings or Buffers + client.channel('someOtherEvent', function() {}); // Can add other handlers dynamically +``` ## Performance analysis -### Requests per minute +**Requests per minute** -| | IPC | TCP | UDP | Web Sockets | -|---|---|---|---|---| -| Raw | 1332330 | 844750 | 822690 | - | -| Kalm | 5558920 | 1102570 | 5219490 | - | -| **Result** | +417.2% | +30.5% | +634.5% | - | + *Benchmarks based on a single-thread queue test with Kalm default bundling settings AND msg-pack enabled* -*5 run average* +**Bytes transfered** -### Bytes transfered + -| | IPC | TCP | UDP | WebSockets | -|---|---|---|---|---| -| Raw | 81000 | 81000 | 57000 | - | -| Kalm | 6759 | 6759 | 8601 | - | -| **Result** | 11.9x less | 11.9x less | 6.6x less | - | - -*Using wireshark - number of bytes transfered per 1000 requests* +*Number of bytes transfered per 1000 requests* ## Adapters Allow you to easily use different socket types, hassle-free -| **Type** | **Library used** | **Status** | -|---|---|---| -| IPC | | STABLE | -| TCP | | STABLE | -| UDP | | STABLE | -| [kalm-websocket](https://github.com/fed135/kalm-websocket) | [socket.io](http://socket.io/) | DEV | +- ipc (bundled) +- tcp (bundled) +- udp (bundled) +- [kalm-websocket](https://github.com/fed135/kalm-websocket) ## Encoders -Encode the payloads before emitting. +Encodes/Decodes the payloads -| **Type** | **Library used** | **Status** | -|---|---|---| -| JSON | | STABLE | -| MSG-PACK | [msgpack-lite](https://github.com/kawanet/msgpack-lite) | STABLE | +- json (bundled) +- msg-pack (bundled) -## Middleware +## Loading custom adapters -Perform batch operation of payloads. +The framework is flexible enough so that you can load your own custom adapters, encoders or middlewares - say you wanted support for protocols like zmq, WebSockets or have yaml encoding. -| **Type** | **Library used** | **Status** | -|---|---|---| -| Bundler | | STABLE | +```node + // Custom adapter loading example + var Kalm = require('Kalm'); + var MyCustomAdapter = require('my-custom-adapter'); ---- + Kalm.adapters.register('my-custom-adapter', MyCustomAdapter); -The framework is flexible enough so that you can load your own custom adapters, encoders or middlewares - say you wanted support for protocols like zmq, WebSockets or have yaml encoding. + var server = new Kalm.Server({ + port: 3000, + adapter: 'my-custom-adapter', + encoder: 'msg-pack' + }); +``` ## Run tests @@ -119,11 +130,9 @@ The framework is flexible enough so that you can load your own custom adapters, ## Debugging -By default, all Kalm logs are absorbed. They can be enabled through the DEBUG environement variable. See [debug](https://github.com/visionmedia/debug) for more info. - -Ex: +By default, all Kalm logs are hidden. They can be enabled through the DEBUG environement variable. See [debug](https://github.com/visionmedia/debug) for more info. - $ DEBUG=kalm + export DEBUG=kalm ## Roadmap @@ -135,5 +144,3 @@ Ex: I am looking for contributors to help improve the codebase and create adapters, encoders and middleware. Email me for details. - -Thank you! diff --git a/index.js b/index.js index 9baa2c9..0f63482 100644 --- a/index.js +++ b/index.js @@ -7,12 +7,6 @@ /* Requires ------------------------------------------------------------------*/ var Kalm = require('./src'); -var pkg = require('./package'); - -/* Init ----------------------------------------------------------------------*/ - -Kalm.Client.pkg = pkg; -Kalm.Server.pkg = pkg; /* Exports -------------------------------------------------------------------*/ diff --git a/package.json b/package.json index 5597c58..479caee 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,11 @@ { "name": "kalm", - "version": "0.1.2", + "version": "0.2.0", "description": "The socket optimizer", "main": "./index.js", "scripts": { - "test": "mocha tests/index.js" + "test": "mocha tests/index.js", + "build": "webpack -p -d -j --define process.env.NODE_ENV='\"browser\"' --config ./webpack.config" }, "repository": { "type": "git", @@ -33,8 +34,6 @@ "homepage": "https://github.com/fed135/Kalm#readme", "devDependencies": { "chai": "3.4.x", - "coveralls": "2.11.x", - "istanbul": "0.4.x", "mocha": "2.4.x" }, "dependencies": { diff --git a/src/Channel.js b/src/Channel.js new file mode 100644 index 0000000..9a2f83b --- /dev/null +++ b/src/Channel.js @@ -0,0 +1,99 @@ +/** + * Channel class + * @class Channel + * @exports {Channel} + */ + +'use strict'; + +/* Methods -------------------------------------------------------------------*/ + +class Channel { + + /** + * Channel constructor + * @constructor + * @param {Socket} socket An optionnal socket object to use for communication + * @param {object} options The configuration options for the client + */ + constructor(name, options, client) { + this.name = name; + this.options = options; + + this._client = client; + this._emitter = client._emit.bind(client); + + this._timer = null; + this._packets = []; + this._handlers = []; + } + + /** + * Tells the channel to process the payload to send + * @method send + * @memberof Channel + * @param {object|string} payload The payload to process + */ + send(payload) { + this._packets.push(payload); + + // Bundling process + if (this._packets.length >= this.options.maxPackets) { + if (this._timer !== null) { + clearTimeout(this._timer); + this._timer = null; + } + + this._emit(); + return; + } + + if (this._timer === null) { + this._timer = setTimeout(this._emit.bind(this), this.options.delay); + } + } + + /** + * Alerts the client to emit the packets for this channel + * @private + * @method _emit + * @memberof Channel + */ + _emit() { + this._emitter(this.name, this._packets); + this._packets.length = 0; + } + + /** + * Adds a method that listens to this channel + * @method addHandler + * @memberof Channel + * @param {function} method The method to bind + */ + addHandler(method) { + this._handlers.push(method); + } + + /** + * Handles channel data + * @method handleData + * @memberof Channel + * @param {array} payload The received payload + */ + handleData(payload) { + var _reqs = payload.length; + var _listeners = this._handlers.length; + var i; + var c; + + for (i = 0; i<_reqs; i++) { + for (c = 0; c<_listeners; c++) { + this._handlers[c](payload[i], this._client); + } + } + }; +} + +/* Exports -------------------------------------------------------------------*/ + +module.exports = Channel; \ No newline at end of file diff --git a/src/Client.js b/src/Client.js index c1fe511..3ccfb80 100644 --- a/src/Client.js +++ b/src/Client.js @@ -8,180 +8,174 @@ /* Requires ------------------------------------------------------------------*/ -var util = require('util'); -var EventEmitter = require('events').EventEmitter; +const EventEmitter = require('events').EventEmitter; -var debug = require('debug')('kalm'); +const debug = require('debug')('kalm'); +var defaults = require('./defaults'); var adapters = require('./adapters'); var encoders = require('./encoders'); -var middleware = require('./middleware'); -/* Methods -------------------------------------------------------------------*/ +var Channel = require('./Channel'); -Client.UID = 0; +/* Local variables -----------------------------------------------------------*/ -/** - * Client constructor - * @constructor - * @param {Socket} socket An optionnal socket object to use for communication - * @param {object} options The configuration options for the client - */ -function Client(socket, options) { - EventEmitter.call(this); - - this.uid = Client.UID++; - - if (options === undefined) { - options = socket; - socket = null; - } - options = options || {}; - - this.options = { - // Basic info - hostname: options.hostname || '0.0.0.0', - port: options.port || 80, - // Adapter - adapter: options.adapter || 'ipc', - // Encoding - encoder: options.encoder || 'json', - // Transformations (middleware) - transform: options.transform || { - bundler: { - maxPackets: 512, - delay: 16 - } +const _channelBase = '/'; + +/* Methods -------------------------------------------------------------------*/ + +class Client extends EventEmitter{ + + /** + * Client constructor + * @constructor + * @param {Socket} socket An optionnal socket object to use for communication + * @param {object} options The configuration options for the client + */ + constructor(socket, options) { + super(); + if (options === undefined) { + options = socket; + socket = null; } - }; - - // List of channels - this.channels = {}; - // Populate channels - if (options.channels) { - for (var c in options.channels) { - this.channel(c, options.channels[c]); + options = options || {}; + + this.options = { + // Basic info + hostname: options.hostname || defaults.hostname, + port: options.port || defaults.port, + // Adapter + adapter: options.adapter || defaults.adapter, + // Encoding + encoder: options.encoder || defaults.encoder, + // Transformations (middleware) + bundler: options.bundler || defaults.bundler + }; + + // List of channels + this.channels = {}; + + // Populate channels + if (options.channels) { + for (var c in options.channels) { + this.channel(c, options.channels[c]); + } } - } - // Socket object - this.use(socket); - - // Data packets - transient state - by channel - this.packets = {}; -} + // Socket object + this.use(socket); + } -/** - * Creates a channel for the client - * @method channel - * @memberof Client - * @param {string} name The name of the channel. - * @param {function} handler The handler to add to the channel - * @returns {Client} The client, for chaining - */ -Client.prototype.channel = function(name, handler) { - name = name || '/'; + /** + * Creates a channel for the client + * @method channel + * @memberof Client + * @param {string} name The name of the channel. + * @param {function} handler The handler to add to the channel + * @returns {Client} The client, for chaining + */ + channel(name, handler) { + name = name || _channelBase; + + if (!this.channels.hasOwnProperty(name)) { + debug( + 'log: new channel ' + + this.options.adapter + '://' + this.options.hostname + ':' + + this.options.port + '/' + name + ); + this.channels[name] = new Channel( + name, + this.options.bundler, + this + ); + } - if (name[0] !== '/') name = '/' + name; + if (handler) { + this.channels[name].addHandler(handler); + } - if (!(name in this.channels)) { - debug( - 'log: new channel ' + - this.options.adapter + '://' + this.options.hostname + ':' + - this.options.port + name - ); - this.channels[name] = []; + return this; } - this.channels[name].push(handler); - return this; -}; + /** + * Defines a socket to use for communication, disconnects previous connection + * @method use + * @memberof Client + * @param {Socket} socket The socket to use + * @returns {Client} The client, for chaining + */ + use(socket) { + if (this.socket) { + debug('log: disconnecting current socket'); + adapters.resolve(this.options.adapter).disconnect(this.socket); + } -/** - * Defines a socket to use for communication, disconnects previous connection - * @method use - * @memberof Client - * @param {Socket} socket The socket to use - * @returns {Client} The client, for chaining - */ -Client.prototype.use = function(socket) { - if (this.socket) { - adapters.resolve(this.options.adapter).disconnect(this.socket); + this.socket = this._createSocket(socket); + return this; } - this.socket = this._createSocket(socket); - return this; -}; - -/** - * Queues a packet for transfer on the given channel - * @method send - * @memberof Client - * @param {string} channel The channel to send to data through - * @param {string|object} payload The payload to send - * @returns {Client} The client, for chaining - */ -Client.prototype.send = function(channel, payload) { - channel = channel || '/'; - if (!this.packets[channel]) this.packets[channel] = []; - this.packets[channel].push(payload); - // Go through middlewares - middleware.process(this, channel, payload); - - return this; -}; + /** + * Queues a packet for transfer on the given channel + * @method send + * @memberof Client + * @param {string} name The channel to send to data through + * @param {string|object} payload The payload to send + * @returns {Client} The client, for chaining + */ + send(name, payload) { + if (!this.channels.hasOwnProperty(name)) { + this.channel(name); + } + this.channels[name].send(payload); + return this; + } -/** - * Creates or attaches a socket for the appropriate adapter - * @private - * @method _createSocket - * @memberof Client - * @param {Socket} socket The socket to use - * @returns {Socket} The created or attached socket for the client - */ -Client.prototype._createSocket = function(socket) { - return adapters.resolve(this.options.adapter).createSocket(this, socket); -}; + /** + * Creates or attaches a socket for the appropriate adapter + * @private + * @method _createSocket + * @memberof Client + * @param {Socket} socket The socket to use + * @returns {Socket} The created or attached socket for the client + */ + _createSocket(socket) { + return adapters.resolve(this.options.adapter).createSocket(this, socket); + } -/** - * Sends a packet - triggered by middlewares - * @private - * @method _emit - * @memberof Client - * @param {string} channel The channel targeted for transfer - */ -Client.prototype._emit = function(channel) { - adapters.resolve(this.options.adapter).send( - this.socket, - encoders.resolve(this.options.encoder).encode({ - c: channel, - d: this.packets[channel] - }) - ); - this.packets[channel].length = 0; -} + /** + * Sends a packet - triggered by middlewares + * @private + * @method _emit + * @memberof Client + * @param {string} channel The channel targeted for transfer + */ + _emit(channel, packets) { + adapters.resolve(this.options.adapter).send( + this.socket, + encoders.resolve(this.options.encoder).encode({ + c: channel, + d: packets + }) + ); + } -/** - * Handler for receiving data through the listener - * @private - * @method _handleRequest - * @memberof Client - * @param {Buffer} evt The data received - */ -Client.prototype._handleRequest = function(evt) { - var raw = encoders.resolve(this.options.encoder).decode(evt); - if (raw.c[0] !== '/') raw.c = '/' + raw.c; - - if (raw.c in this.channels) { - for (var i = 0; i= 0; i--) { - this.connections[i].send(channel, payload); + /** + * Adds a channel to listen for on attached clients + * @method channel + * @memberof Server + * @param {string} name The name of the channel to attach + * @param {function} handler The handler to attach to the channel + * @returns {Server} Returns itself for chaining + */ + channel(name, handler) { + this.channels[name] = handler; + + for (var i = this.connections.length - 1; i >= 0; i--) { + this.connections[i].channel(name, handler); + } + + return this; } - return this; -}; + /** + * Sends data to all connected clients + * @method broadcast + * @memberof Server + * @param {string} channel The name of the channel to send to + * @param {string|object} payload The payload to send + * @returns {Server} Returns itself for chaining + */ + broadcast(channel, payload) { + for (var i = this.connections.length - 1; i >= 0; i--) { + this.connections[i].send(channel, payload); + } + + return this; + } -/** - * Closes the server - * @method stop - * @memberof Server - * @param {function} callback The callback method for the operation - */ -Server.prototype.stop = function(callback) { - this.connections.length = 0; - if (this.listener) { - adapters.resolve(this.options.adapter).stop(this, callback); + /** + * Closes the server + * @method stop + * @memberof Server + * @param {function} callback The callback method for the operation + */ + stop(callback) { + debug('warn: stopping server'); + if (this.listener) { + adapters.resolve(this.options.adapter).stop(this, callback); + } + else { + callback(); + } } -}; -/** - * Handler for receiving a new connection - * @private - * @method _handleRequest - * @memberof Server - * @param {Socket} socket The received connection socket - */ -Server.prototype._handleRequest = function(socket) { - var client = new Client(socket, { - adapter: this.options.adapter, - encoder: this.options.encoder, - channels: this.channels - }); - this.connections.push(client); - this.emit('connection', client); - return client; -}; - -util.inherits(Server, EventEmitter); + /** + * Handler for receiving a new connection + * @private + * @method _handleRequest + * @memberof Server + * @param {Socket} socket The received connection socket + */ + _handleRequest(socket) { + var client = new Client(socket, { + adapter: this.options.adapter, + encoder: this.options.encoder, + channels: this.channels + }); + this.connections.push(client); + this.emit('connection', client); + return client; + } +} /* Exports -------------------------------------------------------------------*/ diff --git a/src/adapters/index.js b/src/adapters/index.js index d1fd1c8..dad29fb 100644 --- a/src/adapters/index.js +++ b/src/adapters/index.js @@ -7,19 +7,16 @@ /* Requires ------------------------------------------------------------------*/ -var ipc = require('./ipc.adapter'); -var tcp = require('./tcp.adapter'); -var udp = require('./udp.adapter'); +const debug = require('debug')('kalm'); -var debug = require('debug')('kalm'); +var list = {}; -/* Local variables -----------------------------------------------------------*/ - -var list = { - ipc: ipc, - tcp: tcp, - udp: udp -}; +// If running in the browser, do not load net adapters +if (process.env.NODE_ENV !== 'browser') { + list.ipc = require('./ipc.adapter'); + list.tcp = require('./tcp.adapter'); + list.udp = require('./udp.adapter'); +} /* Methods -------------------------------------------------------------------*/ @@ -30,7 +27,7 @@ var list = { * @returns {object|undefined} The adapter */ function resolve(name) { - if (list[name]) { + if (list.hasOwnProperty(name)) { return list[name]; } else { diff --git a/src/adapters/ipc.adapter.js b/src/adapters/ipc.adapter.js index af4e5db..08b6a72 100644 --- a/src/adapters/ipc.adapter.js +++ b/src/adapters/ipc.adapter.js @@ -8,12 +8,14 @@ /* Requires ------------------------------------------------------------------*/ -var net = require('net'); -var fs = require('fs'); +const net = require('net'); +const fs = require('fs'); + +const debug = require('debug')('kalm'); /* Local variables -----------------------------------------------------------*/ -var defaultPath = '/tmp/app.socket-'; +const _path = '/tmp/app.socket-'; /* Methods -------------------------------------------------------------------*/ @@ -24,17 +26,31 @@ var defaultPath = '/tmp/app.socket-'; * @param {function} callback The callback for the operation */ function listen(server, callback) { - fs.unlink(defaultPath + server.options.port, function _bindSocket() { + fs.unlink(_path + server.options.port, () => { server.listener = net.createServer(server._handleRequest.bind(server)); - server.listener.listen(defaultPath + server.options.port, callback); - server.listener.on('error', function _handleServerError(err) { + server.listener.listen(_path + server.options.port, callback); + server.listener.on('error', (err) => { + debug('error: ' + err); server.emit('error', err); }); }); }; +/** + * Stops the server. + * @method stop + * @param {Server} server The server object + * @param {function} callback The success callback for the operation + */ function stop(server, callback) { - server.listener.close(callback || function() {}); + server.connections.forEach((e) => { + e.socket.destroy(); + }); + + process.nextTick(() => { + server.connections.length = 0; + server.listener.close(callback || function() {}); + }); } /** @@ -44,7 +60,7 @@ function stop(server, callback) { * @param {Buffer} payload The body of the request */ function send(socket, payload) { - socket.write(payload); + if (socket) socket.write(payload); }; /** @@ -56,13 +72,19 @@ function send(socket, payload) { */ function createSocket(client, socket) { if (!socket) { - socket = net.connect(defaultPath + client.options.port); + socket = net.connect(_path + client.options.port); } socket.on('data', client._handleRequest.bind(client)); - socket.on('error', function _handleSocketError(err) { + socket.on('error', (err) => { + debug('error: ' + err); client.emit('error', err); }); + // Will auto-reconnect + socket.on('close', () => { + client.socket = null; + }); + return socket; }; diff --git a/src/adapters/tcp.adapter.js b/src/adapters/tcp.adapter.js index 7bbda79..1063ca1 100644 --- a/src/adapters/tcp.adapter.js +++ b/src/adapters/tcp.adapter.js @@ -8,7 +8,9 @@ /* Requires ------------------------------------------------------------------*/ -var net = require('net'); +const net = require('net'); + +const debug = require('debug')('kalm'); /* Methods -------------------------------------------------------------------*/ @@ -21,7 +23,8 @@ var net = require('net'); function listen(server, callback) { server.listener = net.createServer(server._handleRequest.bind(server)); server.listener.listen(server.options.port, callback); - server.listener.on('error', function _handleServerError(err) { + server.listener.on('error', (err) => { + debug('error: ' + err); server.emit('error', err); }); } @@ -33,7 +36,7 @@ function listen(server, callback) { * @param {Buffer} payload The body of the request */ function send(socket, payload) { - socket.write(payload); + if (socket) socket.write(payload); } /** @@ -43,7 +46,14 @@ function send(socket, payload) { * @param {function} callback The success callback for the operation */ function stop(server, callback) { - server.listener.close(callback || function() {}); + server.connections.forEach((e) => { + e.socket.destroy(); + }); + + process.nextTick(() => { + server.connections.length = 0; + server.listener.close(callback || function() {}); + }); } /** @@ -58,10 +68,16 @@ function createSocket(client, socket) { socket = net.connect(client.options.port, client.options.hostname); } socket.on('data', client._handleRequest.bind(client)); - socket.on('error', function _handleSocketError(err) { + socket.on('error', (err) => { + debug('error: ' + err); client.emit('error', err); }); + // Will auto-reconnect + socket.on('close', () => { + client.socket = null; + }); + return socket; } diff --git a/src/adapters/udp.adapter.js b/src/adapters/udp.adapter.js index cf984d4..51f17d2 100644 --- a/src/adapters/udp.adapter.js +++ b/src/adapters/udp.adapter.js @@ -8,10 +8,15 @@ /* Requires ------------------------------------------------------------------*/ -var dgram = require('dgram'); +const dgram = require('dgram'); + +const debug = require('debug')('kalm'); /* Helpers -------------------------------------------------------------------*/ +/** + * Creates a socket + Client on UDP data + */ function _handleNewSocket(data, origin) { var key = origin.address+':'+origin.port; @@ -39,9 +44,13 @@ function _handleNewSocket(data, origin) { function listen(server, callback) { server.listener = dgram.createSocket('udp4'); server.listener.on('message', _handleNewSocket.bind(server)); + server.listener.on('error', (err) => { + debug('error: ' + err); + server.emit('error', err); + }); server.listener.bind(server.options.port, '127.0.0.1'); - process.nextTick(callback); + callback(); }; /** @@ -51,7 +60,6 @@ function listen(server, callback) { * @param {Buffer} payload The body of the request */ function send(socket, payload) { - console.log('send'); socket.send( payload, 0, @@ -68,6 +76,7 @@ function send(socket, payload) { * @param {function} callback The success callback for the operation */ function stop(server, callback) { + server.connections.length = 0; if (server.listener && server.listener.close) { server.listener.close(callback); } @@ -87,6 +96,10 @@ function createSocket(client, soc) { var socket = dgram.createSocket('udp4'); socket.__port = client.options.port; socket.__hostname = client.options.hostname; + socket.on('error', (err) => { + debug('error: ' + err); + client.emit('error', err); + }); return socket; }; diff --git a/src/defaults.js b/src/defaults.js new file mode 100644 index 0000000..86979ee --- /dev/null +++ b/src/defaults.js @@ -0,0 +1,18 @@ +/** + * Default properties for new Clients/ Servers + */ + +'use strict' + +/* Exports -------------------------------------------------------------------*/ + +module.exports = { + hostname: '0.0.0.0', + port: 3000, + adapter: 'tcp', + encoder: 'msg-pack', + bundler: { + maxPackets: 2048, + delay: 16 + } +}; \ No newline at end of file diff --git a/src/encoders/index.js b/src/encoders/index.js index 8e9e52d..f2d5a1d 100644 --- a/src/encoders/index.js +++ b/src/encoders/index.js @@ -10,7 +10,7 @@ var json = require('./json'); var msgPack = require('./msg-pack'); -var debug = require('debug')('kalm'); +const debug = require('debug')('kalm'); /* Local variables -----------------------------------------------------------*/ @@ -28,7 +28,7 @@ var list = { * @returns {object|undefined} The encoder */ function resolve(name) { - if (list[name]) { + if (list.hasOwnProperty(name)) { return list[name]; } else { diff --git a/src/encoders/msg-pack.js b/src/encoders/msg-pack.js index a95af3c..0ff77f5 100644 --- a/src/encoders/msg-pack.js +++ b/src/encoders/msg-pack.js @@ -8,8 +8,8 @@ /* Requires ------------------------------------------------------------------*/ -var msgDecode = require('msgpack-decode'); -var msgPack = require('msgpack-lite'); +const msgDecode = require('msgpack-decode'); +const msgPack = require('msgpack-lite'); /* Methods -------------------------------------------------------------------*/ diff --git a/src/index.js b/src/index.js index 103b42a..e9c95cd 100644 --- a/src/index.js +++ b/src/index.js @@ -11,7 +11,7 @@ var Client = require('./Client'); var Server = require('./Server'); var adapters = require('./adapters'); var encoders = require('./encoders'); -var middleware = require('./middleware'); +var defaults = require('./defaults'); /* Exports -------------------------------------------------------------------*/ @@ -20,5 +20,5 @@ module.exports = { Server: Server, adapters: adapters, encoders: encoders, - middleware: middleware + defaults: defaults }; \ No newline at end of file diff --git a/src/middleware/bundler.js b/src/middleware/bundler.js deleted file mode 100644 index 22a4e42..0000000 --- a/src/middleware/bundler.js +++ /dev/null @@ -1,37 +0,0 @@ -function process(client, channel, payload) { - var options = client.options.transform.bundler; - - if (!client.__bundler) { - client.__bundler = { - timers: {} - }; - } - - if (client.packets[channel].length > options.maxPackets) { - if (client.__bundler.timers[channel]) { - clearTimeout(client.__bundler.timers[channel]); - } - client._emit.call(client, channel); - } - - if (!client.__bundler.timers[channel]) { - client.__bundler.timers[channel] = setTimeout( - function _emitBundle() { - client._emit.call(client, channel); - }, - options.delay - ); - } -} - // Bundling logic - // ----------------------------- - // Add new calls to the bundle stack - // If stack length exceeds limit OR - // If time since last chunk sent is greater than the delay - - // AND there is an item in the stack - // Send. - // If an item is added and delay timer is not started, start it. - -module.exports = { - process: process -}; \ No newline at end of file diff --git a/src/middleware/index.js b/src/middleware/index.js deleted file mode 100644 index 0adbb79..0000000 --- a/src/middleware/index.js +++ /dev/null @@ -1,20 +0,0 @@ -var bundler = require('./bundler'); - -var list = { - bundler: bundler -}; - -function process(client, channel, payload) { - for (var t in client.options.transform) { - if (t in list) list[t].process(client, channel, payload); - } -} - -function register(name, mod) { - list[name] = mod; -} - -module.exports = { - process: process, - register: register -}; \ No newline at end of file diff --git a/tests/benchmarks/raw-ipc.test.js b/tests/benchmarks/adapters/ipc.js similarity index 64% rename from tests/benchmarks/raw-ipc.test.js rename to tests/benchmarks/adapters/ipc.js index 88c2fa8..394c596 100644 --- a/tests/benchmarks/raw-ipc.test.js +++ b/tests/benchmarks/adapters/ipc.js @@ -8,8 +8,7 @@ var net = require('net'); -var settings = require('./settings'); -var Kalm = require('../../index'); +var settings = require('../settings'); /* Local variables -----------------------------------------------------------*/ @@ -17,29 +16,46 @@ var server; var client; var count = 0; +var handbreak = true; /* Methods -------------------------------------------------------------------*/ +function _absorb(err) { + console.log(err); + return true; +} + function setup(resolve) { server = net.createServer(function(socket) { + socket.on('error', _absorb); socket.on('data', function() { count++; }); }); + handbreak = false; + server.on('error', _absorb); server.listen('/tmp/app.socket-' + settings.port, resolve); } function teardown(resolve) { - if (server) server.close(); - server = null; - client = null; - resolve(count); + if (client) client.destroy(); + if (server) server.close(function() { + server = null; + client = null; + resolve(count); + }); +} + +function stop(resolve) { + handbreak = true; + setTimeout(resolve, 0); } function step(resolve) { - if (!server) return; + if (handbreak) return; if (!client) { client = net.connect('/tmp/app.socket-' + settings.port); + client.on('error', _absorb); } client.write(JSON.stringify(settings.testPayload)); @@ -51,5 +67,6 @@ function step(resolve) { module.exports = { setup: setup, teardown: teardown, - step: step + step: step, + stop: stop }; \ No newline at end of file diff --git a/tests/benchmarks/kalm.test.js b/tests/benchmarks/adapters/kalm.js similarity index 70% rename from tests/benchmarks/kalm.test.js rename to tests/benchmarks/adapters/kalm.js index f25accf..d80b53b 100644 --- a/tests/benchmarks/kalm.test.js +++ b/tests/benchmarks/adapters/kalm.js @@ -6,8 +6,8 @@ /* Requires ------------------------------------------------------------------*/ -var settings = require('./settings'); -var Kalm = require('../../index'); +var settings = require('../settings'); +var Kalm = require('../../../index'); /* Local variables -----------------------------------------------------------*/ @@ -15,6 +15,7 @@ var server; var client; var count = 0; +var handbreak = true; /* Methods -------------------------------------------------------------------*/ @@ -29,28 +30,33 @@ function setup(resolve) { count++; }); + handbreak = false; server.on('ready', resolve); } function teardown(resolve) { - if (server) server.stop(); - server = null; - client = null; - resolve(count); + if (server) server.stop(function() { + server = null; + client = null; + resolve(count); + }); +} + +function stop(resolve) { + handbreak = true; + setTimeout(resolve, 0); } function step(resolve) { - if (!server) return; + if (handbreak) return; if (!client) { client = new Kalm.Client({ port: settings.port, adapter: settings.adapter, encoder: settings.encoder, - transform: { - bundler: { - maxPackets: settings.bundlerMaxPackets, - delay: settings.bundlerDelay - } + bundler: { + maxPackets: settings.bundlerMaxPackets, + delay: settings.bundlerDelay }, hostname: '0.0.0.0' }); @@ -65,5 +71,6 @@ function step(resolve) { module.exports = { setup: setup, teardown: teardown, - step: step + step: step, + stop: stop }; \ No newline at end of file diff --git a/tests/benchmarks/raw.test.js b/tests/benchmarks/adapters/tcp.js similarity index 63% rename from tests/benchmarks/raw.test.js rename to tests/benchmarks/adapters/tcp.js index daabb70..586c4b0 100644 --- a/tests/benchmarks/raw.test.js +++ b/tests/benchmarks/adapters/tcp.js @@ -8,8 +8,7 @@ var net = require('net'); -var settings = require('./settings'); -var Kalm = require('../../index'); +var settings = require('../settings'); /* Local variables -----------------------------------------------------------*/ @@ -17,31 +16,49 @@ var server; var client; var count = 0; +var handbreak = true; /* Methods -------------------------------------------------------------------*/ +function _absorb(err) { + console.log(err); + return; +} + function setup(resolve) { server = net.createServer(function(socket) { socket.on('data', function() { count++; }); + socket.on('error', _absorb); }); + handbreak = false; + server.on('error', _absorb); server.listen(settings.port, resolve); } function teardown(resolve) { - if (server) server.close(); - server = null; - client = null; - resolve(count); + if (client) client.destroy(); + if (server) server.close(function() { + server = null; + client = null; + resolve(count); + }); +} + +function stop(resolve) { + handbreak = true; + setTimeout(resolve, 0); } function step(resolve) { - if (!server) return; + if (handbreak) return; if (!client) { client = net.connect(settings.port, '0.0.0.0'); + client.on('error', _absorb); } + if (client) client.write(JSON.stringify(settings.testPayload)); resolve(); } @@ -51,5 +68,6 @@ function step(resolve) { module.exports = { setup: setup, teardown: teardown, - step: step + step: step, + stop: stop }; \ No newline at end of file diff --git a/tests/benchmarks/raw-udp.test.js b/tests/benchmarks/adapters/udp.js similarity index 75% rename from tests/benchmarks/raw-udp.test.js rename to tests/benchmarks/adapters/udp.js index 90526bc..0ead264 100644 --- a/tests/benchmarks/raw-udp.test.js +++ b/tests/benchmarks/adapters/udp.js @@ -8,7 +8,7 @@ var dgram = require('dgram'); -var settings = require('./settings'); +var settings = require('../settings'); /* Local variables -----------------------------------------------------------*/ @@ -16,16 +16,24 @@ var server; var client; var count = 0; +var handbreak = true; /* Methods -------------------------------------------------------------------*/ +function _absorb(err) { + console.log(err); + return; +} + function setup(resolve) { server = dgram.createSocket('udp4'); server.on('message', function() { count++; }); + handbreak = false; + server.on('error', _absorb); server.bind(settings.port, '0.0.0.0'); - process.nextTick(resolve); + resolve(); } function teardown(resolve) { @@ -36,10 +44,16 @@ function teardown(resolve) { }); } +function stop(resolve) { + handbreak = true; + setTimeout(resolve, 0); +} + function step(resolve) { - if (!server) return; + if (handbreak) return; if (!client) { client = dgram.createSocket('udp4'); + client.on('error', _absorb); } var payload = new Buffer(JSON.stringify(settings.testPayload)); @@ -59,5 +73,6 @@ function step(resolve) { module.exports = { setup: setup, teardown: teardown, - step: step + step: step, + stop: stop }; \ No newline at end of file diff --git a/tests/benchmarks/index.js b/tests/benchmarks/index.js index f1f23c6..26d6adf 100644 --- a/tests/benchmarks/index.js +++ b/tests/benchmarks/index.js @@ -1,33 +1,51 @@ -// TODO: Cleanup! +/** + * Kalm benchmarking + */ -var Kalm = require('./kalm.test'); -//var Raw = require('./raw.test'); -//var Raw = require('./raw-ipc.test'); -var Raw = require('./raw-udp.test'); -//var Raw = require('./raw-ws.test'); +'use strict'; + +/* Requires ------------------------------------------------------------------*/ + +var Kalm = require('./adapters/kalm'); +var TCP = require('./adapters/tcp'); +var IPC = require('./adapters/ipc'); +var UDP = require('./adapters/udp'); var settings = require('./settings'); -var maxCount = null; -var curr = 0; +/* Local variables -----------------------------------------------------------*/ + +var _maxCount = null; +var _curr = 0; + +var Suite = { + ipc: IPC, + tcp: TCP, + udp: UDP +}; -function _kalm(resolve) { - curr = 0; - Kalm.setup(function() { - setTimeout(function() { - Kalm.teardown(function(total) { - console.log('kalm: ' + total); - resolve(); +var tests = []; +var adpts; +var results = {}; + +/* Methods -------------------------------------------------------------------*/ + +function _measure(adapter, resolve) { + _curr = 0; + adapter.setup(function _setupHandler() { + setTimeout(function _stopAdapter() { + adapter.stop(function _finish() { + adapter.teardown(resolve); }); }, settings.testDuration); function _repeat() { - if (maxCount !== null) { - if (curr >= maxCount) return; - curr++; + if (_maxCount !== null) { + if (_curr >= _maxCount) return; + _curr++; } - setImmediate(function() { - Kalm.step(_repeat); + setImmediate(function _stepHandler() { + adapter.step(_repeat); }); } @@ -35,31 +53,63 @@ function _kalm(resolve) { }); } -function _raw(resolve) { - curr = 0; - Raw.setup(function() { - setTimeout(function() { - Raw.teardown(function(total) { - console.log('raw: ' + total); - resolve(); - }); - }, settings.testDuration); +function _updateSettings(obj, resolve) { + settings.adapter = obj.adapter || settings.adapter; + resolve(); +} - function _repeat() { - if (maxCount !== null) { - if (curr >= maxCount) return; - curr++; - } +function _errorHandler(err) { + console.log(err); +} - setImmediate(function() { - Raw.step(_repeat); - }); - } +function _postResults() { + console.log(JSON.stringify(results)); + // Do something with the info + process.exit(); +} - _repeat(); +/* Init ----------------------------------------------------------------------*/ + + +// Roll port number +settings.port = 3000 + Math.round(Math.random()*1000); + +var adpts = Object.keys(Suite).map(function(k) { + return { + adapter: k, + settings: {adapter: k}, + raw: Suite[k], + kalm: Kalm + }; +}); + +adpts.forEach(function(i) { + tests.push(function(resolve) { + console.log('Configuring ' + i.adapter); + _updateSettings(i.settings, resolve); }); -} -_kalm(function() { - _raw(process.exit); -}) \ No newline at end of file + tests.push(function(resolve) { + console.log('Measuring raw ' + i.adapter); + _measure(i.raw, function(total) { + results['raw_' + i.adapter] = total; + resolve(); + }); + }); + + tests.push(function(resolve) { + console.log('Measuring Kalm ' + i.adapter); + _measure(i.kalm, function(total) { + results['kalm_' + i.adapter] = total; + resolve(); + }); + }); +}); + +tests.push(_postResults); + +tests.reduce(function(current, next) { + return current.then(function(resolve) { + return new Promise(next).then(resolve, _errorHandler); + }, _errorHandler); +}, Promise.resolve()); \ No newline at end of file diff --git a/tests/benchmarks/settings.js b/tests/benchmarks/settings.js index 7a25554..17b8793 100644 --- a/tests/benchmarks/settings.js +++ b/tests/benchmarks/settings.js @@ -3,7 +3,7 @@ module.exports = { encoder: 'msg-pack', port: 3000, bundlerDelay: 16, - bundlerMaxPackets: 512, + bundlerMaxPackets: 2048, testDuration: 1000 * 60, testPayload: { foo: 'bar'}, testChannel: 'test' diff --git a/tests/index.js b/tests/index.js index 5af573d..072a720 100644 --- a/tests/index.js +++ b/tests/index.js @@ -1,31 +1,218 @@ +/** + * Kalm test suite + */ + +'use strict'; + +/* Requires ------------------------------------------------------------------*/ + var assert = require('chai').assert; var Kalm = require('../index'); -describe('Starting service', function() { - it('constructor', function(done) { - var server = new Kalm.Server({ - port: 3000, - adapter: 'ipc', - encoder: 'msg-pack' +/* Models --------------------------------------------------------------------*/ + +var adapterFormat = { + listen: function() {}, + send: function() {}, + stop: function() {}, + createSocket: function() {}, + disconnect: function() {} +}; + +var encoderFormat = { + encode: function() {}, + decode: function() {} +}; + +/* Suite ---------------------------------------------------------------------*/ + +describe('Index', function() { + it('Kalm', function() { + assert.property(Kalm, 'Client', 'Client not exposed in Kalm index'); + assert.property(Kalm, 'Server', 'Server not exposed in Kalm index'); + assert.property(Kalm, 'adapters', 'adapters not exposed in Kalm index'); + assert.property(Kalm, 'encoders', 'encoders not exposed in Kalm index'); + }); +}); + +describe('Adapters', function() { + + it('index', function() { + assert.isFunction(Kalm.adapters.register, 'register method not valid in Kalm adapters'); + assert.isFunction(Kalm.adapters.resolve, 'resolve method not valid in Kalm adapters'); + }); + + describe('bundled', function() { + it('ipc', function() { + var ipc_test = Kalm.adapters.resolve('ipc'); + assert.isObject(ipc_test, 'ipc is not a valid adapter object'); + allMembersTypeMatch(ipc_test, adapterFormat); + }); + + it('tcp', function() { + var tcp_test = Kalm.adapters.resolve('tcp'); + assert.isObject(tcp_test, 'tcp is not a valid adapter object'); + allMembersTypeMatch(tcp_test, adapterFormat); + }); + + it('udp', function() { + var udp_test = Kalm.adapters.resolve('udp'); + assert.isObject(udp_test, 'udp is not a valid adapter object'); + allMembersTypeMatch(udp_test, adapterFormat); + }); + }); + + describe('methods', function() { + it('register', function() { + Kalm.adapters.register('test', adapterFormat); + Kalm.adapters.register('test2', null); + }); + + it('resolve', function() { + assert.deepEqual(Kalm.adapters.resolve('test'), adapterFormat); + assert.equal(Kalm.adapters.resolve('test2'), null); + assert.equal(Kalm.adapters.resolve('test3'), null); + }); + }); +}); + +describe('Encoders', function() { + + it('index', function() { + assert.isFunction(Kalm.encoders.register, 'register method not valid in Kalm encoders'); + assert.isFunction(Kalm.encoders.resolve, 'resolve method not valid in Kalm encoders'); + }); + + describe('bundled', function() { + var objTest = {foo: 'bar'}; + var strTest = '{"foo":"bar}'; + + it('json', function() { + var json_test = Kalm.encoders.resolve('json'); + assert.isObject(json_test, 'json is not a valid encoder object'); + allMembersTypeMatch(json_test, encoderFormat); + + assert.instanceOf(json_test.encode(objTest), Buffer, 'json encoder does not output a buffer'); + assert.deepEqual(json_test.decode(json_test.encode(objTest)), objTest, 'Object is not the same after json decoding.'); + }); + + it('msg-pack', function() { + var msg_test = Kalm.encoders.resolve('msg-pack'); + assert.isObject(msg_test, 'msg-pack is not a valid encoder object'); + allMembersTypeMatch(msg_test, encoderFormat); + + assert.instanceOf(msg_test.encode(objTest), Buffer, 'msg-pack encoder does not output a buffer'); + assert.deepEqual(msg_test.decode(msg_test.encode(objTest)), objTest, 'Object is not the same after msg-pack decoding.'); + }); + }); + + describe('methods', function() { + it('register', function() { + Kalm.encoders.register('test', encoderFormat); + Kalm.encoders.register('test2', null); + }); + + it('resolve', function() { + assert.deepEqual(Kalm.encoders.resolve('test'), encoderFormat); + assert.equal(Kalm.encoders.resolve('test2'), null); + assert.equal(Kalm.encoders.resolve('test3'), null); + }); + }); +}); + +describe('Smoke test', function() { + var server; + var client; + + it('run ipc + json', function(done) { + server = new Kalm.Server({adapter:'ipc', encoder:'json'}); + server.channel('test', function(data) { + assert.deepEqual(data, {foo:'bar'}); + server.stop(done); + }); + + server.on('ready', function() { + client = new Kalm.Client({adapter:'ipc', encoder:'json'}); + client.send('test', {foo:'bar'}); + }); + }); + + it('run ipc + msg-pack', function(done) { + server = new Kalm.Server({adapter:'ipc', encoder: 'msg-pack'}); + server.channel('test', function(data) { + assert.deepEqual(data, {foo:'bar'}); + server.stop(done); }); server.on('ready', function() { - var client = new Kalm.Client({ - port: 3000, - adapter: 'ipc', - encoder:'msg-pack', - hostname: '0.0.0.0' - }); - client.send('test', 'data'); - client.send('test', 'data2'); + client = new Kalm.Client({adapter:'ipc', encoder: 'msg-pack'}); + client.send('test', {foo:'bar'}); + }); + }); - client.send('test2', 'data3'); - done(); + it('run tcp + json', function(done) { + server = new Kalm.Server({adapter:'tcp', encoder:'json'}); + server.channel('test', function(data) { + assert.deepEqual(data, {foo:'bar'}); + server.stop(done); }); - server.on('error', function(err) { - console.log('Server error:'); - console.log(err.stack); + server.on('ready', function() { + client = new Kalm.Client({adapter:'tcp', encoder:'json'}); + client.send('test', {foo:'bar'}); }); }); -}); \ No newline at end of file + + it('run tcp + msg-pack', function(done) { + server = new Kalm.Server({encoder: 'msg-pack', adapter:'tcp'}); + server.channel('test', function(data) { + assert.deepEqual(data, {foo:'bar'}); + server.stop(done); + }); + + server.on('ready', function() { + client = new Kalm.Client({encoder: 'msg-pack', adapter:'tcp'}); + client.send('test', {foo:'bar'}); + }); + }); + + it('run udp + json', function(done) { + server = new Kalm.Server({adapter:'udp', encoder:'json'}); + server.channel('test', function(data) { + assert.deepEqual(data, {foo:'bar'}); + server.stop(done); + }); + + server.on('ready', function() { + client = new Kalm.Client({adapter:'udp', encoder:'json'}); + client.send('test', {foo:'bar'}); + }); + }); + + it('run udp + msg-pack', function(done) { + server = new Kalm.Server({encoder: 'msg-pack', adapter:'udp'}); + server.channel('test', function(data) { + assert.deepEqual(data, {foo:'bar'}); + server.stop(done); + }); + + server.on('ready', function() { + client = new Kalm.Client({encoder: 'msg-pack', adapter:'udp'}); + client.send('test', {foo:'bar'}); + }); + }); +}); + +/* Tooling -------------------------------------------------------------------*/ + +/** + * Checks that all properties are present and of the proper type + */ +function allMembersTypeMatch(set1, model) { + for (var i in model) { + var type = typeof model[i]; + assert.property(set1, i, 'property ' + i + ' is missing'); + assert.typeOf(set1[i], type, 'property ' + i + ' should be ' + type); + } + return true; +} \ No newline at end of file diff --git a/tests/test.js b/tests/test.js deleted file mode 100644 index 30265ff..0000000 --- a/tests/test.js +++ /dev/null @@ -1,40 +0,0 @@ -var Kalm = require('../index'); - -var server = new Kalm.Server({ - port: 3000, - adapter: 'ipc', - encoder: 'msg-pack', - channels: { - channel1: function(data) { - console.log('GOT "' + data + '" on channel1!'); - }, - '/': function(data) { - console.log('GOT "' + data + '" on main channel!'); - } - } -}); - -server.on('connection', function(client) { - // Do stuff - client.send('greetings', 'Hi there!'); -}); - -server.on('ready', function() { - var client = new Kalm.Client({ - port: 3000, - adapter: 'ipc', - encoder:'msg-pack', - hostname: '0.0.0.0', - channels: { - greetings: function(data) { - console.log('Server [greetings]: ' + data); - } - } - }); - - client.send('channel1', 'some data'); - client.send('channel1', 'some more data'); - setTimeout(function() { - client.send(null, {message:'object'}); - }, 1000); -}); \ No newline at end of file diff --git a/webpack.config b/webpack.config new file mode 100644 index 0000000..d236a0c --- /dev/null +++ b/webpack.config @@ -0,0 +1,15 @@ +module.exports = { + entry: __dirname + '/index.js', + context: __dirname, + output: { + filename: 'kalm.min.js', + path: __dirname + '/bin', + library: 'kalm', + libraryTarget: 'amd' + }, + modules: { + loaders: [ + { test: /\.js$/, loader: 'val'} + ] + } +};