├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── example.js ├── index.js ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | chat 30 | chat.lock 31 | testing* 32 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | - "0.12" 5 | - "4.0" 6 | - "4.1" 7 | - "4.2" 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Matteo Collina 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 all 13 | 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 THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # unix-socket-leader 2 | [![travis][travis-badge]][travis-url] 3 | [![git][git-badge]][git-url] 4 | [![npm][npm-badge]][npm-url] 5 | 6 | Elect a leader using unix sockets, inspired by 7 | [level-party](http://npm.im/level-party) and a late night conversation 8 | with [@mafitonsh](http://github.com/mafintosh) at 9 | [nodejsconf.it](http://nodejsconf.it). 10 | 11 | * [Install](#install) 12 | * [Example](#example) 13 | * [API](#api) 14 | * [License](#license) 15 | 16 | 17 | ## Install 18 | To install unix-socket-leader, simply use npm: 19 | 20 | ``` 21 | npm install unix-socket-leader --save 22 | ``` 23 | 24 | 25 | ## Example 26 | 27 | The example below can be found [here][example] and ran using `node example.js`. It 28 | demonstrates how to use unix-socket-leader to build a simple chat room. 29 | 30 | ```js 31 | 'use strict' 32 | 33 | var leader = require('unix-socket-leader')('chat') 34 | var eos = require('end-of-stream') 35 | var sockets = [] 36 | var popts = { end: false } 37 | 38 | leader.on('leader', function () { 39 | console.log('!! I am the leader now', process.pid) 40 | }) 41 | 42 | leader.on('connection', function (sock) { 43 | sock.write('!! connected to ' + process.pid) 44 | sock.write('\n') 45 | 46 | sockets.forEach(function (other) { 47 | other.pipe(sock, popts).pipe(other, popts) 48 | }) 49 | 50 | sockets.push(sock) 51 | 52 | eos(sock, function () { 53 | sockets.splice(sockets.indexOf(sock), 1) 54 | }) 55 | }) 56 | 57 | leader.on('client', function (sock) { 58 | process.stdout.pipe(sock, popts).pipe(process.stdout, popts) 59 | }) 60 | ``` 61 | 62 | 63 | ## API 64 | 65 | * leader() 66 | * instance.close() 67 | 68 | ------------------------------------------------------- 69 | 70 | ### leader(name) 71 | 72 | Creates a new instance of unix-socket-leader. 73 | 74 | Events: 75 | 76 | * `leader`, emitted when this instance is elected leader 77 | * `client`, emitted when this instance is connected to a leader (even 78 | itself); the first argument is the connected socket 79 | * `connection`, emitted when there is a new incoming connection, and 80 | this instance is the leader; the first argument is the connected socket 81 | 82 | ------------------------------------------------------- 83 | 84 | ### instance.close([cb]) 85 | 86 | Closes the instance, severing all current connections. 87 | 88 | ## License 89 | 90 | Copyright Matteo Collina 2015, Licensed under [MIT][]. 91 | 92 | [MIT]: ./LICENSE 93 | [example]: ./example.js 94 | 95 | [travis-badge]: https://img.shields.io/travis/mcollina/unix-socket-leader.svg?style=flat-square 96 | [travis-url]: https://travis-ci.org/mcollina/unix-socket-leader 97 | [git-badge]: https://img.shields.io/github/release/mcollina/unix-socket-leader.svg?style=flat-square 98 | [git-url]: https://github.com/mcollina/unix-socket-leader/releases 99 | [npm-badge]: https://img.shields.io/npm/v/unix-socket-leader.svg?style=flat-square 100 | [npm-url]: https://npmjs.org/package/unix-socket-leader 101 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var leader = require('./')('chat') 4 | var eos = require('end-of-stream') 5 | var sockets = [] 6 | var popts = { end: false } 7 | 8 | leader.on('leader', function () { 9 | console.log('!! I am the the leader now', process.pid) 10 | }) 11 | 12 | leader.on('connection', function (sock) { 13 | sock.write('!! connected to ' + process.pid) 14 | sock.write('\n') 15 | 16 | sockets.forEach(function (other) { 17 | other.pipe(sock, popts).pipe(other, popts) 18 | }) 19 | 20 | sockets.push(sock) 21 | 22 | eos(sock, function () { 23 | sockets.splice(sockets.indexOf(sock), 1) 24 | }) 25 | }) 26 | 27 | leader.on('client', function (sock) { 28 | process.stdout.pipe(sock, popts).pipe(process.stdout, popts) 29 | }) 30 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var net = require('net') 4 | var fs = require('fs') 5 | var path = require('path') 6 | var EE = require('events').EventEmitter 7 | var eos = require('end-of-stream') 8 | var lockfile = require('pidlockfile') 9 | 10 | function leader (file) { 11 | var that = new EE() 12 | var sockPath = path.resolve(process.cwd(), file) 13 | var lock = sockPath + '.lock' 14 | var client = null 15 | var server = null 16 | var sockets = [] 17 | var closed = false 18 | 19 | if (process.platform === 'win32') { 20 | sockPath = '\\\\' + sockPath 21 | } 22 | 23 | that.close = close 24 | 25 | tryConnect() 26 | 27 | return that 28 | 29 | function tryConnect () { 30 | if (closed) return 31 | 32 | var client = net.connect(sockPath) 33 | var removeEos 34 | 35 | client.on('error', function (err) { 36 | client = null 37 | 38 | if (removeEos) { 39 | removeEos() 40 | } 41 | 42 | if (err.code === 'ECONNREFUSED' || err.code === 'ENOENT') { 43 | return setTimeout(unlinkAndStart, 50 + Math.random() * 100) 44 | } 45 | 46 | return that.emit('error', err) 47 | }) 48 | 49 | client.on('connect', function () { 50 | that.emit('client', client) 51 | 52 | removeEos = eos(client, tryConnect) 53 | }) 54 | } 55 | 56 | function unlinkAndStart () { 57 | if (closed) return 58 | 59 | lockfile.lock(lock, function (err) { 60 | if (err) { 61 | return tryConnect() 62 | } 63 | 64 | fs.unlink(sockPath, function () { 65 | startServer() 66 | }) 67 | }) 68 | } 69 | 70 | function startServer () { 71 | server = net.createServer(function (sock) { 72 | sock.unref() 73 | sockets.push(sock) 74 | that.emit('connection', sock) 75 | eos(sock, function () { 76 | sockets.splice(sockets.indexOf(sock), 1) 77 | }) 78 | }) 79 | 80 | server.listen(sockPath, function () { 81 | that.emit('leader') 82 | tryConnect() 83 | }) 84 | 85 | server.on('error', tryConnect) 86 | 87 | server.unref() 88 | } 89 | 90 | function close (cb) { 91 | closed = true 92 | if (server) { 93 | try { 94 | fs.unlinkSync(sockPath) 95 | fs.unlinkSync(lock) 96 | } catch (err) { 97 | // somebody else unlinked the locks 98 | } 99 | sockets.forEach(function (sock) { 100 | sock.destroy() 101 | }) 102 | server.close(cb) 103 | } else if (client) { 104 | if (cb) { 105 | eos(client, cb) 106 | } 107 | client.destroy() 108 | } else { 109 | cb() 110 | } 111 | } 112 | } 113 | 114 | module.exports = leader 115 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unix-socket-leader", 3 | "version": "0.1.2", 4 | "description": "Elect a leader using unix sockets", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "standard && tape test.js | tap-spec" 8 | }, 9 | "precommit": "test", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/mcollina/unix-socket-leader.git" 13 | }, 14 | "keywords": [ 15 | "unix", 16 | "socket", 17 | "leader", 18 | "net" 19 | ], 20 | "author": "Matteo Collina ", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/mcollina/unix-socket-leader/issues" 24 | }, 25 | "homepage": "https://github.com/mcollina/unix-socket-leader#readme", 26 | "devDependencies": { 27 | "pre-commit": "^1.1.1", 28 | "split2": "^1.0.0", 29 | "standard": "^5.3.1", 30 | "tap-spec": "^4.1.0", 31 | "tape": "^4.2.1" 32 | }, 33 | "dependencies": { 34 | "end-of-stream": "^1.1.0", 35 | "pidlockfile": "^1.1.1" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var leader = require('./') 4 | var test = require('tape') 5 | var split = require('split2') 6 | var count = 0 7 | 8 | function testName () { 9 | return './testing-' + process.pid + '-' + count++ 10 | } 11 | 12 | function noError (t) { 13 | return function (err) { 14 | t.error(err, 'no error') 15 | } 16 | } 17 | 18 | test('single instance', function (t) { 19 | t.plan(4) 20 | 21 | var elected = false 22 | var instance = leader(testName()) 23 | 24 | instance.on('leader', function () { 25 | t.pass('elected as leader') 26 | elected = true 27 | }) 28 | 29 | instance.on('connection', function (sock) { 30 | // echo 31 | sock.pipe(sock) 32 | }) 33 | 34 | // client to itself 35 | instance.on('client', function (sock) { 36 | sock.write('hello world\n') 37 | t.ok(elected, 'a leader was elected first') 38 | sock.pipe(split()).on('data', function (line) { 39 | t.equal(line, 'hello world') 40 | instance.close(noError(t)) 41 | }) 42 | }) 43 | }) 44 | 45 | test('two instances', function (t) { 46 | t.plan(6) 47 | 48 | var name = testName() 49 | 50 | var instance1 = leader(name) 51 | 52 | instance1.on('leader', function () { 53 | t.pass('leader started') 54 | 55 | var instance2 = leader(name) 56 | 57 | instance2.on('leader', noLeader) 58 | instance2.once('client', function (sock) { 59 | sock.pipe(split()).once('data', function (line) { 60 | t.equal(line, 'hello world') 61 | instance2.removeListener('leader', noLeader) 62 | instance2.on('leader', function () { 63 | t.pass('second leader elected') 64 | }) 65 | instance2.once('client', function (sock) { 66 | t.pass('second leader connect to itself') 67 | instance2.close(noError(t)) 68 | }) 69 | instance1.close(noError(t)) 70 | }) 71 | }) 72 | }) 73 | 74 | instance1.on('connection', function (sock) { 75 | sock.write('hello world\n') 76 | }) 77 | 78 | function noLeader () { 79 | t.fail('the instance should not be elected leader') 80 | } 81 | }) 82 | --------------------------------------------------------------------------------