├── .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 |
--------------------------------------------------------------------------------