├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── example.js ├── index.js ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '6' 4 | - '8' 5 | - '10' 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Mathias Buus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # udp-request 2 | 3 | Small module for making requests/responses over UDP 4 | 5 | ``` 6 | npm install udp-request 7 | ``` 8 | 9 | [![build status](http://img.shields.io/travis/mafintosh/udp-request.svg?style=flat)](http://travis-ci.org/mafintosh/udp-request) 10 | 11 | ## Usage 12 | 13 | ``` js 14 | var udp = require('udp-request') 15 | var socket = udp() 16 | 17 | socket.on('request', function (request, peer) { 18 | console.log('request:', request.toString()) 19 | socket.response('echo: ' + request.toString(), peer) 20 | }) 21 | 22 | socket.listen(10000, function () { 23 | socket.request('hello', {port: 10000, host: '127.0.0.1'}, function (err, response) { 24 | console.log('response', response.toString()) 25 | socket.destroy() 26 | }) 27 | }) 28 | ``` 29 | 30 | ## API 31 | 32 | #### `var socket = udp([options])` 33 | 34 | Create a new request/response udp socket. Options include: 35 | 36 | ``` js 37 | { 38 | timeout: 1000, // request timeout 39 | socket: udpSocket, // supply your own udp socket 40 | retry: true, // retry requests if they time out. defaults to false 41 | requestEncoding: someEncoder, // abstract-encoding compliant encoder 42 | responseEncoding: someEncoder, // abstract-encoding compliant encoder 43 | } 44 | ``` 45 | 46 | #### `var id = socket.request(buffer, peer, [options], [callback])` 47 | 48 | Send a new request. `buffer` is the request payload and `peer` should be an object containing `{port, host}`. 49 | When the response arrives (or the request times out) the callback is called with the following arguments 50 | 51 | ``` js 52 | callback(error, response, peer) 53 | ``` 54 | 55 | Options include: 56 | 57 | ``` js 58 | { 59 | retry: true 60 | } 61 | ``` 62 | 63 | #### `socket.response(buffer, peer)` 64 | 65 | Send a response back to a request. 66 | 67 | #### `socket.cancel(id)` 68 | 69 | Cancels a pending request. 70 | 71 | #### `socket.on('request', buffer, peer)` 72 | 73 | Emitted when a new request arrives. Call the above `.response` with the same peer object to send a response back to this request. 74 | 75 | #### `socket.on('response', buffer, peer)` 76 | 77 | Emitted when any response arrives. 78 | 79 | #### `socket.on('error', err)` 80 | 81 | Emitted when a critical error happens. 82 | 83 | #### `socket.on('warning', err)` 84 | 85 | Emitted when a non critical error happens (you usually do not need to listen for this). 86 | 87 | #### `socket.on('close')` 88 | 89 | Emitted when the request socket closes (after it is destroyed). 90 | 91 | #### `socket.on('listening')` 92 | 93 | Emitted when the socket is listening. 94 | 95 | #### `socket.listen([port], [callback])` 96 | 97 | Listen on a specific port. If port is omitted a random one will be used. 98 | 99 | #### `socket.destroy()` 100 | 101 | Completely destroy the request socket (cancels all pending requests). 102 | 103 | ## License 104 | 105 | MIT 106 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | var udp = require('udp-request') 2 | var socket = udp() 3 | 4 | socket.on('request', function (request, peer) { 5 | console.log('request:', request.toString()) 6 | socket.response('echo: ' + request.toString(), peer) 7 | }) 8 | 9 | socket.listen(10000, function () { 10 | socket.request('hello', { port: 10000, host: '127.0.0.1' }, function (err, response) { 11 | if (err) throw err 12 | console.log('response', response.toString()) 13 | socket.destroy() 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const dgram = require('dgram') 2 | const { EventEmitter } = require('events') 3 | const passthrough = require('passthrough-encoding') 4 | 5 | const ETIMEDOUT = new Error('Request timed out') 6 | ETIMEDOUT.timeout = true 7 | ETIMEDOUT.code = 'ETIMEDOUT' 8 | 9 | const RETRIES = [4, 8, 12] 10 | 11 | module.exports = opts => new UDPRequest(opts) 12 | 13 | class UDPRequest extends EventEmitter { 14 | constructor (opts) { 15 | if (!opts) opts = {} 16 | 17 | super() 18 | 19 | const timeout = Math.ceil(opts.timeout || 1000, 4) 20 | 21 | this.socket = opts.socket || dgram.createSocket('udp4') 22 | this.retry = !!opts.retry 23 | this.inflight = 0 24 | this.requestEncoding = opts.requestEncoding || opts.encoding || passthrough 25 | this.responseEncoding = opts.responseEncoding || opts.encoding || passthrough 26 | this.destroyed = false 27 | 28 | this._tick = (Math.random() * 32767) | 0 29 | this._tids = [] 30 | this._reqs = [] 31 | this._interval = setInterval(this._checkTimeouts.bind(this), Math.floor(timeout / 4)) 32 | 33 | this.socket.on('error', this._onerror.bind(this)) 34 | this.socket.on('message', this._onmessage.bind(this)) 35 | this.socket.on('listening', this.emit.bind(this, 'listening')) 36 | this.socket.on('close', this.emit.bind(this, 'close')) 37 | } 38 | 39 | address () { 40 | return this.socket.address() 41 | } 42 | 43 | listen (port, cb) { 44 | if (typeof port === 'function') return this.listen(0, port) 45 | if (!port) port = 0 46 | this.socket.bind(port, cb) 47 | } 48 | 49 | request (val, peer, opts, cb) { 50 | if (typeof opts === 'function') return this._request(val, peer, {}, opts) 51 | return this._request(val, peer, opts || {}, cb || noop) 52 | } 53 | 54 | _request (val, peer, opts, cb) { 55 | if (this.destroyed) return cb(new Error('Request cancelled')) 56 | if (this._tick === 32767) this._tick = 0 57 | 58 | const tid = this._tick++ 59 | const header = 32768 | tid 60 | const message = Buffer.allocUnsafe(this.requestEncoding.encodingLength(val) + 2) 61 | 62 | message.writeUInt16BE(header, 0) 63 | this.requestEncoding.encode(val, message, 2) 64 | 65 | this._push(tid, val, message, peer, opts, cb) 66 | this.socket.send(message, 0, message.length, peer.port, peer.host) 67 | 68 | return tid 69 | } 70 | 71 | forwardRequest (val, from, to) { 72 | this._forward(true, val, from, to) 73 | } 74 | 75 | forwardResponse (val, from, to) { 76 | this._forward(false, val, from, to) 77 | } 78 | 79 | _forward (request, val, from, to) { 80 | if (this.destroyed) return 81 | 82 | const enc = request ? this.requestEncoding : this.responseEncoding 83 | const message = Buffer.allocUnsafe(enc.encodingLength(val) + 2) 84 | const header = (request ? 32768 : 0) | from.tid 85 | 86 | message.writeUInt16BE(header, 0) 87 | enc.encode(val, message, 2) 88 | 89 | this.socket.send(message, 0, message.length, to.port, to.host) 90 | } 91 | 92 | response (val, peer) { 93 | if (this.destroyed) return 94 | 95 | const message = Buffer.allocUnsafe(this.responseEncoding.encodingLength(val) + 2) 96 | 97 | message.writeUInt16BE(peer.tid, 0) 98 | this.responseEncoding.encode(val, message, 2) 99 | 100 | this.socket.send(message, 0, message.length, peer.port, peer.host) 101 | } 102 | 103 | destroy (err) { 104 | if (this.destroyed) return 105 | this.destroyed = true 106 | 107 | clearInterval(this._interval) 108 | this.socket.close() 109 | for (var i = 0; i < this._reqs.length; i++) { 110 | if (this._reqs[i]) this._cancel(i, err) 111 | } 112 | } 113 | 114 | cancel (tid, err) { 115 | const i = this._tids.indexOf(tid) 116 | if (i > -1) this._cancel(i, err) 117 | } 118 | 119 | _cancel (i, err) { 120 | const req = this._reqs[i] 121 | this._tids[i] = -1 122 | this._reqs[i] = null 123 | this.inflight-- 124 | req.callback(err || new Error('Request cancelled'), null, req.peer, req.request) 125 | } 126 | 127 | _onmessage (message, rinfo) { 128 | if (this.destroyed) return 129 | 130 | const request = !!(message[0] & 128) 131 | const tid = message.readUInt16BE(0) & 32767 132 | const enc = request ? this.requestEncoding : this.responseEncoding 133 | 134 | try { 135 | var value = enc.decode(message, 2) 136 | } catch (err) { 137 | this.emit('warning', err) 138 | return 139 | } 140 | 141 | const peer = { 142 | port: rinfo.port, 143 | host: rinfo.address, 144 | tid, 145 | request 146 | } 147 | 148 | if (request) { 149 | this.emit('request', value, peer) 150 | return 151 | } 152 | 153 | const state = this._pull(tid) 154 | 155 | this.emit('response', value, peer, state && state.request) 156 | if (state) state.callback(null, value, peer, state.request, state.peer) 157 | } 158 | 159 | _checkTimeouts () { 160 | for (var i = 0; i < this._reqs.length; i++) { 161 | const req = this._reqs[i] 162 | if (!req) continue 163 | 164 | if (req.timeout) { 165 | req.timeout-- 166 | continue 167 | } 168 | if (req.tries < RETRIES.length) { 169 | req.timeout = RETRIES[req.tries++] 170 | this.socket.send(req.buffer, 0, req.buffer.length, req.peer.port, req.peer.host) 171 | continue 172 | } 173 | 174 | this._cancel(i, ETIMEDOUT) 175 | } 176 | } 177 | 178 | _pull (tid) { 179 | const free = this._tids.indexOf(tid) 180 | if (free === -1) return null 181 | 182 | const req = this._reqs[free] 183 | this._reqs[free] = null 184 | this._tids[free] = -1 185 | 186 | this.inflight-- 187 | 188 | return req 189 | } 190 | 191 | _push (tid, req, buf, peer, opts, cb) { 192 | const retry = opts.retry !== undefined ? opts.retry : this.retry 193 | var free = this._tids.indexOf(-1) 194 | if (free === -1) { 195 | this._reqs.push(null) 196 | free = this._tids.push(-1) - 1 197 | } 198 | 199 | this.inflight++ 200 | 201 | this._tids[free] = tid 202 | this._reqs[free] = { 203 | callback: cb || noop, 204 | request: req, 205 | peer: peer, 206 | buffer: buf, 207 | timeout: 5, 208 | tries: retry ? 0 : RETRIES.length 209 | } 210 | } 211 | 212 | _onerror (err) { 213 | if (err.code === 'EADDRINUSE' || err.code === 'EPERM' || err.code === 'EACCES') this.emit('error', err) 214 | else this.emit('warning', err) 215 | } 216 | } 217 | 218 | function noop () {} 219 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "udp-request", 3 | "version": "2.0.0", 4 | "description": "Small module for making requests/responses over UDP", 5 | "main": "index.js", 6 | "dependencies": { 7 | "passthrough-encoding": "^1.2.0" 8 | }, 9 | "devDependencies": { 10 | "standard": "^12.0.1", 11 | "tape": "^4.9.1" 12 | }, 13 | "scripts": { 14 | "test": "standard && tape test.js" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/mafintosh/udp-request.git" 19 | }, 20 | "author": "Mathias Buus (@mafintosh)", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/mafintosh/udp-request/issues" 24 | }, 25 | "homepage": "https://github.com/mafintosh/udp-request" 26 | } 27 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape') 2 | var udp = require('./') 3 | 4 | tape('request', function (t) { 5 | var socket = udp() 6 | 7 | socket.on('request', function (value, peer) { 8 | socket.response(value, peer) 9 | }) 10 | 11 | socket.listen(0, function () { 12 | socket.request('hello', { port: socket.address().port, host: '127.0.0.1' }, function (err, echo) { 13 | socket.destroy() 14 | t.error(err, 'no error') 15 | t.same(echo, Buffer.from('hello'), 'echoed data') 16 | t.end() 17 | }) 18 | }) 19 | }) 20 | 21 | tape('multiple request', function (t) { 22 | t.plan(4) 23 | 24 | var socket = udp() 25 | 26 | socket.on('request', function (value, peer) { 27 | socket.response(value, peer) 28 | }) 29 | 30 | socket.listen(0, function () { 31 | socket.request('hello', { port: socket.address().port, host: '127.0.0.1' }, function (err, echo) { 32 | if (!socket.inflight) socket.destroy() 33 | t.error(err, 'no error') 34 | t.same(echo, Buffer.from('hello'), 'echoed data') 35 | }) 36 | 37 | socket.request('hello', { port: socket.address().port, host: '127.0.0.1' }, function (err, echo) { 38 | if (!socket.inflight) socket.destroy() 39 | t.error(err, 'no error') 40 | t.same(echo, Buffer.from('hello'), 'echoed data') 41 | }) 42 | }) 43 | }) 44 | 45 | tape('timeout', function (t) { 46 | var socket = udp() 47 | 48 | socket.listen(0, function () { 49 | socket.request('hello', { port: socket.address().port, host: '127.0.0.1' }, function (err, echo) { 50 | socket.destroy() 51 | t.ok(err, 'had timeout') 52 | t.end() 53 | }) 54 | }) 55 | }) 56 | 57 | tape('cancel', function (t) { 58 | var socket = udp() 59 | 60 | socket.listen(0, function () { 61 | var tid = socket.request('hello', { port: socket.address().port, host: '127.0.0.1' }, function (err, echo) { 62 | socket.destroy() 63 | t.ok(err, 'was cancelled') 64 | t.end() 65 | }) 66 | 67 | socket.cancel(tid) 68 | }) 69 | }) 70 | 71 | tape('forward', function (t) { 72 | var socket = udp() 73 | var a = udp() 74 | var b = udp() 75 | 76 | a.listen() 77 | b.listen() 78 | 79 | socket.listen(0, function () { 80 | socket.on('request', function (request, peer) { 81 | socket.forwardRequest(request, peer, { port: b.address().port, host: '127.0.0.1' }) 82 | }) 83 | 84 | a.request('echo me', { port: socket.address().port, host: '127.0.0.1' }, function (err, echo) { 85 | a.destroy() 86 | b.destroy() 87 | socket.destroy() 88 | t.error(err, 'no error') 89 | t.same(echo, Buffer.from('echo me'), 'echoed data') 90 | t.end() 91 | }) 92 | }) 93 | 94 | b.on('request', function (request, peer) { 95 | b.forwardResponse(request, peer, { port: a.address().port, host: '127.0.0.1' }) 96 | }) 97 | }) 98 | --------------------------------------------------------------------------------