From b71458dcf966040709efbb907fd86bde518b84c3 Mon Sep 17 00:00:00 2001 From: Chris Russell Date: Wed, 19 Sep 2018 02:40:29 -0400 Subject: [PATCH 1/5] Introduce transcoders for pluggable encode/decode routines --- src/index.js | 252 +++++++++++++++++---------- src/lib/error.js | 34 ++++ src/lib/transcoder/jsonTranscoder.js | 58 ++++++ 3 files changed, 250 insertions(+), 94 deletions(-) create mode 100644 src/lib/error.js create mode 100644 src/lib/transcoder/jsonTranscoder.js diff --git a/src/index.js b/src/index.js index b24eedd..15fedb3 100644 --- a/src/index.js +++ b/src/index.js @@ -9,7 +9,84 @@ const {EventEmitter} = require('events'); const net = require('net'); const fs = require('fs'); -const delimiter = '%%\n'; +const jsonTranscoder = require('./lib/transcoder/jsonTranscoder'); +const {MessageError} = require('./lib/error'); + +/** + * @interface MessageWrapper + * + * @property {string} topic - A non-empty topic for the message. + * @property {*} message - The message. This may be any value that can be encoded by the current Transcoder. Unless a + * custom Transcoder is configured, this must be a JSON serializable object. + */ + +/** + * @interface Transcoder + * + * @property {string} socketEncoding - The encoding to use when reading/writing data on the underlying socket. + * @property {encoderFactoryFunc} createEncoder - A no-argument factory function which returns an `encoderFunc`. The + * returned `encoderFunc` may be used to encode across multiple connections making it impossible to predict which + * encoder instance is used for which connection. For this reason, attempting to make an encoder which "buffers" + * data from several messages before sending it will result in undefined behavior. + * @property {decoderFactoryFunc} createDecoder - A no-argument factory function which returns a `decoder`. Each unique + * connection is guaranteed to receive its own decoder instance. In most cases, each returned decoder should + * contain some sort of stateful "buffer" to handle cases where a message's data is spread across multiple + * encoder calls. + */ + +/** + * @callback encoderFactoryFunc + * @returns {encoderFunc} + */ + +/** + * @callback decoderFactoryFunc + * @returns {decoderFunc} + */ + +/** + * Invoked by an `encoderFunc` when it has finished its work. + * + * The `EncoderFunc` MUST invoke this callback when it is done working with either the first or second argument + * populated. + * + * @callback encodedCallback + * @param {Error, EncodeError} error - If an error occurred, an `Error` should be passed as the first arg. + * @param {*} data - The encoded data. The encoder MAY use `null` as a data value to indicate that the message was + * skipped; no data will be sent in this case and the message will be silently discarded. The return value must + * be ready to be written to the `net.Socket` so it should be in agreement with the socket encoding set by + * `Transcoder#socketEncoding`. + */ + +/** + * Invoked by an `decoderFunc` when it has finished its work. + * + * The `EncoderFunc` MUST invoke this callback when it is done working with either the first or second argument + * populated. + * + * @callback decodedCallback + * @param {Error, DecodeError} error - If an error occurred, an `Error` should be passed as the first arg. + * @param {MessageWrapper[]} data - An array `MessageWrapper` objects that each include a single message and its topic. + * This may be an empty array in cases where the `decoderFunc` did not receive enough data for a complete message. + */ + +/** + * Encodes a `MessageWrapper` so that it can be placed on the socket. + * + * @callback encoderFunc + * @param {MessageWrapper} msgWrapper - The message to be encoded and its topic. + * @param {encodedCallback} callback - The callback to invoke after all work is complete. + */ + +/** + * Decodes raw data. + * + * @callback decoderFunc + * @param {*} chunk - A chunk of data to be decoded. This may contain a full message, a partial message, or multiple + * messages. The data type will depend on the socket encoding defined by `Transcoder#socketEncoding`. + * @param {decodedCallback} callback - The callback to invoke after all work is complete. + */ + /** * Takes in a value and make sure it looks like a reasonable socket file path. @@ -32,69 +109,60 @@ const validateSocketFileOption = (value) => { } }; -const parseMessage = raw => { - - // Decode as JSON - let message; - try { - message = JSON.parse(raw); - } catch(cause) { - // Not able to parse as JSON. Tell the server to emit an error and move on. - throw new MessageError('Failed to parse as JSON', raw); - } - - // It is valid JSON, but it is a valid message? - if (!message.topic) { - // No topic, not a valid message. Tell the server to emit an error and move on. - throw new MessageError('Invalid message structure.', raw); - } - - return message; -}; - -class MessageError extends Error { - constructor(errorMessage, receivedMessage) { - super(errorMessage); - this.message = receivedMessage; - } -} +/** + * Factory method for listening to incoming data on an underlying socket. + * + * @param {Socket} socket - The socket to listen to. + * @param {EventEmitter} emitter - Where to emit events from. + * @param {Transcoder} transcoder - The transcoder to use. + * @param {int} [clientId] - Only relevant in the server context. The id of the client the socket is attached to. + */ +const attachDataListener = (socket, emitter, transcoder, clientId) => { + + /** @type {decoderFunc} */ + const decoder = transcoder.createDecoder(); + const emitError = (err) => { + socket.destroy(err); + emitter.emit('error', err, clientId); + }; + const emitMessage = (msgWrapper) => { + emitter.emit('message', msgWrapper.message, msgWrapper.topic, clientId); + emitter.emit(`message.${msgWrapper.topic}`, msgWrapper.message, clientId); + }; + + socket.on('data', chunk => { + // Run the decoder with a callback that either emits an error or messages. + decoder(chunk, (err, msgWrappers) => { + if (err) { + emitError(err); + return; + } -const send = (socket, data, topic) => { - socket.write(JSON.stringify({ - topic: topic || 'none', - data: data - }) + delimiter); + for (let i = 0; i < msgWrappers.length; i++) { + try { + emitMessage(msgWrappers[i]); + } catch (err) { + // Emit the error and stop processing messages. + emitError(err); + return; + } + } + }); + }); }; -class MessageBuffer { - constructor() { - this.buffer = ''; - } - - data(chunk) { - - // Use the buffer plus this chunk as the data that we need to process. - let data = this.buffer += chunk; - - // Split on the delimiter to find distinct and complete messages. - let messages = data.split(delimiter); - - // Pop the last element off of the message array. It is either an incomplete message - // or an empty string. Use it as the new buffer value. - this.buffer = messages.pop(); - - return messages; - } -} - - class Server extends EventEmitter { /** - * @param {string} options.socketFile - Path to the socket file to use. + * @param {string} options.socketFile - Path to the socket file to use. + * @param {Transcoder} [options.transcoder] - The transcoder to use to prepare messages to be written to the + * underlying socket or to process data being read from the underlying socket. */ constructor(options) { super(); + this._transcoder = options.transcoder || jsonTranscoder; + this._encoder = this._transcoder.createEncoder(); + // See if the given socket file looks like a port. We don't support running the server on a port. let invalidSockFileReason = validateSocketFileOption(options.socketFile); if (invalidSockFileReason) { @@ -169,7 +237,6 @@ class Server extends EventEmitter { this._server.on('connection', socket => { - const buffer = new MessageBuffer(); const id = this._nextClientId++; this._sockets.set(socket, id); this._clientLookup.set(id, socket); @@ -184,22 +251,7 @@ class Server extends EventEmitter { this.emit('connection', id); // Listen for messages on the socket. - socket.on('data', chunk => { - // Process each message. - buffer.data(chunk).forEach(raw => { - let message; - try { - message = parseMessage(raw); - } catch(e) { - this.emit('messageError', e, id); - return; - } - - // Message events get emitted by the server. - this.emit('message', message.data, message.topic, id); - this.emit(`message.${message.topic}`, message.data, id); - }); - }); + attachDataListener(socket, this, this._transcoder, id); }); this._server.listen(this._socketFile); @@ -214,9 +266,17 @@ class Server extends EventEmitter { } broadcast(topic, message) { - // Broadcast the message to all known sockets. - this._sockets.forEach((id, socket) => { - send(socket, message, topic); + // Encode once, send many + this._encoder({topic, message}, (err, data) => { + if (err) { + this.emit('error', err); + return; + } + + // Broadcast the message to all known sockets. + this._sockets.forEach((id, socket) => { + socket.write(data) + }); }); } @@ -226,18 +286,30 @@ class Server extends EventEmitter { return; } - send(this._clientLookup.get(clientId), message, topic); + const socket = this._clientLookup.get(clientId); + this._encoder({topic, message}, (err, data) => { + if (err) { + this.emit('error', err); + } else { + socket.write(data); + } + }); } } class Client extends EventEmitter { /** + * @param {Transcoder} [options.transcoder] - The transcoder to use to prepare messages to be written to the + * underlying socket or to process data being read from the underlying socket. * @param {int} [options.retryDelay=1000] - The number of milliseconds to wait between connection attempts. * @param {int} [options.reconnectDelay=100] - The number of milliseconds to wait before reconnecting. * @param {string} options.socketFile - The path to the socket file to use. */ constructor(options) { super(); + + this._transcoder = options.transcoder || jsonTranscoder; + this._encoder = this._transcoder.createEncoder(); // See if the given socket file looks like a port. We don't support running the server on a port. let invalidSockFileReason = validateSocketFileOption(options.socketFile); @@ -297,23 +369,8 @@ class Client extends EventEmitter { }); }); - const buffer = new MessageBuffer(); - socket.on('data', chunk => { - // Process each message. - buffer.data(chunk).forEach(raw => { - let message; - try { - message = parseMessage(raw); - } catch(e) { - this.emit('messageError', e); - return; - } - - // Emit the events. - this.emit('message', message.data, message.topic); - this.emit(`message.${message.topic}`, message.data); - }); - }); + // Listen for data on the socket. + attachDataListener(socket, this, this._transcoder); } close() { @@ -327,8 +384,15 @@ class Client extends EventEmitter { } send(topic, message) { - send(this._socket, message, topic); + this._encoder({topic, message}, (err, data) => { + if (err) { + this.emit('error', err); + } else { + this._socket.write(data); + } + }); } + } module.exports = { diff --git a/src/lib/error.js b/src/lib/error.js new file mode 100644 index 0000000..2071afb --- /dev/null +++ b/src/lib/error.js @@ -0,0 +1,34 @@ +/** + * @author Chris Russell + * @copyright Chris Russell 2018 + * @license MIT + */ + +'use strict'; + +class DecodeError extends Error { + /** + * @param errorMessage - A description of what happened. + * @param {*} rawData - The data which failed to decode. + */ + constructor(errorMessage, rawData) { + super(errorMessage); + this.rawData = rawData; + } +} + +class EncodeError extends Error { + /** + * @param {string} errorMessage - A description of what happened. + * @param {MessageWrapper} messageWrapper - The message which failed to encode and its topic. + */ + constructor(errorMessage, messageWrapper) { + super(errorMessage); + this.topic = messageWrapper.topic; + this.message = messageWrapper.message; + } +} + +module.exports = { + EncodeError, DecodeError +}; \ No newline at end of file diff --git a/src/lib/transcoder/jsonTranscoder.js b/src/lib/transcoder/jsonTranscoder.js new file mode 100644 index 0000000..21fdea2 --- /dev/null +++ b/src/lib/transcoder/jsonTranscoder.js @@ -0,0 +1,58 @@ +/** + * @author Chris Russell + * @copyright Chris Russell 2018 + * @license MIT + */ + +'use strict'; + +const {DecodeError, EncodeError} = require('../error'); +const delimiter = '%EOM%'; + +module.exports = { + socketEncoding: 'utf8', + + createEncoder: () => { + // Return an encoder function. + return (msgWrapper, cb) => { + try { + cb(null, JSON.stringify(msgWrapper) + delimiter); + } catch (err) { + cb(new EncodeError('Failed to encode, caused by: ' + err.message, msgWrapper)); + } + }; + }, + + createDecoder: () => { + // Each decoder gets it own buffer. + let buffer = ''; + + // Return an encoder function. + return (chunk, cb) => { + + // Use the buffer plus this chunk as the data that we need to process. + let data = buffer += chunk; + + // Split on the delimiter to find distinct and complete messages. + let rawMessages = data.split(delimiter); + + // Pop the last element off of the message array. It is either an incomplete message + // or an empty string. Use it as the new buffer value. + buffer = rawMessages.pop(); + + // Build out the list of decoded messages. + const messages = []; + for (let i = 0; i < rawMessages.length; i++) { + try { + messages.push(JSON.parse(rawMessages[i])); + } catch (err) { + // Invoke the callback with a DecodeError and stop processing. + cb(new DecodeError('Failed to decode, caused by: ' + err.message, rawMessages[i])); + return; + } + } + + cb(null, messages); + } + } +}; \ No newline at end of file From a7f1b7ac0e4509bed4b32f3bcd464a39817d03cc Mon Sep 17 00:00:00 2001 From: Chris Russell Date: Wed, 19 Sep 2018 18:05:11 -0400 Subject: [PATCH 2/5] Update event documentation - Misc formatting changes to try to improve readability - Mention new `transcoder` option when describing message data types - Make note of removal of the `errorMessage` event. --- README.md | 84 +++++++++++++++++++++++++++++++++---------------------- 1 file changed, 51 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 6588c45..7916ac8 100644 --- a/README.md +++ b/README.md @@ -209,34 +209,42 @@ Possible signatures: - `listening` - Fires when the server is ready for incoming connections. - - `connection` (clientId) - Fires when a client connects to the server. - * `clientId` (number) - The id of the client. Use this to send a message to the client. + - `connection (clientId)` - Fires when a client connects to the server. + * `clientId` (`number`) - The id of the client. Use this to send a message to the client. - - `message` (message, topic, clientId) - Fired whenever a message is received from a client, regardless + - `message (message, topic, clientId)` - Fired whenever a message is received from a client, regardless of the `topic`. - * `message` (any) - The message from the client. This could be any JSON deserializable - type (including `null`) - * `topic` (string) - The topic of the message as declared by the client. - * `clientId` (number) - The id of the client. Use this to send a message to the client. + * `message` (`any`) - The message from the client. By default, this can be any JSON deserializable + type (including `null`). By using of a custom _transcoder_ that can be expanded! + * `topic` (`string`) - The topic of the message as declared by the client. + * `clientId` (`number`) - The id of the client. Use this to send a message to the client. - - `message.`_`topic`_ (message, clientId) - Fired whenever a message of a specific topic is received. This + - `message.`_`topic`_` (message, topic, clientId)` - Fired whenever a message of a specific topic is received. This is a dynamic event type. If a message with the topic of `desserts` (yum) is receive, it would be published under the `message.desserts` event. - * `message` (any) - The message from the client. This could be any JSON deserializable - type (including `null`) - * `clientId` (number) - The id of the client. Use this to send a message to the client. + * `message` (`any`) - The message from the client. By default, this can be any JSON deserializable + type (including `null`). By using of a custom _transcoder_ you control the type range! + * `clientId` (`number`) - The id of the client. Use this to send a message to the client. - - `messageError` (error, clientId ) - Fires when a data is received, but could not be understood. - * `error` (MessageError) - The error containing the raw data that was received. - * `clientId` (number) - The id of the client that sent the bad data. - - - `connectionClose` (clientId) - Fires when a client's connection closes. - * `clientId` (number) - The id of the client. Do not send messages to clients that have disconnected. + - `connectionClose (clientId)` - Fires when a client's connection closes. + * `clientId` (`number`) - The id of the client. Do not send messages to clients that have disconnected. - `close` - Fires when the server is closed and all connections have been ended. - - `error` (error) - Fires when an error occurs. `Node` provides special treatment of `error` events. - * `error` (Error) - The error that occurred. + - `error (error)` - Fires when an error occurs. `Node` provides special treatment of `error` events; if you do not + listen for this event, it will throw the `Error`. If this is the result of an error while decoding messages, the + connection to the client that sent the message will be closed. + * `error` (`Error`) - The error that occurred. If the error occurred while encoding a message, it will be an + `EncodeError`. Similarly a decoding error will emit a `DecodeError`. + +**Recent Changes** + + - `v0.2.0`: + * The `messageError` event has been removed. The `error` event has been enhanced to emit an `EncodeError` or + `DecodeError` to cover cases previously covered by `messageError`. This was done to simplify the code and API. + * Some `error` events would include a second, undocumented arg which provided the client id. This is no longer + the case. + ### Client @@ -305,29 +313,39 @@ Possible signatures: #### Events - - `connectError` (error) - Fires when a connection attempt fails. - * `error` (Error) - The error that occurred. + - `connectError (error)` - Fires when a connection attempt fails and a connection retry is queued. + * `error` (`Error`) - The error that occurred. - `connect` - Fires when the client establishes an **initial** connection with the server. - `disconnect` - Fires when a client unexpectedly loses connection. This is distinct from the `close` event which - indicates completion of a delibrate call to `client.close()`. + indicates completion of a deliberate call to `client.close()`. - `reconnect` - Fires when the client reestablishes a connection with the server after an unexpected disconnect. - - `message` (message, topic) - Fired whenever a message is received from the server, regardless of the `topic`. - * `message` (any) - The message from the server. This could be any JSON deserializable type (including `null`) + - `message (message, topic)` - Fired whenever a message is received from the server, regardless of the `topic`. + * `message` (`any`) - The message from the client. By default, this can be any JSON deserializable + type (including `null`). By using of a custom _transcoder_ you control the type range! * `topic` (string) - The topic of the message as declared by the server. - - `message.`_`topic`_ (message) - Fired whenever a message of a specific topic is received. This is a dynamic - event type. If a message with the topic of `desserts` (yum) is receive, it would be published under the - `message.desserts` event. - * `message` (any) - The message from the server. This could be any JSON deserializable type (including `null`) - - - `messageError` (error) - Fires when a data is received, but could not be understood. - * `error` (MessageError) - The error containing the raw data that was received. + - `message.`_`topic`_` (message, topic, clientId)` - Fired whenever a message of a specific topic is received. This + is a dynamic event type. If a message with the topic of `desserts` (yum) is receive, it would be published + under the `message.desserts` event. + * `message` (`any`) - The message from the client. By default, this can be any JSON deserializable + type (including `null`). By using of a custom _transcoder_ you control the type range! - `close` - Fires when the client is fully closed after a call to `client.close()`. - - `error` - Fires when an error occurs. `Node` provides special treatment of `error` events. - * `error` (Error) - The error that occurred. \ No newline at end of file + - `error (error)` - Fires when an error occurs. `Node` provides special treatment of `error` events; if you do not + listen for this event, it will throw the `Error`. If this is the result of an error while decoding messages, the + connection to the server that sent the message will be closed. + * `error` (`Error`) - The error that occurred. If the error occurred while encoding a message, it will be an + `EncodeError`. Similarly a decoding error will emit a `DecodeError`. + +**Recent Changes** + + - `v0.2.0`: + * The `messageError` event has been removed. The `error` event has been enhanced to emit an `EncodeError` or + `DecodeError` to cover cases previously covered by `messageError`. This was done to simplify the code and API. + * Some `error` events would include a second, undocumented arg which provided the client id. This is no longer + the case. \ No newline at end of file From 5ffbf554f7f4970db833c080b20730f1899fbe1c Mon Sep 17 00:00:00 2001 From: Chris Russell Date: Wed, 19 Sep 2018 18:05:31 -0400 Subject: [PATCH 3/5] Version bump to v0.2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4be2902..4b5ec78 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@crussell52/socket-ipc", - "version": "0.1.2", + "version": "0.2.0", "description": "An event-driven IPC implementation using unix file sockets.", "keywords": ["sockets", "ipc", "unix"], "homepage": "https://crussell52.github.io/node-socket-ipc/", From eb208c2acdde45e9669634c70edd6ed270d5b4a8 Mon Sep 17 00:00:00 2001 From: Chris Russell Date: Wed, 19 Sep 2018 19:19:20 -0400 Subject: [PATCH 4/5] Start splitting the documentation up into separate pages. --- README.md | 393 ++++++++++++----------------------------------- docs/ADVANCED.md | 10 ++ docs/API.md | 228 +++++++++++++++++++++++++++ docs/USAGE.md | 28 ++++ 4 files changed, 362 insertions(+), 297 deletions(-) create mode 100644 docs/ADVANCED.md create mode 100644 docs/API.md create mode 100644 docs/USAGE.md diff --git a/README.md b/README.md index 7916ac8..1ce1e94 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,101 @@ -# @crussell52/socket-ipc +# `@crussell52/socket-ipc` + An event-driven IPC implementation using unix file sockets. +[Documentation Page](https://crussell52.github.io/node-socket-ipc/) + +## Contents + +- [About](/README.md) + * [Quick Start](#quick-start) + - [Install](#install) + - [A Simple Example](#a-simple-example) + - [More Examples](#more-examples) + * [Limitations](#limitations) + * [Why another IPC lib?](#why-another-ipc-lib) + - [A Strong Alternative](#a-strong-alternative) + - [Why is this one different?](#why-is-this-one-different) + +- [API](/docs/API.md) + + +## Quick Start + +Want to get up and running quickly? This is for you. + +### Install + +``` +npm install --save @crussell52/socket-ipc +``` + +### A Simple Example + +Client: +```js +const {Client} = require('@crussell52/socket-ipc'); +const client = new Client({socketFile: '/tmp/myApp.sock'}); + +// Say hello as soon as we connect to the server with a simple message +// that give it our name. +client.on('connect', () => client.send('hello', {name: 'crussell52'})); + +// Connect. It will auto-retry if the connection fails and auto-reconnect if the connection drops. +client.connect(); +``` + +Server: +```js +const {Server} = require('@crussell52/socket-ipc'); +const server = new Server({socketFile: '/tmp/myApp.sock'}); + +// Listen for errors so they don't bubble up and kill the app. +server.on('error', err => console.error('IPC Server Error!', err)); + +// Log all messages. Topics are completely up to the sender! +server.on('message', (message, topic) => console.log(topic, message)); + +// Say hello back to anybody that sends a message with the "hello" topic. +server.on('message.hello', (message, clientId) => server.send('hello', `Hello, ${message.name}!`, clientId)); + +// Start listening for connections. +server.listen(); + +// Always clean up when you are ready to shut down your app to clean up socket files. If the app +// closes unexpectedly, the server will try to "reclaim" the socket file on the next start. +function shutdown() { + server.close(); +} +``` + +### More Examples + +Check out the `/examples` directory in the [source](https://github.com/crussell52/node-socket-ipc) for more +code samples. (Make sure you set the `SOCKET_FILE` constant at the top of the example files before you run them!) + ## Limitations -Let's start with what this lib can't do so you can move on if it isn't a good fit for your project. +Let's get this out of the way early... -Supports: +Requires: - NodeJS >= 8.x LTS (might work with perfectly fine with some older versions -- but not tested) + +Transport Support: - Unix socket files (might work with windows socket files too -- but not tested) -Doesn't Support: +Unsupported Features: - TCP Sockets - UDP Sockets - Windows socket files (well *maybe* it does, I haven't tried ) - Native client-to-client communication (although you could implement it!) + +Love the project, but you need it to do something it doesn't? Open up a +[feature request](https://github.com/crussell52/node-socket-ipc)! ## Why another IPC lib? -I had a need for high speed IPC. Simply put, I went looking at the IPC libraries that were -currently published and struggled to find one that met my needs. +I had a need for high speed IPC. I went looking at the IPC modules that were available and found a LOT of them. None +of them really met my needs... In some cases, they were bound to specific message formats: - [avsc](https://www.npmjs.com/package/avsc) (implements the Avro specification) @@ -32,7 +109,7 @@ Several were linked to specific technologies: - [python-bridge](https://www.npmjs.com/package/python-bridge) (Python) - [node-jet](https://www.npmjs.com/package/node-jet) (jetbus.io) - A few others covered specific use cases: +A few others covered specific use cases: * [ipc-event-emitter](https://www.npmjs.com/package/ipc-event-emitter) (Node Subprocesses) Etc... @@ -40,7 +117,7 @@ Etc... So, like the 50 billion other authors, I chose to make my own. Not because I thought any of the others were particularily _bad_, but they did not seem to meet _my_ goals for IPC. -## A strong alternative +### A strong alternative I have to give a shoutout to [node-ipc](https://www.npmjs.com/package/node-ipc). It offers a very robust IPC implementation which checks a lot of the boxes when compared against my humble lib's goals **and** covers many more transport protocols. You @@ -50,302 +127,24 @@ _For me_, `node-ipc` did not make sense to use for a number of reasons. Upon rev more complexity than I need (a tough combination). I also struggle with its choice in licensing. That said, it's mere existence **did** make me _seriously_ wonder whether or not I should bother publishing this library. (Obviously I decided it was worth it!) -## Why is this one different? +### Why is this one different? I can't say that it is different than *all* of the others -- there really are a lot of projects tagged as IPC and I honestly didn't review them all. But from what I was able to review, I did feel like this one is worth adding to the pile... Here are the goals: -- Simple server/client, bi-direction communication over Unix sockets (maybe other transports, one day) +- Bidirectional communication over Unix sockets (maybe other transports, in the future) - Simple interface for sending messages: * From the server to a specific client * From the server to all clients (broadcast) * From any client to the server -- Event driven, using native NodeJS `EventEmitter` -- Ability to listen for all messages or messages related to a specific "topic" -- Client resiliency (automatic reconnection, automatic connection retry) -- Generic enough that specific implementations can be built around it - -# Usage - -Phew... Sorry it took so long to get here. IPC is a crowded space and I thought that background was important. - -## Quick Start - -Server: -```js -const {Server} = require('@crussell52/socket-ipc'); -const server = new Server({socketFile: '/tmp/myApp.sock'}); - -// Log all messages. Topics are completely up to the sender! -server.on('message', (message, topic) => console.log(topic, message)); - -// Say hello back to anybody that sends a message with the "hello" topic. -// The recevier has complete control over which topics they listen to! -server.on('message.hello', (message, clientId) => { - server.send('hello', `Hello, ${message.name}!`, clientId); -}); - -// Start listening for clients. -server.listen(); - -//... -function shutdown() { - // Shutdown the server with your app to clean up the socket file! - // If you crash without doing cleanup, don't worry. The server will - // "reclaim" (delete + recreate) abandoned socket files. - server.close(); -} -``` -Client: -```js -const {Client} = require('@crussell52/socket-ipc'); -const client = new Client({socketFile: '/tmp/myApp.sock'}); - -// Say hello as soon as we connect to the server. -client.on('connect', () => { - // Message is whatever your application needs it to be! - // Here, we are telling the server our name. - client.send('hello', {name: 'crussell52'}); -}); - -// Start the connection. If the server isn't up yet, that's okay. -// There is a built in auto-retry. If the server drops after you -// connect, that's okay too; the client will stubbornly attempt -// to reconnect. -client.connect(); -``` - -## More Examples - -Check out the `/examples` directory of the source code for more samples! - -## A note about security - -Unix socket files exist on the file system. This library does not provide any special handling of their -creation; it leaves that up to the expert: the [NodeJs net module](https://nodejs.org/api/net.html). In fact, -that page has a section dedicated to Node's [IPC support](https://nodejs.org/api/net.html#net_ipc_support) -that you should probably read, if you are not already famliar with it. - -Because they are files, they are subject to permissions. Make sure you understand how those permissions work -for sockets on your target OS. Use approriate caution to not expose your application's messages to unintended -audiences **or expose your application to messages from unintended clients!**. - -## API - -Here are the full details of this the `@crussell52/socket-ipc` API. - -### Server - -This library follows a standard server/client pattern. There is one server which listens for connections -from one or more clients. Intuitively, the `Server` class provides the interface for establishing the -server side of the equation. - -The server can receive messages from any of the clients. It can also `send()` messsages to a specific client -or it can `broadcast()` a message to all connected clients. - -#### Constructor - -Creates a new server, but it does not start listening until you call `server.listen()`. You can immediately -attach listeners to the server instance. - -Possible signatures: - - `Server(options)` - * `options` (object, required) - The server configuration options - - `socketFile` (string, required): The path to the socket file to use when it is told to "listen". See - `server.listen()` for more details on how this file is handled. - - -#### `server.listen()` - -Tells the server to start listening for client connections. This is an async operation and the `listening` -event will fire once the server is ready for connections. - -This may only be called **once** per instance. Calling this method a second time will result in an `Error` -being thrown (note, the `error` event will not fire in this case). - -Possible signatures: - - `server.listen()` - -#### `server.send(topic, message, clientId)` - -Sends a message to a specific, connected, client. On the client-side, this message can be heard by -listening for the `message` or the `message.`_`topic`_ event. - -Possible signatures: - - `server.send(topic, message, clientId)` - * `topic` (string, required) - The topic to publish the message under. If an empty value, `none` is - used as the value. - * `message` (*, required) - The message. May be any JSON serializable value (including `null`) - * `clientId` (number, required) - The id of the client to send the message to. This is usually - obtained by capturing it when the client connects or sends the server a message. Attempting to - send to a clientId which does not exist will fire an `error` event. - -#### `server.broadcast(topic, message)` - -Sends a message to **all** connected clients. On the client-side, this message can be heard by -listening for the `message` or the `message.`_`topic`_ event. - -Possible signatures: - - `server.broadcast(topic, message)` - * `topic` (string, required) - The topic to publish the message under. If an empty value, `none` is - used as the value. - * `message` (any, required) - The message. May be any JSON serializable value (including `null`) - -#### `server.close()` - -Closes all active connections and stops listening for new connections. This is an asynchronous -operation. Once the server is fully closed, the `close` event will be fired. - -Once a server has been "closed", it can not start listening again. A new instance must be created. If -you have a scenario that requires servers to be routinely closed and restarted, a factory function can be -effective for handling the server setup. - -Possible signatures: - - `server.close()` - -#### Events - - - `listening` - Fires when the server is ready for incoming connections. - - - `connection (clientId)` - Fires when a client connects to the server. - * `clientId` (`number`) - The id of the client. Use this to send a message to the client. - - - `message (message, topic, clientId)` - Fired whenever a message is received from a client, regardless - of the `topic`. - * `message` (`any`) - The message from the client. By default, this can be any JSON deserializable - type (including `null`). By using of a custom _transcoder_ that can be expanded! - * `topic` (`string`) - The topic of the message as declared by the client. - * `clientId` (`number`) - The id of the client. Use this to send a message to the client. - - - `message.`_`topic`_` (message, topic, clientId)` - Fired whenever a message of a specific topic is received. This - is a dynamic event type. If a message with the topic of `desserts` (yum) is receive, it would be published - under the `message.desserts` event. - * `message` (`any`) - The message from the client. By default, this can be any JSON deserializable - type (including `null`). By using of a custom _transcoder_ you control the type range! - * `clientId` (`number`) - The id of the client. Use this to send a message to the client. - - - `connectionClose (clientId)` - Fires when a client's connection closes. - * `clientId` (`number`) - The id of the client. Do not send messages to clients that have disconnected. - - - `close` - Fires when the server is closed and all connections have been ended. - - - `error (error)` - Fires when an error occurs. `Node` provides special treatment of `error` events; if you do not - listen for this event, it will throw the `Error`. If this is the result of an error while decoding messages, the - connection to the client that sent the message will be closed. - * `error` (`Error`) - The error that occurred. If the error occurred while encoding a message, it will be an - `EncodeError`. Similarly a decoding error will emit a `DecodeError`. - -**Recent Changes** - - - `v0.2.0`: - * The `messageError` event has been removed. The `error` event has been enhanced to emit an `EncodeError` or - `DecodeError` to cover cases previously covered by `messageError`. This was done to simplify the code and API. - * Some `error` events would include a second, undocumented arg which provided the client id. This is no longer - the case. - - -### Client - -This library follows a standard server/client pattern. There is one server which listens for connections -from one or more clients. Intuitively, the `Client` class provides the interface for establishing the -client side of the equation. - -The client can receive messages from the server and tt can `send()` messsages. - -#### Constructor - -Creates a new client, but it does not connect until you call `client.connect()`. You can immediately -attach listeners to the client instance. - -Possible signatures: - - `Client(options)` - * `options` (object, required) - The client configuration options - - `socketFile` (string, required): The path to the socket file to use to establish a connection. - - `retryDelay` (number, optional, default=1000) - The number of milliseconds to wait between connection attempts. - - `reconnectDelay` (number, optional, default=100) - The number of milliseconds to wait before automatically - reconnecting after an unexpected disconnect. - -#### `client.connect()` - -Tells the client to connect to the server. This is an async operation and the `connect` event will fire once the -a connection has been established. - -This may only be called **once** per instance. Calling this method a second time will result in an `Error` -being thrown (note, the `error` event will not fire in this case). - -If the server is unavailable when the client attempts to connect, a `connectError` event will be fired and the client will -automatically retry after a delay defined by the `options.retryDelay` value that was passed into the constructor. This -cycle of a `connectError` event followed by a delayed retry will continue to happen until a connection is established or -until `client.close()` is called. If you want to limit the number of retries, you can count the `connectError` events and -call`client.close()` after some threshold. - -Once connected, if an unexpected disconnect occurs (e.g. not an explicit call to `client.close()`) a `disconnect` event -will be fired and the client will automatically start attempting to reconnect to the server. The connection process will -occur as described above, including the automatic retry behavior. The only difference is that, once a new connection is -established, a `reconnect` event will fire instead of a `connect` event. - -Possible signatures: - - `client.connect()` - -#### `client.send(topic, message)` - -Sends a message to the server. On the server-side, this message can be heard by listening for the `message` or the `message.`_`topic`_ event. - -Possible signatures: - - `client.send(topic, message)` - * `topic` (string, required) - The topic to publish the message under. If an empty value, `none` is - used as the value. - * `message` (*, required) - The message. May be any JSON serializable value (including `null`) - -#### `client.close()` - -Permanently closes the connection. There will be no automatic reconnect attempts. This is an aysnchronous -operation; the `close` event will fire after the close operation is complete. - -Once a client has been "closed", it can not reconnect. A new instance must be created. If you have a scenario that -requires clients to be routinely closed and restarted, a factory function can be effective for handling the client -setup. - -Possible signatures: - - `client.close()` - -#### Events - - - `connectError (error)` - Fires when a connection attempt fails and a connection retry is queued. - * `error` (`Error`) - The error that occurred. - - - `connect` - Fires when the client establishes an **initial** connection with the server. - - - `disconnect` - Fires when a client unexpectedly loses connection. This is distinct from the `close` event which - indicates completion of a deliberate call to `client.close()`. - - - `reconnect` - Fires when the client reestablishes a connection with the server after an unexpected disconnect. - - - `message (message, topic)` - Fired whenever a message is received from the server, regardless of the `topic`. - * `message` (`any`) - The message from the client. By default, this can be any JSON deserializable - type (including `null`). By using of a custom _transcoder_ you control the type range! - * `topic` (string) - The topic of the message as declared by the server. - - - `message.`_`topic`_` (message, topic, clientId)` - Fired whenever a message of a specific topic is received. This - is a dynamic event type. If a message with the topic of `desserts` (yum) is receive, it would be published - under the `message.desserts` event. - * `message` (`any`) - The message from the client. By default, this can be any JSON deserializable - type (including `null`). By using of a custom _transcoder_ you control the type range! - - - `close` - Fires when the client is fully closed after a call to `client.close()`. - - - `error (error)` - Fires when an error occurs. `Node` provides special treatment of `error` events; if you do not - listen for this event, it will throw the `Error`. If this is the result of an error while decoding messages, the - connection to the server that sent the message will be closed. - * `error` (`Error`) - The error that occurred. If the error occurred while encoding a message, it will be an - `EncodeError`. Similarly a decoding error will emit a `DecodeError`. - -**Recent Changes** - - - `v0.2.0`: - * The `messageError` event has been removed. The `error` event has been enhanced to emit an `EncodeError` or - `DecodeError` to cover cases previously covered by `messageError`. This was done to simplify the code and API. - * Some `error` events would include a second, undocumented arg which provided the client id. This is no longer - the case. \ No newline at end of file +- Minimize dependencies (So far, `0`!). They may creep in where they make sense, but I'm looking to raw NodeJS + for solutions, _first_. +- Event driven (using native NodeJS `EventEmitter`) +- Ability to listen for _all_ messages or to narrow in on specific _topics_. +- Built-in client resiliency (automatic reconnection, automatic connection retry) +- Extensible design: + * _Pluggable_ where it makes sense + * Stable API with thorough docs to make wrapping or extending easy + * Leave domain details to the domain experts diff --git a/docs/ADVANCED.md b/docs/ADVANCED.md new file mode 100644 index 0000000..9e8cbcf --- /dev/null +++ b/docs/ADVANCED.md @@ -0,0 +1,10 @@ +# Advanced Usage + +For the most obvious use cases, you probably don't need this stuff. But these are the things you might find useful +when things get... _interesting_. + +## Custom Encoding and Decoding + +## Talking to Processes Outside of Node + +## Throttling Messages \ No newline at end of file diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..dda06e1 --- /dev/null +++ b/docs/API.md @@ -0,0 +1,228 @@ +# API + +Here are the full details of the `@crussell52/socket-ipc` API. + +## Classes + +These are instantiable classes what will pass an `instanceof` check. There are also a number of +[`interfaces`](#interfaces) in the next section which are, at best, duck-typed at key spots in the module. + +### Server + +This library follows a standard server/client pattern. There is one server which listens for connections +from one or more clients. Intuitively, the `Server` class provides the interface for establishing the +server side of the equation. + +The server can receive messages from any of the clients. It can also `send()` messsages to a specific client +or it can `broadcast()` a message to all connected clients. + +#### Constructor + +Creates a new server, but it does not start listening until you call `server.listen()`. You can immediately +attach listeners to the server instance. + +Possible signatures: + - `Server(options)` + * `options` (object, required) - The server configuration options + - `socketFile` (string, required): The path to the socket file to use when it is told to "listen". See + `server.listen()` for more details on how this file is handled. + + +#### `server.listen()` + +Tells the server to start listening for client connections. This is an async operation and the `listening` +event will fire once the server is ready for connections. + +This may only be called **once** per instance. Calling this method a second time will result in an `Error` +being thrown (note, the `error` event will not fire in this case). + +Possible signatures: + - `server.listen()` + +#### `server.send(topic, message, clientId)` + +Sends a message to a specific, connected, client. On the client-side, this message can be heard by +listening for the `message` or the `message.`_`topic`_ event. + +Possible signatures: + - `server.send(topic, message, clientId)` + * `topic` (string, required) - The topic to publish the message under. If an empty value, `none` is + used as the value. + * `message` (*, required) - The message. May be any JSON serializable value (including `null`) + * `clientId` (number, required) - The id of the client to send the message to. This is usually + obtained by capturing it when the client connects or sends the server a message. Attempting to + send to a clientId which does not exist will fire an `error` event. + +#### `server.broadcast(topic, message)` + +Sends a message to **all** connected clients. On the client-side, this message can be heard by +listening for the `message` or the `message.`_`topic`_ event. + +Possible signatures: + - `server.broadcast(topic, message)` + * `topic` (string, required) - The topic to publish the message under. If an empty value, `none` is + used as the value. + * `message` (any, required) - The message. May be any JSON serializable value (including `null`) + +#### `server.close()` + +Closes all active connections and stops listening for new connections. This is an asynchronous +operation. Once the server is fully closed, the `close` event will be fired. + +Once a server has been "closed", it can not start listening again. A new instance must be created. If +you have a scenario that requires servers to be routinely closed and restarted, a factory function can be +effective for handling the server setup. + +Possible signatures: + - `server.close()` + +#### Events + + - `listening` - Fires when the server is ready for incoming connections. + + - `connection (clientId)` - Fires when a client connects to the server. + * `clientId` (`number`) - The id of the client. Use this to send a message to the client. + + - `message (message, topic, clientId)` - Fired whenever a message is received from a client, regardless + of the `topic`. + * `message` (`any`) - The message from the client. By default, this can be any JSON deserializable + type (including `null`). By using of a custom _transcoder_ that can be expanded! + * `topic` (`string`) - The topic of the message as declared by the client. + * `clientId` (`number`) - The id of the client. Use this to send a message to the client. + + - `message.`_`topic`_` (message, topic, clientId)` - Fired whenever a message of a specific topic is received. This + is a dynamic event type. If a message with the topic of `desserts` (yum) is receive, it would be published + under the `message.desserts` event. + * `message` (`any`) - The message from the client. By default, this can be any JSON deserializable + type (including `null`). By using of a custom _transcoder_ you control the type range! + * `clientId` (`number`) - The id of the client. Use this to send a message to the client. + + - `connectionClose (clientId)` - Fires when a client's connection closes. + * `clientId` (`number`) - The id of the client. Do not send messages to clients that have disconnected. + + - `close` - Fires when the server is closed and all connections have been ended. + + - `error (error)` - Fires when an error occurs. `Node` provides special treatment of `error` events; if you do not + listen for this event, it will throw the `Error`. If this is the result of an error while decoding messages, the + connection to the client that sent the message will be closed. + * `error` (`Error`) - The error that occurred. If the error occurred while encoding a message, it will be an + `EncodeError`. Similarly a decoding error will emit a `DecodeError`. + +**Recent Changes** + + - `v0.2.0`: + * The `messageError` event has been removed. The `error` event has been enhanced to emit an `EncodeError` or + `DecodeError` to cover cases previously covered by `messageError`. This was done to simplify the code and API. + * Some `error` events would include a second, undocumented arg which provided the client id. This is no longer + the case. + + +### Client + +This library follows a standard server/client pattern. There is one server which listens for connections +from one or more clients. Intuitively, the `Client` class provides the interface for establishing the +client side of the equation. + +The client can receive messages from the server and tt can `send()` messsages. + +#### Constructor + +Creates a new client, but it does not connect until you call `client.connect()`. You can immediately +attach listeners to the client instance. + +Possible signatures: + - `Client(options)` + * `options` (object, required) - The client configuration options + - `socketFile` (string, required): The path to the socket file to use to establish a connection. + - `retryDelay` (number, optional, default=1000) - The number of milliseconds to wait between connection attempts. + - `reconnectDelay` (number, optional, default=100) - The number of milliseconds to wait before automatically + reconnecting after an unexpected disconnect. + +#### `client.connect()` + +Tells the client to connect to the server. This is an async operation and the `connect` event will fire once the +a connection has been established. + +This may only be called **once** per instance. Calling this method a second time will result in an `Error` +being thrown (note, the `error` event will not fire in this case). + +If the server is unavailable when the client attempts to connect, a `connectError` event will be fired and the client will +automatically retry after a delay defined by the `options.retryDelay` value that was passed into the constructor. This +cycle of a `connectError` event followed by a delayed retry will continue to happen until a connection is established or +until `client.close()` is called. If you want to limit the number of retries, you can count the `connectError` events and +call`client.close()` after some threshold. + +Once connected, if an unexpected disconnect occurs (e.g. not an explicit call to `client.close()`) a `disconnect` event +will be fired and the client will automatically start attempting to reconnect to the server. The connection process will +occur as described above, including the automatic retry behavior. The only difference is that, once a new connection is +established, a `reconnect` event will fire instead of a `connect` event. + +Possible signatures: + - `client.connect()` + +#### `client.send(topic, message)` + +Sends a message to the server. On the server-side, this message can be heard by listening for the `message` or the `message.`_`topic`_ event. + +Possible signatures: + - `client.send(topic, message)` + * `topic` (string, required) - The topic to publish the message under. If an empty value, `none` is + used as the value. + * `message` (*, required) - The message. May be any JSON serializable value (including `null`) + +#### `client.close()` + +Permanently closes the connection. There will be no automatic reconnect attempts. This is an aysnchronous +operation; the `close` event will fire after the close operation is complete. + +Once a client has been "closed", it can not reconnect. A new instance must be created. If you have a scenario that +requires clients to be routinely closed and restarted, a factory function can be effective for handling the client +setup. + +Possible signatures: + - `client.close()` + +#### Events + + - `connectError (error)` - Fires when a connection attempt fails and a connection retry is queued. + * `error` (`Error`) - The error that occurred. + + - `connect` - Fires when the client establishes an **initial** connection with the server. + + - `disconnect` - Fires when a client unexpectedly loses connection. This is distinct from the `close` event which + indicates completion of a deliberate call to `client.close()`. + + - `reconnect` - Fires when the client reestablishes a connection with the server after an unexpected disconnect. + + - `message (message, topic)` - Fired whenever a message is received from the server, regardless of the `topic`. + * `message` (`any`) - The message from the client. By default, this can be any JSON deserializable + type (including `null`). By using of a custom _transcoder_ you control the type range! + * `topic` (string) - The topic of the message as declared by the server. + + - `message.`_`topic`_` (message, topic, clientId)` - Fired whenever a message of a specific topic is received. This + is a dynamic event type. If a message with the topic of `desserts` (yum) is receive, it would be published + under the `message.desserts` event. + * `message` (`any`) - The message from the client. By default, this can be any JSON deserializable + type (including `null`). By using of a custom _transcoder_ you control the type range! + + - `close` - Fires when the client is fully closed after a call to `client.close()`. + + - `error (error)` - Fires when an error occurs. `Node` provides special treatment of `error` events; if you do not + listen for this event, it will throw the `Error`. If this is the result of an error while decoding messages, the + connection to the server that sent the message will be closed. + * `error` (`Error`) - The error that occurred. If the error occurred while encoding a message, it will be an + `EncodeError`. Similarly a decoding error will emit a `DecodeError`. + +**Recent Changes** + + - `v0.2.0`: + * The `messageError` event has been removed. The `error` event has been enhanced to emit an `EncodeError` or + `DecodeError` to cover cases previously covered by `messageError`. This was done to simplify the code and API. + * Some `error` events would include a second, undocumented arg which provided the client id. This is no longer + the case. + +### EncodeError + +### DecodeError + +## Interfaces \ No newline at end of file diff --git a/docs/USAGE.md b/docs/USAGE.md new file mode 100644 index 0000000..890a666 --- /dev/null +++ b/docs/USAGE.md @@ -0,0 +1,28 @@ +# Usage + +General knowledge for the simplest use cases. + +## A Note on Safety + +Unix socket files exist on the file system. This library does not provide any special handling of their +creation; it leaves that up to the expert: the [NodeJs net module](https://nodejs.org/api/net.html). In fact, +that page has a section dedicated to Node's [IPC support](https://nodejs.org/api/net.html#net_ipc_support) +that you should probably read, if you are not already famliiar with it. + +Because they are files, they are subject to permissions. Make sure you understand how those permissions work +for sockets on your target OS. Use appropriate caution to not expose your application's messages to unintended +audiences **or expose your application to messages from unintended clients!**. + +Socket files are very commonly used. You could use this library to tap into any socket file that your process has +access to! That could be _very_ interesting... but it could also be hazardous. In particular, the `Server` will +try to use _any_ socket file you tell it to -- even if that socket file is normally used by another service. Now, it +can't "hijack" a socket file that another server is actively using, but if you occupy it, the other service may fail +to start or its client's may think you are their server and start sending unexpected data! + +The details of how socket files work and the traps that might lurk in the shadows are **far** beyond the scope of this +module's documentation. Like any good module, `socket-ipc` tries to hide this complexity from you and get you up +and running fast. But if this is your first time stepping into this territory, it might still be worth the effort to +learn a bit about them. + +## Throughput + From c319992d46ea07d8a87cd536cf6c65c658549564 Mon Sep 17 00:00:00 2001 From: Chris Russell Date: Thu, 20 Sep 2018 01:10:39 -0400 Subject: [PATCH 5/5] More documentation - Structure API page similar to NodeJS docs - Add ToC to main README and API docs - Fill in more details on API docs --- README.md | 36 +++-- docs/API.md | 371 +++++++++++++++++++++++++++++--------------------- docs/USAGE.md | 6 + 3 files changed, 245 insertions(+), 168 deletions(-) diff --git a/README.md b/README.md index 1ce1e94..2ca84d4 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,31 @@ -# `@crussell52/socket-ipc` +# About -An event-driven IPC implementation using unix file sockets. +An event-driven IPC implementation for NodeJS using unix file sockets -[Documentation Page](https://crussell52.github.io/node-socket-ipc/) +[Docs](https://crussell52.github.io/node-socket-ipc/) | +[Source](https://github.com/crussell52/node-socket-ipc/) | +[Releases](https://github.com/crussell52/node-socket-ipc/releases) | +[NPM](https://www.npmjs.com/package/@crussell52/socket-ipc) -## Contents +## Table of Contents -- [About](/README.md) - * [Quick Start](#quick-start) - - [Install](#install) - - [A Simple Example](#a-simple-example) - - [More Examples](#more-examples) - * [Limitations](#limitations) - * [Why another IPC lib?](#why-another-ipc-lib) - - [A Strong Alternative](#a-strong-alternative) - - [Why is this one different?](#why-is-this-one-different) +### [About](/) (you are here) +- [Quick Start](#quick-start) + * [Install](#install) + * [A Simple Example](#a-simple-example) + * [More Examples](#more-examples) +- [Limitations](#limitations) +- [Why another IPC lib?](#why-another-ipc-lib) + * [A Strong Alternative](#a-strong-alternative) + * [Why is this one different?](why-is-this-one-different) -- [API](/docs/API.md) +#### [Usage](/docs/USAGE.md) + +#### [Advanced Usage](/docs/ADVANCED.md) + +#### [API](/docs/API.md) +# About ## Quick Start diff --git a/docs/API.md b/docs/API.md index dda06e1..b2f08a6 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1,6 +1,75 @@ # API -Here are the full details of the `@crussell52/socket-ipc` API. +An event-driven IPC implementation for NodeJS using unix file sockets + +[Docs](https://crussell52.github.io/node-socket-ipc/) | +[Source](https://github.com/crussell52/node-socket-ipc/) | +[Releases](https://github.com/crussell52/node-socket-ipc/releases) | +[NPM](https://www.npmjs.com/package/@crussell52/socket-ipc) + +## Table of Contents + +#### [About](/) + +#### [Usage](/docs/USAGE.md) + +#### [Advanced Usage](/docs/ADVANCED.md) + +### [API](/docs/API.md) (you are here) +- [Recent Changes](#recent-changes) +- [Classes](#classes) + * [Server](#server) + - [new Server()](#server-1) + - [Event: 'close'](#event-close) + - [Event: 'connection'](#event-connection) + - [Event: 'connectionClose'](#event-connectionclose) + - [Event: 'error'](#event-error) + - [Event: 'listening'](#event-listening) + - [Event: 'message'](#event-message) + - [Event: 'message._topic_'](#event-messagetopic) + - [server.close()](#serverclose) + - [server.send(topic, message, clientId)](#serversendtopic-message-clientid) + - [server.listen()](#serverlisten) + - [server.broadcast(topic, message)](#serverbroadcasttopic-message) + * [Client](#client) + - [new Client()](#client-1) + - [Event: 'close'](#event-close-1) + - [Event: 'connect'](#event-connect) + - [Event: 'connectError'](#event-connecterror) + - [Event: 'disconnect'](#event-disconnect) + - [Event: 'error'](#event-error) + - [Event: 'message'](#event-message) + - [Event: 'message._topic_'](#event-messagetopic) + - [Event: 'reconnect'](#event-reconnect) + - [client.close()](#clientclose) + - [client.connect()](#clientconnect) + - [client.send(topic, message)](#clientsendtopic-message) +- [Interfaces (Classes)](#interfaces-classes) + * [Transcoder](#transcoder) + - [transcoder.createDecoder()](#transcodercreatedecoder) + - [transcoder.socketEncoding](#transcodersocketencoding) + - [transcoder.createEncoder()](#transcodercreateencoder) + * [MessageWrapper](#messagewrapper) + - [messageWrapper.topic](#messagewrappertopic) + - [messageWrapper.message](#messagewrappermessage) +- [Interfaces (Callbacks/Functions)](#interfaces-callbacksfunctions) + * [decoderFunc](#decoderfunc) + * [decodedCallback](#decodedcallback) + * [decoderFactoryFunc](#decoderfactoryfunc) + * [encodedCallback](#encodedcallback) + * [encoderFunc](#encoderfunc) + * [encoderFactoryFunc](#encoderfactoryfunc) + +## Recent Changes + + - `v0.2.0`: + * Introduced `transcoder` option for both `Client` and `Server` + * The `messageError` event has been removed. The `error` event has been enhanced to emit an `EncodeError` or + `DecodeError` to cover cases previously covered by `messageError`. This was done to simplify the code and API. + * Some `error` events would include a second, undocumented arg which provided the client id. This is no longer + the case; `error` listeners will now always be given exactly one argument -- the `Error`. + * Calling `client.connect()` or `server.listen()` a second time will now emit an `error` event instead of throwing + the Error. This is more consistent than have a couple of cases which throw instead of emitting an error event. ## Classes @@ -16,106 +85,90 @@ server side of the equation. The server can receive messages from any of the clients. It can also `send()` messsages to a specific client or it can `broadcast()` a message to all connected clients. -#### Constructor +#### new Server(options) + - `options` (`object`) - The server configuration options + * `socketFile` (`string`): The path to the socket file to use when it is told to "listen". See + [`server.listen()`](#serverlisten) for more details on how this file is handled. + * `[transcoder=jsonTranscoder]` (`Transcoder`) - A custom [`Transcoder`](#transcoder). Useful when encoding/decoding + messages with JSON is not sufficient. -Creates a new server, but it does not start listening until you call `server.listen()`. You can immediately -attach listeners to the server instance. +Creates a new server, but it does not start listening until you call [`server.listen()`](#serverlisten). You can +immediately attach listeners to the `Server` instance. + +#### Event: 'close' -Possible signatures: - - `Server(options)` - * `options` (object, required) - The server configuration options - - `socketFile` (string, required): The path to the socket file to use when it is told to "listen". See - `server.listen()` for more details on how this file is handled. +Emitted when the server has stopped listening for connections **and** all existing connections have ended. +#### Event: 'connection' + - `clientId` (`number`) - The id of the client. Use this to send a message to the client. + +Emitted when a client establishes a connection to the server. -#### `server.listen()` +#### Event: 'connectionClose' + - `clientId` (`number`) - The connection id of the client. -Tells the server to start listening for client connections. This is an async operation and the `listening` -event will fire once the server is ready for connections. +Emitted when a client's connection closes for any reason. + +#### Event: 'error' + - `error` (`Error`) - The error that occurred. If the error occurred while encoding a message, it will be an + [`EncodeError`](#encodeerror). Similarly, a decoding error will emit a [`DecodeError`](#decodeerror). -This may only be called **once** per instance. Calling this method a second time will result in an `Error` -being thrown (note, the `error` event will not fire in this case). +Emitted when an error occurs. If the error was the result of a decoding error, the connection to the sender will +be closed. -Possible signatures: - - `server.listen()` +#### Event: 'listening' -#### `server.send(topic, message, clientId)` +Emitted when the server is ready for incoming connections. -Sends a message to a specific, connected, client. On the client-side, this message can be heard by -listening for the `message` or the `message.`_`topic`_ event. +#### Event: 'message' + - `message` (`any`) - The message from the client. By default, this can be any JSON deserializable + type (including `null`). By using of a custom _transcoder_ that can be expanded! + - `topic` (`string`) - The topic of the message as declared by the client. + - `clientId` (`number`) - The id of the client. Use this to send a message to the client. -Possible signatures: - - `server.send(topic, message, clientId)` - * `topic` (string, required) - The topic to publish the message under. If an empty value, `none` is - used as the value. - * `message` (*, required) - The message. May be any JSON serializable value (including `null`) - * `clientId` (number, required) - The id of the client to send the message to. This is usually - obtained by capturing it when the client connects or sends the server a message. Attempting to - send to a clientId which does not exist will fire an `error` event. +Emitted when a message is received, regardless of the _topic_. -#### `server.broadcast(topic, message)` +#### Event: 'message._topic_' + - `message` (`any`) - The message from the client. By default, this can be any JSON deserializable + type (including `null`) but a custom [Transcoder](#transcoder) can be used to influence the type range. + - `clientId` (`number`) - The connection id of the client. -Sends a message to **all** connected clients. On the client-side, this message can be heard by -listening for the `message` or the `message.`_`topic`_ event. +Emitted when a message with the specified _topic_ is received. For example, messages with a _topic_ of "dessert" +would emit the `message.dessert` event. (Yum!) -Possible signatures: - - `server.broadcast(topic, message)` - * `topic` (string, required) - The topic to publish the message under. If an empty value, `none` is +#### server.broadcast(topic, message) + - `topic` (`string`) - The topic to publish the message under. If an empty value, `none` is used as the value. - * `message` (any, required) - The message. May be any JSON serializable value (including `null`) + - `message` (`any`) - The message. May be any JSON serializable value (including `null`) -#### `server.close()` - -Closes all active connections and stops listening for new connections. This is an asynchronous -operation. Once the server is fully closed, the `close` event will be fired. - -Once a server has been "closed", it can not start listening again. A new instance must be created. If -you have a scenario that requires servers to be routinely closed and restarted, a factory function can be -effective for handling the server setup. - -Possible signatures: - - `server.close()` - -#### Events +Sends a message to **all** connected clients. On the client-side, this message can be heard by +listening for the `message` or the `message.`_`topic`_ event. - - `listening` - Fires when the server is ready for incoming connections. +#### server.listen() - - `connection (clientId)` - Fires when a client connects to the server. - * `clientId` (`number`) - The id of the client. Use this to send a message to the client. +Tells the server to start listening for client connections. This is an async operation and the +[`listening`](#event-listening) event will emitted when the server is ready for connections. - - `message (message, topic, clientId)` - Fired whenever a message is received from a client, regardless - of the `topic`. - * `message` (`any`) - The message from the client. By default, this can be any JSON deserializable - type (including `null`). By using of a custom _transcoder_ that can be expanded! - * `topic` (`string`) - The topic of the message as declared by the client. - * `clientId` (`number`) - The id of the client. Use this to send a message to the client. +This may only be called **once** per instance. Calling this method a second time will emit an `error` event. - - `message.`_`topic`_` (message, topic, clientId)` - Fired whenever a message of a specific topic is received. This - is a dynamic event type. If a message with the topic of `desserts` (yum) is receive, it would be published - under the `message.desserts` event. - * `message` (`any`) - The message from the client. By default, this can be any JSON deserializable - type (including `null`). By using of a custom _transcoder_ you control the type range! - * `clientId` (`number`) - The id of the client. Use this to send a message to the client. +#### server.send(topic, message, clientId) + - `topic` (`string`) - The topic to publish the message under. If an empty value is given, `none` is + used as the message topic. + - `message` (`*`) - The message. May be any JSON serializable value (including `null`) + - `clientId` (`number`) - The id of the client to send the message to. This is usually + obtained by capturing it when the client connects or sends the server a message. - - `connectionClose (clientId)` - Fires when a client's connection closes. - * `clientId` (`number`) - The id of the client. Do not send messages to clients that have disconnected. - - - `close` - Fires when the server is closed and all connections have been ended. +Sends a message to a specific, connected, client. On the client-side, this message can be heard by +listening for the `message` or the `message.`_`topic`_ event. + +#### server.close() - - `error (error)` - Fires when an error occurs. `Node` provides special treatment of `error` events; if you do not - listen for this event, it will throw the `Error`. If this is the result of an error while decoding messages, the - connection to the client that sent the message will be closed. - * `error` (`Error`) - The error that occurred. If the error occurred while encoding a message, it will be an - `EncodeError`. Similarly a decoding error will emit a `DecodeError`. - -**Recent Changes** +Closes all active connections and stops listening for new connections. This is an asynchronous +operation. Once the server is fully closed, the `close` event will be emitted. - - `v0.2.0`: - * The `messageError` event has been removed. The `error` event has been enhanced to emit an `EncodeError` or - `DecodeError` to cover cases previously covered by `messageError`. This was done to simplify the code and API. - * Some `error` events would include a second, undocumented arg which provided the client id. This is no longer - the case. - +Once a server has been "closed", it can not start listening again. A new instance must be created. If +you have a scenario that requires servers to be routinely closed and restarted, a factory function can be +effective of handling the server setup. ### Client @@ -123,106 +176,116 @@ This library follows a standard server/client pattern. There is one server which from one or more clients. Intuitively, the `Client` class provides the interface for establishing the client side of the equation. -The client can receive messages from the server and tt can `send()` messsages. +The client can receive messages from the server and it can [`send()`](#clientsend) messages to the server. -#### Constructor +#### new Client(options) + - `options` (`object`) - The client configuration options + * `socketFile` (`string`): The path to the socket file to connect to. + * `[transcoder=jsonTranscoder]` (`Transcoder`) - A custom [`Transcoder`](#transcoder). Useful when encoding/decoding + messages with JSON is not sufficient. + * `[retryDelay=1000]` (`number`) - The number of milliseconds to wait between connection attempts. + * `[reconnectDelay=100]` (`number`) - The number of milliseconds to wait before automatically + reconnecting after an unexpected disconnect. Creates a new client, but it does not connect until you call `client.connect()`. You can immediately attach listeners to the client instance. -Possible signatures: - - `Client(options)` - * `options` (object, required) - The client configuration options - - `socketFile` (string, required): The path to the socket file to use to establish a connection. - - `retryDelay` (number, optional, default=1000) - The number of milliseconds to wait between connection attempts. - - `reconnectDelay` (number, optional, default=100) - The number of milliseconds to wait before automatically - reconnecting after an unexpected disconnect. +#### Event: 'close' -#### `client.connect()` +Emitted when [`client.close()`](#clientclose) has been called **and** the client has disconnected from the server. -Tells the client to connect to the server. This is an async operation and the `connect` event will fire once the -a connection has been established. +#### Event: 'connect' + +Emitted when the `Client` establishes its **initial** connection with the server. -This may only be called **once** per instance. Calling this method a second time will result in an `Error` -being thrown (note, the `error` event will not fire in this case). +Note: This is distinct from the [`reconnect`](#event-reconnect) event which is emitted after the client has experienced +an unexpected disconnect and successfully reconnects to the server. -If the server is unavailable when the client attempts to connect, a `connectError` event will be fired and the client will -automatically retry after a delay defined by the `options.retryDelay` value that was passed into the constructor. This -cycle of a `connectError` event followed by a delayed retry will continue to happen until a connection is established or -until `client.close()` is called. If you want to limit the number of retries, you can count the `connectError` events and -call`client.close()` after some threshold. +#### Event: 'connectError' + - `error` (`Error`) - The error that occurred. + +Emitted when a connection attempt fails. -Once connected, if an unexpected disconnect occurs (e.g. not an explicit call to `client.close()`) a `disconnect` event -will be fired and the client will automatically start attempting to reconnect to the server. The connection process will -occur as described above, including the automatic retry behavior. The only difference is that, once a new connection is -established, a `reconnect` event will fire instead of a `connect` event. +This event is common when the server is not yet listening. Because of the auto-retry mechanism, this event may be +emitted several times while the client waits for the server to start listening. For some applications, waiting "forever" +for the server to start may make sense; for others, you can use this event count the number of connection attempts and +"give up" after some limit. -Possible signatures: - - `client.connect()` +#### Event: 'disconnect' + +Emitted when a client unexpectedly loses connection. This is distinct from the [`close`](#event-close-1) event that is +emitted when the client disconnects because [`client.close()`](#clientclose) was called. -#### `client.send(topic, message)` +#### Event: 'error' + - `error` (`Error`) - The error that occurred. If the error occurred while encoding a message, it will be an + [`EncodeError`](#encodeerror). Similarly, a decoding error will emit a [`DecodeError`](#decodeerror). -Sends a message to the server. On the server-side, this message can be heard by listening for the `message` or the `message.`_`topic`_ event. +Emitted when an error occurs. If the error was the result of a decoding error, the connection to the sender will +be closed. -Possible signatures: - - `client.send(topic, message)` - * `topic` (string, required) - The topic to publish the message under. If an empty value, `none` is - used as the value. - * `message` (*, required) - The message. May be any JSON serializable value (including `null`) - -#### `client.close()` +#### Event: 'message' + - `message` (`any`) - The message from the client. By default, this can be any JSON deserializable + type (including `null`). By using of a custom _transcoder_ that can be expanded! + - `topic` (`string`) - The topic of the message as declared by the client. + - `clientId` (`number`) - The id of the client. Use this to send a message to the client. -Permanently closes the connection. There will be no automatic reconnect attempts. This is an aysnchronous -operation; the `close` event will fire after the close operation is complete. +Emitted when a message is received, regardless of the _topic_. -Once a client has been "closed", it can not reconnect. A new instance must be created. If you have a scenario that -requires clients to be routinely closed and restarted, a factory function can be effective for handling the client -setup. - -Possible signatures: - - `client.close()` +#### Event: 'message._topic_' + - `message` (`any`) - The message from the client. By default, this can be any JSON deserializable + type (including `null`) but a custom [Transcoder](#transcoder) can be used to influence the type range. + - `clientId` (`number`) - The connection id of the client. -#### Events +Emitted when a message with the specified _topic_ is received. For example, messages with a _topic_ of "dessert" +would emit the `message.dessert` event. (Yum!) - - `connectError (error)` - Fires when a connection attempt fails and a connection retry is queued. - * `error` (`Error`) - The error that occurred. +#### Event: 'reconnect' - - `connect` - Fires when the client establishes an **initial** connection with the server. +- Emitted when the client successfully **reconnects** with the server after an unexpected disconnect. This is distinct +from the [`connect`](#event-connect) that is emitted when the server successfully establishes its initial connection +with the server. - - `disconnect` - Fires when a client unexpectedly loses connection. This is distinct from the `close` event which - indicates completion of a deliberate call to `client.close()`. +#### client.close() - - `reconnect` - Fires when the client reestablishes a connection with the server after an unexpected disconnect. +Permanently closes the connection. There will be no automatic reconnect attempts. This is an asynchronous +operation; the [`close`](#event-close-1) event will be emitted when the close operation is complete. - - `message (message, topic)` - Fired whenever a message is received from the server, regardless of the `topic`. - * `message` (`any`) - The message from the client. By default, this can be any JSON deserializable - type (including `null`). By using of a custom _transcoder_ you control the type range! - * `topic` (string) - The topic of the message as declared by the server. - - - `message.`_`topic`_` (message, topic, clientId)` - Fired whenever a message of a specific topic is received. This - is a dynamic event type. If a message with the topic of `desserts` (yum) is receive, it would be published - under the `message.desserts` event. - * `message` (`any`) - The message from the client. By default, this can be any JSON deserializable - type (including `null`). By using of a custom _transcoder_ you control the type range! - - - `close` - Fires when the client is fully closed after a call to `client.close()`. - - - `error (error)` - Fires when an error occurs. `Node` provides special treatment of `error` events; if you do not - listen for this event, it will throw the `Error`. If this is the result of an error while decoding messages, the - connection to the server that sent the message will be closed. - * `error` (`Error`) - The error that occurred. If the error occurred while encoding a message, it will be an - `EncodeError`. Similarly a decoding error will emit a `DecodeError`. - -**Recent Changes** +Once a client has been "closed", it can not reconnect. A new instance must be created. If you have a scenario that +requires clients to be routinely closed and restarted, a factory function can be effective for handling the client +setup. - - `v0.2.0`: - * The `messageError` event has been removed. The `error` event has been enhanced to emit an `EncodeError` or - `DecodeError` to cover cases previously covered by `messageError`. This was done to simplify the code and API. - * Some `error` events would include a second, undocumented arg which provided the client id. This is no longer - the case. +#### client.connect() + +Tells the client to connect to the server. This is an async operation and the [`connect`](#event-connect) event will +be emitted once the connection has been established. + +This may only be called **once** per instance. Calling this method a second time will emit an +[`error`](#event-error-1) event. + +If the server is unavailable when the client attempts to connect, a [`connectError`](#event-connecterror) event will be +emitted and the client will automatically retry after a delay defined by the value of `options.retryDelay`. This +sequence (`connectError` event followed by a delayed retry) will repeat until a connection is established or until +[`client.close()`](#clientclose) is called. If you want to limit the number of retries, you can count the `connectError` +events and call`client.close()` after some threshold. + +Once connected, if an unexpected disconnect occurs (e.g. not an explicit call to `client.close()`) a +[`disconnect`](#event-disconnect) event will be emitted and the client will automatically start attempting to reconnect +to the server. The reconnection routine is almost identical to the connection routine described above, including the +automatic retry behavior. The only difference is that a successful connection will emit a [`reconnect`](#event-reconnect) +event instead of a `connect` event. + +#### client.send(topic, message) + - `topic` (string, required) - The topic to publish the message under. If an empty value, `none` is + used as the value. + - `message` (*, required) - The message. May be any JSON serializable value (including `null`) + +Sends a message to the server. On the server-side, this message can be heard by listening for the +[`message`](#event-message-1) or the [`message.`_`topic`_](#event-messagetopic-1) event. ### EncodeError ### DecodeError -## Interfaces \ No newline at end of file +## Interfaces (Classes) + +## Interfaces (Callbacks/Functions) \ No newline at end of file diff --git a/docs/USAGE.md b/docs/USAGE.md index 890a666..fc2f835 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -24,5 +24,11 @@ module's documentation. Like any good module, `socket-ipc` tries to hide this co and running fast. But if this is your first time stepping into this territory, it might still be worth the effort to learn a bit about them. +## Automatic Retry + +## Automatic Reconnect + +## Working with client ids + ## Throughput