├── _config.yml ├── package.json ├── docs ├── ADVANCED.md ├── USAGE.md └── API.md ├── LICENSE ├── .gitignore ├── examples ├── resilience │ ├── client.js │ └── server.js ├── spam │ ├── server.js │ └── client.js └── conversation.js ├── src ├── lib │ ├── transcoder │ │ └── jsonTranscoder.js │ └── error.js └── index.js └── README.md /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@crussell52/socket-ipc", 3 | "version": "0.4.1", 4 | "description": "An event-driven IPC implementation using unix file sockets.", 5 | "keywords": [ 6 | "ipc", 7 | "interprocess", 8 | "messages", 9 | "communication", 10 | "sockets", 11 | "event", 12 | "unix", 13 | "mac", 14 | "linux" 15 | ], 16 | "homepage": "https://crussell52.github.io/node-socket-ipc/", 17 | "bugs": { 18 | "url": "https://github.com/crussell52/node-socket-ipc/issues" 19 | }, 20 | "repository": "github:crussell52/node-socket-ipc", 21 | "main": "src/index.js", 22 | "scripts": { 23 | "test": "echo \"Error: no test specified\" && exit 1" 24 | }, 25 | "author": "Chris Russell ", 26 | "license": "MIT", 27 | "engines": { 28 | "node": ">=8.0.0" 29 | }, 30 | "dependencies": { 31 | "uuid": "^3.4.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /docs/ADVANCED.md: -------------------------------------------------------------------------------- 1 | # Advanced Usage 2 | 3 | For the most obvious use cases, you probably don't need this stuff. But these are the things you might find useful 4 | when things get... _interesting_. 5 | 6 | ## Compatible Libraries 7 | 8 | Compatible servers or clients can be created in other languages. The implementation simply needs to 9 | read and write compatible messages using a unix domain socket. (Server implementations must also 10 | establish the socket). 11 | 12 | The details of the message are determined by the transcoder. 13 | 14 | ### Message Format: JSON Decoder 15 | 16 | The default transcoder writes messages encoded as a JSON string. The message structure is as follows. 17 | ``` 18 | { 19 | "topic": string, 20 | "message": * 21 | } 22 | ``` 23 | 24 | The message is serialized as a string terminated by a two null-byte (`\0`) characters. For example, 25 | a message with the `'hello` as its topic and `world` as its message would be put on the line as: 26 | 27 | ``` 28 | {"topic":"hello","message":"world"}\0\0 29 | ``` 30 | 31 | ## Custom Encoding and Decoding 32 | 33 | ## Throttling Messages -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Chris Russell 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # Intellij 64 | .idea/ 65 | *.iml 66 | -------------------------------------------------------------------------------- /examples/resilience/client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides a simple client which makes a lot of noise about the server state. 3 | * 4 | * Run one or more copies of in conjunction with `server.js` in the same directory to 5 | * see how the client deals with an unreliable server. 6 | * 7 | * Hint: Don't forget to define the SOCKET_FILE before running! 8 | * 9 | * @author Chris Russell 10 | * @copyright Chris Russell 2018 11 | * @license MIT 12 | */ 13 | 14 | const {Client} = require('../../src/index'); 15 | 16 | const SOCKET_FILE = undefined; 17 | 18 | const client = new Client({ 19 | socketFile: SOCKET_FILE, 20 | retryDelay: {min: 100, max: 1000}, 21 | reconnectDelay: {min: 5000, max: 10000} 22 | }); 23 | 24 | client.on('connectError', () => console.log('no server')); 25 | client.on('connect', () => console.log('connected to server')); 26 | client.on('disconnect', () => console.log('disconnected from server')); 27 | client.on('reconnect', () => console.log('reconnected to server')); 28 | client.on('message', (message, topic) => console.log(`Heard: [${topic}]`, message)); 29 | client.connect(); 30 | 31 | 32 | function shutdown(reason) { 33 | // Stop all processing and let node naturally exit. 34 | console.log('shutting down: ', reason); 35 | client.close(); 36 | } 37 | 38 | process.on('SIGTERM', () => shutdown('sigterm')); 39 | process.on('SIGINT', () => shutdown('sigint')); 40 | -------------------------------------------------------------------------------- /examples/spam/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides a simple client which makes a lot of noise about the server state. 3 | * 4 | * Run one or more copies of in conjunction with `server.js` in the same directory to 5 | * see how the client deals with an unreliable server. 6 | * 7 | * Hint: Don't forget to define the SOCKET_FILE before running! 8 | * 9 | * @author Chris Russell 10 | * @copyright Chris Russell 2018 11 | * @license MIT 12 | */ 13 | 14 | const {Server} = require('../../src/index'); 15 | 16 | const SOCKET_FILE = undefined; 17 | 18 | const server = new Server({socketFile: SOCKET_FILE}); 19 | server.on('listen', () => { 20 | console.log('listening'); 21 | }); 22 | 23 | 24 | let msgCount = 0; 25 | let clientCount = 0; 26 | server.on('connection', (id, socket) => { 27 | clientCount++; 28 | socket.on('close', () => clientCount--); 29 | }); 30 | server.on('message', (message) => msgCount++); 31 | let statusInterval = setInterval(() => { 32 | console.log(`~${msgCount} messages in last 1s from ${clientCount} clients.`); 33 | msgCount = 0; 34 | }, 1000); 35 | server.listen(); 36 | 37 | 38 | function shutdown(reason) { 39 | // Stop all processing and let node naturally exit. 40 | console.log('shutting down: ', reason); 41 | clearInterval(statusInterval); 42 | server.close(); 43 | } 44 | 45 | process.on('SIGTERM', () => shutdown('sigterm')); 46 | process.on('SIGINT', () => shutdown('sigint')); -------------------------------------------------------------------------------- /docs/USAGE.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | General knowledge for the simplest use cases. 4 | 5 | ## A Note on Safety 6 | 7 | Unix socket files exist on the file system. This library does not provide any special handling of their 8 | creation; it leaves that up to the expert: the [NodeJs net module](https://nodejs.org/api/net.html). In fact, 9 | that page has a section dedicated to Node's [IPC support](https://nodejs.org/api/net.html#net_ipc_support) 10 | that you should probably read, if you are not already famliiar with it. 11 | 12 | Because they are files, they are subject to permissions. Make sure you understand how those permissions work 13 | for sockets on your target OS. Use appropriate caution to not expose your application's messages to unintended 14 | audiences **or expose your application to messages from unintended clients!**. 15 | 16 | Socket files are very commonly used. You could use this library to tap into any socket file that your process has 17 | access to! That could be _very_ interesting... but it could also be hazardous. In particular, the `Server` will 18 | try to use _any_ socket file you tell it to -- even if that socket file is normally used by another service. Now, it 19 | can't "hijack" a socket file that another server is actively using, but if you occupy it, the other service may fail 20 | to start or its client's may think you are their server and start sending unexpected data! 21 | 22 | The details of how socket files work and the traps that might lurk in the shadows are **far** beyond the scope of this 23 | module's documentation. Like any good module, `socket-ipc` tries to hide this complexity from you and get you up 24 | and running fast. But if this is your first time stepping into this territory, it might still be worth the effort to 25 | learn a bit about them. 26 | 27 | ## Automatic Retry 28 | 29 | ## Automatic Reconnect 30 | 31 | ## Working with client ids -------------------------------------------------------------------------------- /src/lib/transcoder/jsonTranscoder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Chris Russell 3 | * @copyright Chris Russell 2018 4 | * @license MIT 5 | */ 6 | 7 | 'use strict'; 8 | 9 | const {DecodeError, EncodeError} = require('../error'); 10 | const delimiter = "\0\0"; 11 | 12 | module.exports = { 13 | socketEncoding: 'utf8', 14 | 15 | createEncoder: () => { 16 | // Return an encoder function. 17 | return (msgWrapper, cb) => { 18 | try { 19 | cb(null, JSON.stringify(msgWrapper) + delimiter); 20 | } catch (err) { 21 | cb(new EncodeError('Failed to encode, caused by: ' + err.message, msgWrapper)); 22 | } 23 | }; 24 | }, 25 | 26 | createDecoder: () => { 27 | // Each decoder gets it own buffer. 28 | let buffer = ''; 29 | 30 | // Return an encoder function. 31 | return (chunk, cb) => { 32 | 33 | // Use the buffer plus this chunk as the data that we need to process. 34 | let data = buffer += chunk; 35 | 36 | // Split on the delimiter to find distinct and complete messages. 37 | let rawMessages = data.split(delimiter); 38 | 39 | // Pop the last element off of the message array. It is either an incomplete message 40 | // or an empty string. Use it as the new buffer value. 41 | buffer = rawMessages.pop(); 42 | 43 | // Build out the list of decoded messages. 44 | const messages = []; 45 | for (let i = 0; i < rawMessages.length; i++) { 46 | try { 47 | messages.push(JSON.parse(rawMessages[i])); 48 | } catch (err) { 49 | // Invoke the callback with a DecodeError and stop processing. 50 | cb(new DecodeError('Failed to decode, caused by: ' + err.message, rawMessages[i])); 51 | return; 52 | } 53 | } 54 | 55 | cb(null, messages); 56 | } 57 | } 58 | }; -------------------------------------------------------------------------------- /examples/spam/client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides a simple client which makes a lot of noise about the server state. 3 | * 4 | * Run one or more copies of in conjunction with `server.js` in the same directory to 5 | * see how the client deals with an unreliable server. 6 | * 7 | * Hint: Don't forget to define the SOCKET_FILE before running! 8 | * 9 | * @author Chris Russell 10 | * @copyright Chris Russell 2018 11 | * @license MIT 12 | */ 13 | 14 | const {Client} = require('../../src/index'); 15 | 16 | const SOCKET_FILE = undefined; 17 | 18 | 19 | let spamInterval; 20 | function spam() { 21 | spamInterval = setInterval(() => { 22 | for (let i = 0; i < 300; i++) { 23 | client.send('hello', i + ':' + data[Math.floor(Math.random() * Math.floor(999))]); 24 | } 25 | }, 5); 26 | } 27 | 28 | 29 | function generate_random_data1(size){ 30 | let chars = 'abcdefghijklmnopqrstuvwxyz'.split(''); 31 | let len = chars.length; 32 | let random_data = []; 33 | 34 | while (size--) { 35 | random_data.push(chars[Math.random()*len | 0]); 36 | } 37 | 38 | return random_data.join(''); 39 | } 40 | 41 | let data = []; 42 | for (let i = 0; i < 1000; i++) { 43 | data.push(generate_random_data1(32)); 44 | } 45 | 46 | 47 | const client = new Client({socketFile: SOCKET_FILE}); 48 | client.on('connect', (socket) => { 49 | socket.on('close', () => { 50 | console.log('Server went away (close).'); 51 | clearInterval(spamInterval); 52 | }); 53 | 54 | socket.on('end', () => { 55 | console.log('Server went away.'); 56 | clearInterval(spamInterval); 57 | }); 58 | 59 | console.log('connected to server'); 60 | spam() 61 | }); 62 | 63 | client.on('error', (e) => { 64 | console.log(e); 65 | }); 66 | 67 | client.connect(); 68 | 69 | function shutdown(reason) { 70 | // Stop all processing and let node naturally exit. 71 | console.log('shutting down: ', reason); 72 | clearInterval(spamInterval); 73 | client.close(); 74 | } 75 | 76 | process.on('SIGTERM', () => shutdown('sigterm')); 77 | process.on('SIGINT', () => shutdown('sigint')); 78 | -------------------------------------------------------------------------------- /src/lib/error.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Chris Russell 3 | * @copyright Chris Russell 2018 4 | * @license MIT 5 | */ 6 | 7 | 'use strict'; 8 | 9 | class DecodeError extends Error { 10 | /** 11 | * @param errorMessage - A description of what happened. 12 | * @param {*} rawData - The data which failed to decode. 13 | */ 14 | constructor(errorMessage, rawData) { 15 | super(errorMessage); 16 | this.rawData = rawData; 17 | this.clientId = clientId; 18 | } 19 | } 20 | 21 | class SendError extends Error { 22 | /** 23 | * @param {string} errorMessage - A description of what went wrong. 24 | * @param {*} message - The message being sent. 25 | * @param {string} topic - The topic of the message being sent. 26 | */ 27 | constructor(errorMessage, message, topic) { 28 | super(errorMessage); 29 | this.sentTopic = topic; 30 | this.sentMessage = message; 31 | } 32 | } 33 | 34 | class SendAfterCloseError extends SendError { } 35 | class NoServerError extends SendError { } 36 | 37 | class BadClientError extends SendError { 38 | /** 39 | * @param {string} errorMessage - A description of what went wrong. 40 | * @param {*} message - The message being sent. 41 | * @param {string} topic - The topic of the message being sent. 42 | * @param {string} clientId - The client id which is invalid. 43 | */ 44 | constructor(errorMessage, message, topic, clientId) { 45 | super(errorMessage, message, topic); 46 | this.clientId = clientId; 47 | } 48 | } 49 | 50 | /** 51 | * Indicates that an error happened during the encoding phase of sending a message. 52 | */ 53 | class EncodeError extends SendError { 54 | /** 55 | * @param {string} errorMessage - A description of what went wrong. 56 | * @param {MessageWrapper} msgWrapper - The message being sent and its topic. 57 | */ 58 | constructor(errorMessage, msgWrapper) { 59 | super(errorMessage, msgWrapper.message, msgWrapper.topic); 60 | } 61 | } 62 | 63 | module.exports = { 64 | EncodeError, DecodeError, SendError, SendAfterCloseError, NoServerError, BadClientError 65 | }; -------------------------------------------------------------------------------- /examples/resilience/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file provides an IPC server with a TERRIBLE uptime record. 3 | * 4 | * It says "hello" to every client that connects but always closes 10 seconds 5 | * after its first "hello". 6 | * 7 | * 10 seconds after the server closes, a new one spins up to replace it. 8 | * 9 | * Run one copy of this file in conjuction with `client.js` from the same directory 10 | * to see a demonstration of client resliency. 11 | * 12 | * Hint: Don't forget to define the SOCKET_FILE before running! 13 | * 14 | * @author Chris Russell 15 | * @copyright Chris Russell 2018 16 | * @license MIT 17 | */ 18 | const {Server} = require('../../src/index'); 19 | 20 | const SOCKET_FILE = undefined; 21 | 22 | let server; 23 | let newServerTimeout; 24 | let serverCloseTimeout; 25 | let nextServerNum = 1; 26 | let shuttingDown = false; 27 | function createServer() { 28 | clearTimeout(newServerTimeout); 29 | const serverNum = nextServerNum++; 30 | console.log(`creating server (${serverNum})`); 31 | 32 | server = new Server({socketFile: SOCKET_FILE}); 33 | server.on('connectionClose', (clientId) => console.log(`Client (${clientId}) disconnected`)); 34 | server.on('listening', () => console.log(`server (${serverNum}) is listening`)); 35 | server.on('connection', (clientId) => { 36 | // Say hello to the client. 37 | console.log(`Client ${clientId} connected... saying "hello"`); 38 | server.send('hello', `Hello, Client ${clientId}!`, clientId); 39 | }); 40 | 41 | // After the first client connects, start a time to shut down the server. 42 | // This will show what happens to the client(s) when a server disappears. 43 | server.once('connection', () => { 44 | if (shuttingDown) { 45 | return; 46 | } 47 | 48 | console.log(`Server (${serverNum}) will shut down in 10 second.`); 49 | clearTimeout(serverCloseTimeout); 50 | serverCloseTimeout = setTimeout(() => server.close(), 10000); 51 | }); 52 | 53 | // When the server closes, auto-spawn a new one after a second. 54 | server.on('close', () => { 55 | if (shuttingDown) { 56 | return; 57 | } 58 | 59 | console.log(`server (${serverNum}) closed. Starting a replacement server in 5 second.`); 60 | newServerTimeout = setTimeout(() => createServer(), 5000); 61 | }); 62 | 63 | // Start listening. 64 | server.listen(); 65 | } 66 | 67 | createServer(); 68 | 69 | 70 | 71 | function shutdown(reason) { 72 | // Stop all processing and let node naturally exit. 73 | console.log('shutting down: ', reason); 74 | shuttingDown = true; 75 | clearTimeout(newServerTimeout); 76 | clearTimeout(serverCloseTimeout); 77 | server.close(); 78 | } 79 | 80 | process.on('SIGTERM', () => shutdown('sigterm')); 81 | process.on('SIGINT', () => shutdown('sigint')); 82 | -------------------------------------------------------------------------------- /examples/conversation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * All-in-one script (e.g. clients and server both defined here) which demonstrates 3 | * a brief conversation between client and server. 4 | * 5 | * Since IPC is free-flow, bidirectional communication, replies are not implicitly 6 | * related to the original messages. Implementations need to include some sort of 7 | * identifier to indicate that messages are part of the same conversation. 8 | * (JSON-RPC 2.0 provides a simple protocol which provides a way to do that.) 9 | * 10 | * Every conversation in this example, starts with an incrementing value. That 11 | * value is repeated for every message within the conversation. The example does 12 | * not intrinsically use that value, but it is printed in the console so that you 13 | * can visually relate the messages. 14 | * 15 | * The conversations go like this: 16 | * 17 | * - Server: "hello" 18 | * - Client: "helloback" 19 | * - Server: (if client name is "charlie") "helloagain" 20 | * 21 | * All messages are logged to the console when they are received by the server or a client. 22 | * 23 | * Hint: Don't forget to define the SOCKET_FILE before running! 24 | * 25 | * @author Chris Russell 26 | * @copyright Chris Russell 2018 27 | * @license MIT 28 | */ 29 | 30 | const SOCKET_FILE = undefined; 31 | 32 | const {Server, Client} = require ('../src/index'); 33 | 34 | const createClient = (clientName) => { 35 | // DO NOT pass the server in; DO NOT return the client. 36 | // They must both be able to send messages without first-hand knowledge of the other. 37 | 38 | // Create the client and give it a unique local id. 39 | const client = new Client({socketFile: SOCKET_FILE}); 40 | 41 | // Log ALL messages from the server, regardless of the topic. 42 | client.on('message', (message, topic) => { 43 | console.log(`Server -> Client (${clientName}): `, `[${topic}]`, message); 44 | }); 45 | 46 | // Whenever the server says hello, send a "helloback" message. Include 47 | // the original server message and this client's name. 48 | client.on('message.hello', (original) => client.send('helloback', {clientName, original})); 49 | 50 | // Go ahead and connect. 51 | client.connect(); 52 | }; 53 | 54 | // Create the server. 55 | const server = new Server({socketFile: SOCKET_FILE}); 56 | 57 | // Log all messages coming into the server. Note, that server-side callbacks get an 58 | // extra argument which provides a client id. 59 | server.on('message', (message, topic, clientId) => { 60 | console.log(`Client (${clientId}) -> Server: `, `[${topic}]`, message); 61 | }); 62 | 63 | // When the client replies to our "hello" with a "helloback", reply with a "helloagain"... BUT only for 64 | // clients named "charlie". 65 | server.on('message.helloback', (message, clientId) => { 66 | // Inspect the name in the message. 67 | if (message.clientName === 'charlie') { 68 | // Use the client id to say hello again, repeating the original message which the client 69 | // was nice enough to include. 70 | server.send('helloagain', message.original, clientId); 71 | } 72 | }); 73 | 74 | // Start everything up. 75 | let helloCounter = 1; 76 | server.listen(); 77 | server.on('listening', () => { 78 | // make note of the fact that the server is listening. 79 | console.log('server listening'); 80 | 81 | // Start broadcasting hello immediately, even though we don't have any clients. 82 | // The first few messages will be missed. 83 | setInterval(() => { 84 | server.broadcast('hello', helloCounter++); 85 | }, 1000); 86 | 87 | // After about 3 seconds, make the first client (bob) and then at the 6 second mark add in the other (charlie) 88 | setTimeout(() => createClient('bob'), 3000); 89 | setTimeout(() => createClient('charlie'), 3000); 90 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | An event-driven IPC implementation for NodeJS using unix file sockets 4 | 5 | [Docs](https://crussell52.github.io/node-socket-ipc/) | 6 | [Source](https://github.com/crussell52/node-socket-ipc/) | 7 | [Releases](https://github.com/crussell52/node-socket-ipc/releases) | 8 | [NPM](https://www.npmjs.com/package/@crussell52/socket-ipc) 9 | 10 | ## Table of Contents 11 | 12 | ### [About](/) (you are here) 13 | - [Quick Start](#quick-start) 14 | * [Install](#install) 15 | * [A Simple Example](#a-simple-example) 16 | * [More Examples](#more-examples) 17 | - [Limitations](#limitations) 18 | - [Alternate solutions](#alternate-solutions) 19 | * [Why this One](#why-this-one) 20 | 21 | #### [Usage](/docs/USAGE.md) 22 | 23 | #### [Advanced Usage](/docs/ADVANCED.md) 24 | 25 | #### [API](/docs/API.md) 26 | 27 | ## Quick Start 28 | 29 | Want to get up and running quickly? This is for you. 30 | 31 | ### Install 32 | 33 | ``` 34 | npm install --save @crussell52/socket-ipc 35 | ``` 36 | 37 | ### A Simple Example 38 | 39 | Client: 40 | ```js 41 | const {Client} = require('@crussell52/socket-ipc'); 42 | const client = new Client({socketFile: '/tmp/myApp.sock'}); 43 | 44 | // Say hello as soon as we connect to the server with a simple message 45 | // that give it our name. 46 | client.on('connect', () => client.send('hello', {name: 'crussell52'})); 47 | 48 | // Connect. It will auto-retry if the connection fails and auto-reconnect if the connection drops. 49 | client.connect(); 50 | ``` 51 | 52 | Server: 53 | ```js 54 | const {Server} = require('@crussell52/socket-ipc'); 55 | const server = new Server({socketFile: '/tmp/myApp.sock'}); 56 | 57 | // Listen for errors so they don't bubble up and kill the app. 58 | server.on('error', err => console.error('IPC Server Error!', err)); 59 | 60 | // Log all messages. Topics are completely up to the sender! 61 | server.on('message', (message, topic) => console.log(topic, message)); 62 | 63 | // Say hello back to anybody that sends a message with the "hello" topic. 64 | server.on('message.hello', (message, clientId) => server.send('hello', `Hello, ${message.name}!`, clientId)); 65 | 66 | // Start listening for connections. 67 | server.listen(); 68 | 69 | // Always clean up when you are ready to shut down your app to clean up socket files. If the app 70 | // closes unexpectedly, the server will try to "reclaim" the socket file on the next start. 71 | function shutdown() { 72 | server.close(); 73 | } 74 | ``` 75 | 76 | ### More Examples 77 | 78 | Check out the `/examples` directory in the [source](https://github.com/crussell52/node-socket-ipc) for more 79 | code samples. (Make sure you set the `SOCKET_FILE` constant at the top of the example files before you run them!) 80 | 81 | ## Limitations 82 | 83 | Let's get this out of the way early... 84 | 85 | Requires: 86 | - NodeJS >= 8.x LTS (might work with perfectly fine with some older versions -- but not tested) 87 | 88 | Transport Support: 89 | - Unix socket files (might work with windows socket files too -- but not tested) 90 | 91 | Unsupported Features: 92 | - TCP Sockets 93 | - UDP Sockets 94 | - Windows socket files (well *maybe* it does, I haven't tried ) 95 | - Native client-to-client communication (although you could implement it!) 96 | 97 | Love the project, but you need it to do something it doesn't? Open up a 98 | [feature request](https://github.com/crussell52/node-socket-ipc)! 99 | 100 | ## Alternate solutions 101 | 102 | [node-ipc](https://www.npmjs.com/package/node-ipc) 103 | [ZeroMQ](https://github.com/zeromq/zeromq.js) 104 | 105 | ### Why this one? 106 | 107 | When I needed to solve this problem, most of what I found was tied to some extra external dependency or platform 108 | (electron, redis, etc). The `node-ipc` lib caught my eye for a time, but I wasn't in love with the interface and it 109 | was published under a non-standard (and sometimes considered "satirical") license. 110 | 111 | So this package was born. Here are the goals of this project: 112 | 113 | - Bidirectional communication over Unix sockets (maybe other transports, in the future) 114 | - Simple interface for sending messages: 115 | * From the server to a specific client 116 | * From the server to all clients (broadcast) 117 | * From any client to the server 118 | - Minimize dependencies (So far, `0`!). 119 | - Event driven (using native NodeJS `EventEmitter`) 120 | - Ability to listen for _all_ messages or to narrow in on specific _topics_. 121 | - Built-in client resiliency (automatic reconnection, automatic connection retry) 122 | - Extensible design: 123 | * _Pluggable_ 124 | * Stable API 125 | * Thorough docs to make wrapping or extending easy 126 | * Leave domain details to the domain experts 127 | -------------------------------------------------------------------------------- /docs/API.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | An event-driven IPC implementation for NodeJS using unix file sockets 4 | 5 | [Docs](https://crussell52.github.io/node-socket-ipc/) | 6 | [Source](https://github.com/crussell52/node-socket-ipc/) | 7 | [Releases](https://github.com/crussell52/node-socket-ipc/releases) | 8 | [NPM](https://www.npmjs.com/package/@crussell52/socket-ipc) 9 | 10 | ## Table of Contents 11 | 12 | #### [About](/) 13 | 14 | #### [Usage](/docs/USAGE.md) 15 | 16 | #### [Advanced Usage](/docs/ADVANCED.md) 17 | 18 | ### [API](/docs/API.md) (you are here) 19 | - [Important Changes](#important-changes) 20 | - [Classes](#classes) 21 | * [Server](#server) 22 | - [new Server()](#new-serveroptions) 23 | - [Event: 'close'](#event-close) 24 | - [Event: 'closed'](#event-closed) 25 | - [Event: 'connection'](#event-connection) 26 | - [Event: 'error'](#event-error) 27 | - [Event: 'listening'](#event-listening) 28 | - [Event: 'message'](#event-message) 29 | - [Event: 'message._topic_'](#event-messagetopic) 30 | - [server.close()](#serverclose) 31 | - [server.send(topic, message, clientId)](#serversendtopic-message-clientid) 32 | - [server.listen()](#serverlisten) 33 | - [server.broadcast(topic, message)](#serverbroadcasttopic-message) 34 | * [Client](#client) 35 | - [new Client()](#new-clientoptions) 36 | - [Event: 'close'](#event-close-1) 37 | - [Event: 'closed'](#event-closed-1) 38 | - [Event: 'connect'](#event-connect) 39 | - [Event: 'connectError'](#event-connecterror) 40 | - [Event: 'disconnect'](#event-disconnect) 41 | - [Event: 'error'](#event-error-1) 42 | - [Event: 'message'](#event-message-1) 43 | - [Event: 'message._topic_'](#event-messagetopic-1) 44 | - [Event: 'reconnect'](#event-reconnect) 45 | - [client.close()](#clientclose) 46 | - [client.connect()](#clientconnect) 47 | - [client.send(topic, message)](#clientsendtopic-message) 48 | - [Interfaces (Classes)](#interfaces-classes) 49 | * [Transcoder](#transcoder) 50 | - [transcoder.createDecoder()](#transcodercreatedecoder) 51 | - [transcoder.socketEncoding](#transcodersocketencoding) 52 | - [transcoder.createEncoder()](#transcodercreateencoder) 53 | * [MessageWrapper](#messagewrapper) 54 | - [messageWrapper.topic](#messagewrappertopic) 55 | - [messageWrapper.message](#messagewrappermessage) 56 | - [Interfaces (Callbacks/Functions)](#interfaces-callbacksfunctions) 57 | * [decoderFunc](#decoderfunc) 58 | * [decodedCallback](#decodedcallback) 59 | * [decoderFactoryFunc](#decoderfactoryfunc) 60 | * [encodedCallback](#encodedcallback) 61 | * [encoderFunc](#encoderfunc) 62 | * [encoderFactoryFunc](#encoderfactoryfunc) 63 | 64 | ## Important Changes 65 | 66 | ### v0.4.0: 67 | * Client id is now a v4 uuid. This is part of the official interface and implementations can rely on this detail 68 | (thanks to @aamiryu7 for making the case). 69 | 70 | ### v0.3.0: 71 | * `closed` event Deprecated in favor of `close` event for both `Server` and `Client` 72 | 73 | ### v0.2.0: 74 | 75 | * Introduced `transcoder` option for both `Client` and `Server` 76 | * **(Breaking change)** Dropped `connectionClose` event. Applications should now listen for events on the underlying 77 | `net.Socket` (now provided as part of the `connection` event) 78 | * **(Breaking change)** `clientId` is now a `string` (It is still numeric, but applications should not rely on 79 | this detail). 80 | * The server-side `connection` event and client-side `connect` event now provide the underlying `net.Socket` instance. 81 | [Some applications](/docs/USAGE.md#event-timing) may benefit from listening directly to socket events. 82 | * The client-side `connect` event now provides a `net.Socket` instance. 83 | * Documentation has been enhanced to make note that some events are in response to events the specific events 84 | being emitted from the underlying `net.Socket` events. 85 | * The `messageError` event has been removed. The `error` event has been enhanced to emit an `EncodeError`, 86 | `SendError`, or `DecodeError` to cover cases previously covered by `messageError`. 87 | * Some `error` events would include a second, undocumented arg which provided the client id. This is no longer 88 | the case; `error` listeners will now always be given exactly one argument -- the `Error`. 89 | * Calling `client.connect()` or `server.listen()` a second time will now emit an `error` event instead of throwing 90 | the Error. This is more consistent than have a couple of cases which throw instead of emitting an error event. 91 | 92 | ## Classes 93 | 94 | These are instantiable classes what will pass an `instanceof` check. There are also a number of 95 | [`interfaces`](#interfaces) in the next section which are, at best, duck-typed at key spots in the module. 96 | 97 | ### Server 98 | 99 | This library follows a standard server/client pattern. There is one server which listens for connections 100 | from one or more clients. Intuitively, the `Server` class provides the interface for establishing the 101 | server side of the equation. 102 | 103 | The server can receive messages from any of the clients. It can also `send()` messsages to a specific client 104 | or it can `broadcast()` a message to all connected clients. 105 | 106 | #### new Server(options) 107 | - `options` (`object`) - The server configuration options 108 | * `socketFile` (`string`): The path to the socket file to use when it is told to "listen". See 109 | [`server.listen()`](#serverlisten) for more details on how this file is handled. 110 | * `[transcoder=jsonTranscoder]` (`Transcoder`) - A custom [`Transcoder`](#transcoder). Useful when encoding/decoding 111 | messages with JSON is not sufficient. 112 | 113 | Creates a new server, but it does not start listening until you call [`server.listen()`](#serverlisten). You can 114 | immediately attach listeners to the `Server` instance. 115 | 116 | #### Event: 'close' 117 | 118 | Emitted when the server has stopped listening for connections **and** all existing connections have been ended. 119 | 120 | #### Event: 'closed' 121 | 122 | _Deprecated in v0.3.0 - scheduled for removal in v1.0.0_ 123 | 124 | Alias of `close` event. 125 | 126 | #### Event: 'connection' 127 | - `clientId` (`string`) - The id of the client. Use this to send a message to the client. This is a version 4 UUID. 128 | - `connection` (`net.Socket`) - The NodeJS [`net.Socket`](https://nodejs.org/docs/latest-v8.x/api/net.html#net_class_net_socket) 129 | of the connection. 130 | 131 | Emitted when a client establishes a connection to the server. 132 | 133 | #### Event: 'error' 134 | - `error` (`Error`) - The error that occurred. 135 | 136 | Emitted when an error occurs. Any errors emitted by the underlying `net.Server` will also be repeated via 137 | this event as well as those from the underlying `net.Socket` instances of each connected client. 138 | 139 | Additionally, these specific error classes may be emitted. 140 | 141 | The following conditions will cause `Server` to emit an `error` event: 142 | 143 | - ([`EncodeError`](#encodeerror)) - When the message can not be encoded by the active [`Transcoder`](#transcoder). 144 | - ([`SendAfterCloseError`](#sendaftercloseerror)) - When this method is called after [`server.close()`](#serverclose) 145 | is called. 146 | - ([`BadClientError`](#badclienterror)) - When a message is sent to a client that does not exist. To avoid this error, 147 | stop sending messages to clients once their underlying `net.Socket` -- provided by the 148 | [`connection`](#event-connection) event -- emits a `close` event. 149 | 150 | #### Event: 'listening' 151 | 152 | Emitted when the server is ready for incoming connections. 153 | 154 | #### Event: 'message' 155 | - `message` (`any`) - The message from the client. By default, this can be any JSON deserializable 156 | type (including `null`) but a custom [Transcoder](#transcoder) can be used to change these rules. 157 | - `topic` (`string`) - The topic of the message as declared by the client. 158 | - `clientId` (`string`) - The id of the client. Use this to send a message to the client. 159 | 160 | Emitted when a message is received, regardless of the _topic_. 161 | 162 | #### Event: 'message._topic_' 163 | - `message` (`any`) - The message from the client. By default, this can be any JSON deserializable 164 | type (including `null`) but a custom [Transcoder](#transcoder) can be used to change these rules. 165 | - `clientId` (`string`) - The connection id of the client. Use this to send a message to the client. 166 | 167 | Emitted when a message with the specified _topic_ is received. For example, messages with a _topic_ of "dessert" 168 | would emit the `message.dessert` event. (Yum!) 169 | 170 | #### server.broadcast(topic, message) 171 | - `topic` (`string`) - The topic to publish the message under. If an empty value, `none` is 172 | used as the value. 173 | - `message` (`any`) - The message. May be any JSON serializable value (including `null`) 174 | 175 | Sends a message to **all** connected clients. On the client-side, this message can be heard by 176 | listening for the `message` or the `message.`_`topic`_ event. 177 | 178 | If there are no connected clients, this method will quietly do nothing. 179 | 180 | #### server.listen() 181 | 182 | Tells the server to start listening for client connections. This is an async operation and the 183 | [`listening`](#event-listening) event will emitted when the server is ready for connections. 184 | 185 | This may only be called **once** per instance. Calling this method a second time will emit an `error` event. 186 | 187 | #### server.send(topic, message, clientId) 188 | - `topic` (`string`) - The topic to publish the message under. If an empty value is given, `none` is 189 | used as the message topic. 190 | - `message` (`*`) - The message. By default, this may be any JSON serializable value (including `null`) but a 191 | custom [Transcoder](#transcoder) can be used to change these rules. 192 | - `clientId` (`string`) - The id of the client to send the message to. This is usually 193 | obtained by capturing it when the client connects or sends the server a message. 194 | 195 | Sends a message to a specific, connected, client. On the client-side, this message can be heard by 196 | listening for the `message` or the `message.`_`topic`_ event. 197 | 198 | #### server.close() 199 | 200 | Closes all active connections and stops listening for new connections. This is an asynchronous 201 | operation. Once the server is fully closed, the [`close`](#event-close) event will be emitted. 202 | 203 | Any future calls to [`server.send()`](#serversendtopic-message-clientid) or 204 | [`server.broadcast()`](#serverbroadcasttopic-message) will cause the server to emit an [`error`](#event-error) event. 205 | 206 | Once this method has been called, a new `Server` instance is needed to re-establish a connection with the server. 207 | 208 | ### Client 209 | 210 | This library follows a standard server/client pattern. There is one server which listens for connections 211 | from one or more clients. Intuitively, the `Client` class provides the interface for establishing the 212 | client side of the equation. 213 | 214 | The client can receive messages from the server and it can [`send()`](#clientsend) messages to the server. 215 | 216 | #### new Client(options) 217 | - `options` (`object`) - The client configuration options 218 | * `socketFile` (`string`): The path to the socket file to connect to. 219 | * `[transcoder=jsonTranscoder]` (`Transcoder`) - A custom [`Transcoder`](#transcoder). Useful when encoding/decoding 220 | messages with JSON is not sufficient. 221 | * `[retryDelay=1000]` (`number|{min: int, max:int}`) - If an integer, the number of milliseconds to wait between 222 | connection attempts. If an object, each delay will be a random value between the `min` and `max` values. 223 | * `[reconnectDelay=100]` (`number|{min: int, max:int}`) - If an integer, the number of milliseconds to wait before 224 | automatically reconnecting after an unexpected disconnect. If an object, each delay will be a random value between 225 | the `min` and `max` values. 226 | 227 | Creates a new client, but it does not connect until you call `client.connect()`. You can immediately 228 | attach listeners to the client instance. 229 | 230 | #### Event: 'close' 231 | 232 | Emitted when [`client.close()`](#clientclose) has been called **and** the client connection has been fully closed. 233 | 234 | #### Event: 'closed' 235 | 236 | _Deprecated in v0.3.0 - scheduled for removal in v1.0.0_ 237 | 238 | Alias of `close` event. 239 | 240 | #### Event: 'connect' 241 | - `connection` (`net.Socket`) - The NodeJS [`net.Socket`](https://nodejs.org/docs/latest-v8.x/api/net.html#net_class_net_socket) 242 | of the connection. 243 | 244 | Emitted when the `Client` establishes a connection with the server. This occurs during initial connection and during 245 | reconnect scenarios. 246 | 247 | #### Event: 'connectError' 248 | - `error` (`Error`) - The error that occurred. 249 | 250 | Emitted when a connection attempt fails. 251 | 252 | This event is common when the server is not yet listening. Because of the auto-retry mechanism, this event may be 253 | emitted several times while the client waits for the server to start listening. For some applications, waiting "forever" 254 | for the server to start may make sense; for others, you can use this event count the number of connection attempts and 255 | "give up" after some limit. 256 | 257 | #### Event: 'disconnect' 258 | 259 | Emitted when a client unexpectedly loses connection. This is distinct from the [`close`](#event-close-1) event that is 260 | a result of [`client.close()`](#clientclose) being called. 261 | 262 | The client emits this when it both conditions are met: 263 | - A `close` event is heard from the underlying `net.Socket` 264 | - [`client.close()`](#clientclose) has not been called 265 | 266 | This event is emitted when the client hears an `close` event from the underlying `net.Socket`. Some applications may 267 | [benefit from listening directly](/docs/USAGE.md#event-timing) to the socket events. 268 | 269 | #### Event: 'error' 270 | - `error` (`Error`) - The error that occurred. 271 | 272 | Emitted when an error occurs. 273 | 274 | After establishing a connection, the `Client` listens for `error` events from the underlying `net.Socket` and repeats 275 | them as a local event. 276 | 277 | Additionally, the following Error objects may be emitted. 278 | 279 | - ([`EncodeError`](#encodeerror)) - When the message can not be encoded by the active [`Transcoder`](#transcoder). 280 | - ([`SendAfterCloseError`](#sendaftercloseerror)) - When a `client.send()` after [`client.close()`](#serverclose) 281 | is called. 282 | - ([`NoServerError`](#noservererror)) - When `client.send()` is called and there is no active server connection. The 283 | [`connect`](#event-connect), [`reconnect`](#event-reconnect), and [`disconnect`](#event-reconnect) events to avoid 284 | this error. 285 | 286 | #### Event: 'message' 287 | - `message` (`any`) - The message from the client. By default, this can be any JSON deserializable 288 | type (including `null`). By using of a custom _transcoder_ that can be expanded! 289 | - `topic` (`string`) - The topic of the message as declared by the server. 290 | 291 | Emitted when a message is received, regardless of the _topic_. 292 | 293 | #### Event: 'message._topic_' 294 | - `message` (`any`) - The message from the server. By default, this can be any JSON deserializable 295 | type (including `null`) but a custom [Transcoder](#transcoder) can be used to influence the type range. 296 | 297 | Emitted when a message with the specified _topic_ is received. For example, messages with a _topic_ of "dessert" 298 | would emit the `message.dessert` event. (Yum!) 299 | 300 | #### Event: 'reconnect' 301 | 302 | An duplication of the [`connect`](#event-connect) that is only emitted when a client successfully performs an automatic 303 | reconnect. This event will always be immediately preceded by the `connect` event. It is useful when you want additional 304 | behavior in reconnect scenarios. You can also leverage `EventEmitter.once()` to handle initial connections and 305 | reconnects differently: 306 | 307 | ```js 308 | client.once('connect', /* ... */); // Only respond to the first connect event 309 | client.on('reconnect', /* ... */); // But respond to every reconnect event 310 | ``` 311 | 312 | #### client.close() 313 | 314 | Permanently closes the connection. There will be no automatic reconnect attempts. This is an asynchronous 315 | operation; the [`close`](#event-close-1) event will be emitted when the connection to the client has been completely 316 | closed. 317 | 318 | Any future call to [`client.send()`](#clientsendtopic-message) will cause the client to emit an [`error`](#event-error-1) 319 | event. 320 | 321 | #### client.connect() 322 | 323 | Tells the client to connect to the server. This is an async operation and the [`connect`](#event-connect) event will 324 | be emitted once the connection has been established. 325 | 326 | This may only be called **once** per instance. Calling this method a second time will emit an 327 | [`error`](#event-error-1) event. 328 | 329 | If the connection fails, a [`connectError`](#event-connecterror) event will be emitted and the client will automatically 330 | try again after a the delay defined by [`options.retryDelay`](#new-clientoptions). This cycle will be repeated until a 331 | connection is established or until [`client.close()`](#clientclose) is called. You can limit the number of retries by 332 | listening and counting the `connectError` events, then calling `client.close()` when you decide that it is time to 333 | "give up". 334 | 335 | Once connected a [`connect`](#event-connect) will be emitted providing access to the underlying `net.Socket` instance. 336 | 337 | If the underlying socket emits a [`close`](https://nodejs.org/docs/latest-v8.x/api/net.html#net_event_close) event, 338 | the behavior varies depending on whether or not [`client.close()`](#clientclose)` has been called: 339 | - If `client.close()` has been called, the client will emit a [`close`](#event-close-1) and no more messages may 340 | be sent from this instance. 341 | - if `client.close()` has NOT been called, the client will emit a [`disconnect`](#event-disconnect) event and 342 | it will automatically try to reconnect. The reconnection routine is identical to the initial connection routine with 343 | the exception that a [`reconnect`](#event-reconnect) event will be emitted _in addition to_ the `connect` event. 344 | 345 | #### client.send(topic, message) 346 | - `topic` (string, required) - The topic to publish the message under. If an empty value, `none` is 347 | used as the value. 348 | - `message` (*, required) - The message. By default, this may be any JSON serializable value (including `null`) but a 349 | custom [Transcoder](#transcoder) can be used to change these rules. 350 | 351 | Sends a message to the server. On the server-side, this message can be heard by listening for the 352 | [`message`](#event-message-1) or the [`message.`_`topic`_](#event-messagetopic-1) event. 353 | 354 | ### EncodeError 355 | ### DecodeError 356 | ### SendError 357 | ### SendAfterCloseError 358 | ### NoServerError 359 | ### BadClientError 360 | 361 | ## Interfaces (Classes) 362 | 363 | ## Interfaces (Callbacks/Functions) -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Chris Russell 3 | * @copyright Chris Russell 2018 4 | * @license MIT 5 | */ 6 | 7 | 'use strict'; 8 | 9 | const {EventEmitter} = require('events'); 10 | const net = require('net'); 11 | const fs = require('fs'); 12 | const jsonTranscoder = require('./lib/transcoder/jsonTranscoder'); 13 | const {MessageError, SendAfterCloseError, BadClientError, NoServerError} = require('./lib/error'); 14 | const uuid = require('uuid/v4'); 15 | 16 | /** 17 | * @interface MessageWrapper 18 | * 19 | * @property {string} topic - A non-empty topic for the message. 20 | * @property {*} message - The message. This may be any value that can be encoded by the current Transcoder. Unless a 21 | * custom Transcoder is configured, this must be a JSON serializable object. 22 | */ 23 | 24 | /** 25 | * @interface Transcoder 26 | * 27 | * @property {string} socketEncoding - The encoding to use when reading/writing data on the underlying socket. 28 | * @property {encoderFactoryFunc} createEncoder - A no-argument factory function which returns an `encoderFunc`. The 29 | * returned `encoderFunc` may be used to encode across multiple connections making it impossible to predict which 30 | * encoder instance is used for which connection. For this reason, attempting to make an encoder which "buffers" 31 | * data from several messages before sending it will result in undefined behavior. 32 | * @property {decoderFactoryFunc} createDecoder - A no-argument factory function which returns a `decoder`. Each unique 33 | * connection is guaranteed to receive its own decoder instance. In most cases, each returned decoder should 34 | * contain some sort of stateful "buffer" to handle cases where a message's data is spread across multiple 35 | * encoder calls. 36 | */ 37 | 38 | /** 39 | * @callback encoderFactoryFunc 40 | * @returns {encoderFunc} 41 | */ 42 | 43 | /** 44 | * @callback decoderFactoryFunc 45 | * @returns {decoderFunc} 46 | */ 47 | 48 | /** 49 | * Invoked by an `encoderFunc` when it has finished its work. 50 | * 51 | * The `EncoderFunc` MUST invoke this callback when it is done working with either the first or second argument 52 | * populated. 53 | * 54 | * @callback encodedCallback 55 | * @param {Error, EncodeError} error - If an error occurred, an `Error` should be passed as the first arg. 56 | * @param {*} data - The encoded data. The encoder MAY use `null` as a data value to indicate that the message was 57 | * skipped; no data will be sent in this case and the message will be silently discarded. The return value must 58 | * be ready to be written to the `net.Socket` so it should be in agreement with the socket encoding set by 59 | * `Transcoder#socketEncoding`. 60 | */ 61 | 62 | /** 63 | * Invoked by an `decoderFunc` when it has finished its work. 64 | * 65 | * The `EncoderFunc` MUST invoke this callback when it is done working with either the first or second argument 66 | * populated. 67 | * 68 | * @callback decodedCallback 69 | * @param {Error, DecodeError} error - If an error occurred, an `Error` should be passed as the first arg. 70 | * @param {MessageWrapper[]} data - An array `MessageWrapper` objects that each include a single message and its topic. 71 | * This may be an empty array in cases where the `decoderFunc` did not receive enough data for a complete message. 72 | */ 73 | 74 | /** 75 | * Encodes a `MessageWrapper` so that it can be placed on the socket. 76 | * 77 | * @callback encoderFunc 78 | * @param {MessageWrapper} msgWrapper - The message to be encoded and its topic. 79 | * @param {encodedCallback} callback - The callback to invoke after all work is complete. 80 | */ 81 | 82 | /** 83 | * Decodes raw data. 84 | * 85 | * @callback decoderFunc 86 | * @param {*} chunk - A chunk of data to be decoded. This may contain a full message, a partial message, or multiple 87 | * messages. The data type will depend on the socket encoding defined by `Transcoder#socketEncoding`. 88 | * @param {decodedCallback} callback - The callback to invoke after all work is complete. 89 | */ 90 | 91 | 92 | /** 93 | * Takes in a value and make sure it looks like a reasonable socket file path. 94 | * 95 | * Nodejs does some auto-detection in some cases to determine what type of connection 96 | * to make. This helps guard against misconfiguration leading to other types of 97 | * connections. 98 | * 99 | * @param {*} value - The value to test 100 | */ 101 | const validateSocketFileOption = (value) => { 102 | // See if the value is empty. 103 | if (!value) { 104 | return `Is empty` 105 | } 106 | 107 | // See if it looks like a port (all digits) 108 | if (/^\d+$/.test(value)) { 109 | return `Looks like a port`; 110 | } 111 | }; 112 | 113 | /** 114 | * Factory method for listening to incoming data on an underlying socket. 115 | * 116 | * @param {Socket} socket - The socket to listen to. 117 | * @param {EventEmitter} emitter - Where to emit events from. 118 | * @param {Transcoder} transcoder - The transcoder to use. 119 | * @param {string} [clientId] - Only relevant in the server context. The id of the client the socket is attached to. 120 | */ 121 | const attachDataListener = (socket, emitter, transcoder, clientId) => { 122 | 123 | /** @type {decoderFunc} */ 124 | const decoder = transcoder.createDecoder(); 125 | const emitError = (err) => { 126 | socket.destroy(err); 127 | emitter.emit('error', err, clientId); 128 | }; 129 | const emitMessage = (msgWrapper) => { 130 | emitter.emit('message', msgWrapper.message, msgWrapper.topic, clientId); 131 | emitter.emit(`message.${msgWrapper.topic}`, msgWrapper.message, clientId); 132 | }; 133 | 134 | socket.on('data', chunk => { 135 | // Run the decoder with a callback that either emits an error or messages. 136 | decoder(chunk, (err, msgWrappers) => { 137 | if (err) { 138 | emitError(err); 139 | return; 140 | } 141 | 142 | for (let i = 0; i < msgWrappers.length; i++) { 143 | try { 144 | emitMessage(msgWrappers[i]); 145 | } catch (err) { 146 | // Emit the error and stop processing messages. 147 | emitError(err); 148 | return; 149 | } 150 | } 151 | }); 152 | }); 153 | }; 154 | 155 | /** 156 | * Helper function for getting a random int between two values. 157 | */ 158 | function getRandomIntInclusive(min, max) { 159 | min = Math.ceil(min); 160 | max = Math.floor(max); 161 | return Math.floor(Math.random() * (max - min + 1)) + min; //The maximum is inclusive and the minimum is inclusive 162 | } 163 | 164 | class Server extends EventEmitter { 165 | /** 166 | * @param {string} options.socketFile - Path to the socket file to use. 167 | * @param {Transcoder} [options.transcoder] - The transcoder to use to prepare messages to be written to the 168 | * underlying socket or to process data being read from the underlying socket. 169 | */ 170 | constructor(options) { 171 | super(); 172 | 173 | this._transcoder = options.transcoder || jsonTranscoder; 174 | this._encoder = this._transcoder.createEncoder(); 175 | 176 | // See if the given socket file looks like a port. We don't support running the server on a port. 177 | let invalidSockFileReason = validateSocketFileOption(options.socketFile); 178 | if (invalidSockFileReason) { 179 | throw new Error(`Invalid value for 'options.socketFile' (${options.socketFile}): ${invalidSockFileReason}`); 180 | } 181 | 182 | this._socketFile = options.socketFile; 183 | 184 | // In the socket map, the keys are the sockets and the values are the client id. This allows incoming messages 185 | // to be easily associated with their client id. 186 | this._sockets = new Map(); 187 | 188 | // In the socket lookup, the ids are the keys and the sockets are the value. This allows an application 189 | // to send a message to a particular client by just knowing the client id. 190 | this._clientLookup = new Map(); 191 | } 192 | 193 | /** 194 | * Creates a standard Node net server and immediately starts listening to the provided socket file. 195 | * 196 | * This method may only be called once. 197 | */ 198 | listen() { 199 | if (this._server) { 200 | throw new Error('Can not listen twice.'); 201 | } 202 | 203 | // Create the server. 204 | this._server = net.createServer(); 205 | this._server.on('error', err => { 206 | if (err.code === 'EADDRINUSE') { 207 | // See if it is a valid server by trying to connect to it. 208 | const testSocket = net.createConnection({path: this._socketFile}); 209 | 210 | // If the connection is established, then there is an active server and the originl 211 | // error stands. 212 | testSocket.on('connect', () => this.emit('error', err)); 213 | 214 | // If the connection errors out, then there is a chance we can recover. 215 | testSocket.on('error', testErr => { 216 | if (testErr.code !== 'ECONNREFUSED') { 217 | // We didn't connect, but it does NOT look like its because it is a dead sock file. 218 | // Let the original error stand. 219 | this.emit('error', err); 220 | return; 221 | } 222 | 223 | // conn-refused implies that this is a dead sock file. Attempt to unlink it. 224 | try { 225 | fs.unlinkSync(this._socketFile); 226 | } catch (unlinkErr) { 227 | // Nope... unlink failed. Possibly because we don't have the perms to remove the sock. 228 | // Emit the original error. 229 | this.emit('error', err); 230 | return; 231 | } 232 | 233 | // Try listening again. 234 | this._server.listen(this._socketFile); 235 | }); 236 | } else { 237 | this.emit('error', err); 238 | } 239 | }); 240 | this._server.on('close', () => { 241 | this.emit('closed'); // TODO-1.0: Remove 242 | this.emit('close'); 243 | }); 244 | 245 | this._server.on('listening', () => { 246 | this.emit('listening'); 247 | }); 248 | 249 | this._server.on('connection', socket => { 250 | 251 | const id = uuid(); 252 | this._sockets.set(socket, id); 253 | this._clientLookup.set(id, socket); 254 | 255 | const forgetClient = () => { 256 | // "Forget" about this client" 257 | this._sockets.delete(socket); 258 | this._clientLookup.delete(id); 259 | }; 260 | 261 | socket.setEncoding(this._transcoder.socketEncoding); 262 | 263 | // Forget the client on both end and close. 264 | socket.on('end', forgetClient); 265 | socket.on('close', forgetClient); 266 | 267 | this.emit('connection', id, socket); 268 | 269 | // Listen for messages on the socket. 270 | attachDataListener(socket, this, this._transcoder, id); 271 | }); 272 | 273 | this._server.listen(this._socketFile); 274 | } 275 | 276 | close() { 277 | // A second close does nothing. 278 | if (this._closeCalled) { 279 | return; 280 | } 281 | 282 | // Close the server to stop incoming connections, then end all known sockets. 283 | this._closeCalled = true; 284 | this._server.close(); 285 | this._sockets.forEach((id, socket) => { 286 | socket.end(); 287 | }); 288 | } 289 | 290 | broadcast(topic, message) { 291 | // Refuse if close has been called. 292 | if (this._closeCalled) { 293 | this.emit(`error`, 294 | new SendAfterCloseError(`Can not '.broadcast()' after '.close()'`, message, topic)); 295 | return; 296 | } 297 | 298 | // Encode it once. 299 | this._encoder({topic, message}, (err, data) => { 300 | if (err) { 301 | this.emit('error', err); 302 | } else { 303 | // Broadcast the message to all known sockets. 304 | this._sockets.forEach((id, socket) => { 305 | socket.write(data) 306 | }); 307 | } 308 | }); 309 | } 310 | 311 | send(topic, message, clientId) { 312 | // Refuse if close has been called. 313 | if (this._closeCalled) { 314 | this.emit(`error`, 315 | new SendAfterCloseError(`Can not '.send()' after '.close()'`, message, topic)); 316 | return; 317 | } 318 | 319 | // Refuse if we don't recognize the client id. This could be because it never existed or because the client 320 | // disconnected. 321 | if (!this._clientLookup.has(clientId)) { 322 | this.emit(`error`, new BadClientError(`Invalid client id: ${clientId}`, message, topic, clientId)); 323 | return; 324 | } 325 | 326 | // Get the socket and send data to it. 327 | const socket = this._clientLookup.get(clientId); 328 | this._encoder({topic, message}, (err, data) => { 329 | if (err) { 330 | this.emit('error', err); 331 | } else { 332 | socket.write(data); 333 | } 334 | }); 335 | } 336 | } 337 | 338 | class Client extends EventEmitter { 339 | /** 340 | * @param {Transcoder} [options.transcoder] - The transcoder to use to prepare messages to be written to the 341 | * underlying socket or to process data being read from the underlying socket. 342 | * @param {int|{min:{int}, max:{int}}} [options.retryDelay=1000] - If an integer, this is the number of milliseconds 343 | * to wait between connection attempts. If an object then each delay will delayed by a random value between the 344 | * `min` and `max` value. 345 | * @param {int|{min:{int}, max:{int}}} [options.reconnectDelay=100] - If an integer, this is the number of 346 | * milliseconds before trying to reconnect. If an object then each delay will be a random value between the `min` 347 | * and `max` value. 348 | * @param {string} options.socketFile - The path to the socket file to use. 349 | */ 350 | constructor(options) { 351 | super(); 352 | 353 | this._transcoder = options.transcoder || jsonTranscoder; 354 | this._encoder = this._transcoder.createEncoder(); 355 | 356 | // See if the given socket file looks like a port. We don't support running the server on a port. 357 | let invalidSockFileReason = validateSocketFileOption(options.socketFile); 358 | if (invalidSockFileReason) { 359 | throw new Error(`Invalid value for 'options.socketFile' (${options.socketFile}): ${invalidSockFileReason}`); 360 | } 361 | this._socketFile = options.socketFile; 362 | 363 | const retryDelayOpt = options.retryDelay || 1000; 364 | if (Number.isInteger(retryDelayOpt)) { 365 | this._retryDelay = {min: retryDelayOpt, max: retryDelayOpt} 366 | } else { 367 | this._retryDelay = { 368 | min: retryDelayOpt.min || 1000, 369 | max: retryDelayOpt.max || 1000 370 | } 371 | } 372 | 373 | const reconDelayOpt = options.reconnectDelay || 1000; 374 | if (Number.isInteger(reconDelayOpt)) { 375 | this._reconnectDelay = {min: reconDelayOpt, max: reconDelayOpt} 376 | } else { 377 | this._reconnectDelay = { 378 | min: reconDelayOpt.min || 100, 379 | max: reconDelayOpt.max || 100 380 | } 381 | } 382 | } 383 | 384 | connect() { 385 | // Only allow a single call to connect() 386 | if (this._connectCalled) { 387 | throw new Error('ipc.Client.connect() already called.'); 388 | } 389 | 390 | this._connectCalled = true; 391 | this._connect(false); 392 | } 393 | 394 | _connect(isReconnect) { 395 | 396 | const socket = net.createConnection({path: this._socketFile}); 397 | socket.setEncoding(this._transcoder.socketEncoding); 398 | 399 | // Until a connection is established, handle errors as connection errors. 400 | const handleConnectError = (err) => { 401 | this.emit('connectError', err); 402 | const retryDelay = getRandomIntInclusive(this._retryDelay.min, this._retryDelay.max); 403 | this._retryTimeoutId = setTimeout(() => this._connect(isReconnect), retryDelay); 404 | }; 405 | socket.on('error', handleConnectError); 406 | 407 | socket.on('connect', () => { 408 | this._socket = socket; 409 | 410 | // Always emit a connect event. Conditionally, also emit a reconnect event. 411 | this.emit('connect', socket); 412 | if (isReconnect) { 413 | this.emit('reconnect', socket); 414 | } 415 | 416 | // Swap out the connection error handling for standard error handling. 417 | socket.removeListener('error', handleConnectError); 418 | socket.on('error', (err) => this.emit('error', err)); // Just repeat socket errors 419 | 420 | // As soon as the socket emits an end event, we "forget" about the socket so that no more messages 421 | // can be sent to it. However, anything in the buffer may still be until we hear the `close` event. 422 | socket.on('end', () => { 423 | this._socket = null; 424 | }); 425 | 426 | // We don't start reconnection logic until the socket finishes closing. This makes sure that any messages 427 | // previously put into the buffer get time to flush before we start putting more data on the wire. 428 | socket.on('close', () => { 429 | 430 | // Make sure we have "forgotten" the socket. This helps cases where `close` happens without `end` which 431 | // seems to happen in abrupt disconnect scenarios. 432 | this._socket = null; 433 | 434 | // See if this was an explicit close. 435 | if (this._explicitClose) { 436 | // Emit the "closed" event. 437 | this.emit('closed'); // TODO-1.0: Remove 438 | this.emit('close'); 439 | } else { 440 | // Announce the disconnect, then try to reconnect after a configured delay. 441 | this.emit('disconnect'); 442 | const reconnectDelay = getRandomIntInclusive(this._reconnectDelay.min, this._reconnectDelay.max); 443 | this._reconnectDelayTimeoutId = setTimeout(() => this._connect(true), reconnectDelay); 444 | } 445 | }); 446 | }); 447 | 448 | // Listen for data on the socket. 449 | attachDataListener(socket, this, this._transcoder); 450 | } 451 | 452 | close() { 453 | this._explicitClose = true; 454 | // Stop any retry or reconnect timers. 455 | clearTimeout(this._retryTimeoutId); 456 | clearTimeout(this._reconnectDelayTimeoutId); 457 | if (this._socket) { 458 | this._socket.end(); 459 | } else { 460 | // No underlying socket, so no close event to proxy. Emit it manually. 461 | this.emit('close') 462 | } 463 | } 464 | 465 | send(topic, message) { 466 | // Refuse to send once client was explicitly closed. 467 | if (this._explicitClose) { 468 | this.emit('error', new SendAfterCloseError(`Can not 'send()' after 'close()'.`, message, topic)); 469 | return; 470 | } 471 | 472 | // Refuse to send if we don't have an active connection. 473 | if (!this._socket) { 474 | this.emit('error', new NoServerError(`Can not send, no active server connection.`, message, topic)) 475 | return; 476 | } 477 | 478 | // Encode, then write to the socket if everything went okay. 479 | this._encoder({topic, message}, (err, data) => { 480 | if (err) { 481 | this.emit('error', err); 482 | } else { 483 | this._socket.write(data); 484 | } 485 | }); 486 | } 487 | } 488 | 489 | module.exports = { 490 | Client, 491 | Server, 492 | MessageError 493 | }; --------------------------------------------------------------------------------