Skip to content

Commit

Permalink
Merge pull request #7 from JohnBra/release/v2.0.0
Browse files Browse the repository at this point in the history
Release/v2.0.0
  • Loading branch information
JohnBra committed Nov 1, 2020
2 parents 05e5b2b + 36e58ac commit fffdae2
Show file tree
Hide file tree
Showing 14 changed files with 146 additions and 41 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.0.0] - 2020-11-01
### Added
- Documentation, more usage examples in README.md, comments in code

### Changed
- BREAKING: MessageHandler process interface to take calling context as first parameter, to handle 'this' in function call properly. This WILL BREAK custom message handler implementations. If you used the provided message handlers, the changes are backwards compatible.

## [1.0.3] - 2020-09-03
### Changed
- fixed main file path in package json
Expand Down
101 changes: 86 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ Wraps the popular [ws](https://github.com/websockets/ws) lib.
- [Usage examples](#usage-examples)
- [Create namespaces for your rpc](#create-namespaces-for-your-rpc)
- [Server](#server)
- [SimpleMessageHandler](#simplemessagehandler)
- [Overriding provided WebSocketServer functionality](#overriding-provided-websocketserver-functionality)
- [Changelog](#changelog)
- [Contributing](#contributing)
- [License](#license)
Expand Down Expand Up @@ -45,22 +47,22 @@ Add experimental decorators and emit metadata to your `tsconfig.json`
```
## Features, limitations and possible features to be added
### This lib offers the following out of the box:
* Extensive documentation for ease of development
* Retains all functionality of the [ws](https://github.com/websockets/ws) lib
* RPC namespace creation
* [JSON RPC 2](https://www.jsonrpc.org/specification) conform message handler (incl. errors, responses and the like)
* Simple message handler (super simplistic message handler)
* Easily readable and maintainable registration of namespace methods with decorators
* Convenience methods to interact with clients (e.g. broadcast messages to all clients). *You are also able to reimplement all ws listeners and convenience methods if you wish*
* Defined interfaces to implement your own custom message handlers
- Extensive documentation for ease of development
- Retains all functionality of the [ws](https://github.com/websockets/ws) lib
- RPC namespace creation
- [JSON RPC 2](https://www.jsonrpc.org/specification) conform message handler (incl. errors, responses and the like)
- Simple message handler (super simplistic message handler)
- Easily readable and maintainable registration of namespace methods with decorators
- Convenience methods to interact with clients (e.g. broadcast messages to all clients). *You are also able to override all ws listeners and convenience methods if you wish*
- Defined interfaces to implement your own custom message handlers

### This lib does **NOT** offer the following:
* Batch request handling
* Runtime parameter typechecking on remote procedure call
- Batch request handling
- Runtime parameter typechecking on remote procedure call

### Possible features to be added in the future:
* [Swagger](https://swagger.io/) like documentation generation with [OpenRPC](https://open-rpc.org/) as model
* Protected methods (require authentication before calling rpc)
- [Swagger](https://swagger.io/) like documentation generation with [OpenRPC](https://open-rpc.org/) as model
- Protected methods (require authentication before calling rpc)

## Usage examples

Expand Down Expand Up @@ -117,9 +119,9 @@ const app = express();
const server = http.createServer(app);

// pass message handler instances and WebSocket.ServerOptions to the respective namespaces
const namespaceA = new RPCNamespaceA(new SimpleMessageHandler(), { noServer: true });
const namespaceA = new NamespaceA(new SimpleMessageHandler(), { noServer: true });
// use different message handlers for different namespaces
const namespaceB = new RPCNamespaceB(new JSONRPC2MessageHandler(), { noServer: true });
const namespaceB = new NamespaceB(new JSONRPC2MessageHandler(), { noServer: true });


server.on('upgrade', function upgrade(request, socket, head) {
Expand All @@ -143,7 +145,76 @@ server.listen(10001, '0.0.0.0', 1024, () => {
});
```

That's it!
That's it for the server!

### SimpleMessageHandler
Once you have started the server, you can start firing away messages to the implemented endpoints. Provided the example code above, we have two endpoints:
- ws://localhost:10001/a (SimpleMessageHandler)
- ws://localhost:10001/b (JSONRPC2MessageHandler)

Once you have connected to the endpoint with the **SimpleMessageHandler** you have to adhere to the defined message format:
- Incoming messages must be of type string or Buffer
- After reading the string or Buffer, the RPC must be an object
- The object must have the "method" field with a value of type string
- The object can have the "params" key. It may also be omitted.
- If provided, the "params" field must either be of type object (named parameters), or of type array (positional parameters)

Valid remote procedure calls for the SimpleMessageHandler

Positional parameters:
```json
{
"method": "sum",
"params": [1, 2]
}
```
Named parameters:
```json
{
"method": "sum",
"params": { "b": 2, "a": 1 }
}
```
Omitted parameters:
```json
{
"method": "doSomething"
}
```

### Overriding provided WebSocketServer functionality
Currently, the [WebSocketServer](https://github.com/JohnBra/rpc-websocketserver/blob/master/src/lib/websocket-server.ts#L21) offers the following functionality out of the box:
- **Public** function to **retrieve all registered methods** for the specific namespace
- **Public** function to **broadcast a message** to all clients of this namespace
- **Protected** function to **send a message** to a specific client
- **Protected** function to **set ws listeners** once a connection was established
- **Protected** function to **handle received messages**

All protected functions can be overridden for your specific namespaces. You are encouraged to override the 'onConnection' handler with handlers for the possible [ws events](https://github.com/websockets/ws/blob/master/doc/ws.md#event-close-1) (e. g. error) like so:
```typescript
import WebSocket from 'ws';
import { MessageHandler, WebSocketServer, register, param } from 'rpc-websocketserver';

// inherit from WebSocketServer
class NamespaceA extends WebSocketServer {
constructor(messageHandler: MessageHandler, options: WebSocket.ServerOptions) {
super(messageHandler, options);
}

@register()
sum(@param('a') a: number, @param('b') b: number) {
return a + b;
}

// overriding the onConnection handler to add more event listeners once a connection is established
protected _onConnection(ws: WebSocket): void {
super._onConnection(ws);
ws.addListener('error', (err: Error) => console.log(err));
}
}
```

This inheritance based approach should facilitate your own implementation for custom error/message handling, logging, clean up functionality on close events and so on.

## Changelog
[Changelog](https://github.com/JohnBra/rpc-websocketserver/blob/master/CHANGELOG.md)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "rpc-websocketserver",
"version": "1.0.3",
"version": "2.0.0",
"description": "Simple rpc websocket server, wrapping the very popular 'ws' library. Register your RPCs with convenient decorators.",
"keywords": [
"RPC",
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/decorators.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'reflect-metadata';
import { PARAM_NAMES_KEY, param, register } from '../lib/decorators';
import { WebSocketServer } from '../lib/websocket-server';
import {MessageHandler, Method} from '../lib/interfaces';
import { MessageHandler } from '../lib/interfaces';


class MockNamespace extends WebSocketServer {
Expand Down
1 change: 0 additions & 1 deletion src/__tests__/json-rpc-2/errors.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
JSONRPC2Error,
ParseError
} from '../../lib/json-rpc-2/errors';
import exp = require("constants");

describe('JSONRPC2Error base class', () => {
it('should not throw error on constructor call', () => {
Expand Down
1 change: 0 additions & 1 deletion src/__tests__/json-rpc-2/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { assertValidJSONRPC2Request, buildError, buildResponse } from '../../lib/json-rpc-2/utils';
import { ResponseObject } from '../../lib/json-rpc-2/interfaces';

describe('assertValidJSONRPC2Request', () => {
const validRequestA = { jsonrpc: '2.0', method: 'foo', params: [], id: 1 };
Expand Down
10 changes: 5 additions & 5 deletions src/__tests__/message-handlers/json-rpc-2.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import JSONRPC2MessageHandler from '../../lib/message-handlers/json-rpc-2';
import { HandlerResult, Method } from '../../lib/interfaces';
import { NOOP } from "../../lib/constants";
import { NOOP } from '../../lib/constants';

function assertString(val: any): asserts val is string {
if (typeof val !== 'string') throw Error('Value is not of type string');
Expand Down Expand Up @@ -104,7 +104,7 @@ describe('JSONRPC2MessageHandler class', () => {
args: [0, 1]
};
const messageHandler = new JSONRPC2MessageHandler();
const res = await messageHandler.process(mockHandlerResult);
const res = await messageHandler.process({}, mockHandlerResult);

expect(res).toBeDefined();
expect(typeof res === 'string').toBe(true);
Expand All @@ -126,7 +126,7 @@ describe('JSONRPC2MessageHandler class', () => {
args: [0, 1]
};
const messageHandler = new JSONRPC2MessageHandler();
const res = await messageHandler.process(mockHandlerResult);
const res = await messageHandler.process({}, mockHandlerResult);

expect(res).toBeUndefined();
});
Expand All @@ -143,7 +143,7 @@ describe('JSONRPC2MessageHandler class', () => {
args: [0, 1]
};
const messageHandler = new JSONRPC2MessageHandler();
const res = await messageHandler.process(mockHandlerResult);
const res = await messageHandler.process({}, mockHandlerResult);

expect(res).toBeDefined();
expect(typeof res === 'string').toBe(true);
Expand All @@ -166,7 +166,7 @@ describe('JSONRPC2MessageHandler class', () => {
args: [0, 1]
};
const messageHandler = new JSONRPC2MessageHandler();
const res = await messageHandler.process(mockHandlerResult);
const res = await messageHandler.process({}, mockHandlerResult);

expect(res).toBeDefined();
expect(typeof res === 'string').toBe(true);
Expand Down
14 changes: 7 additions & 7 deletions src/__tests__/message-handlers/simple.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {HandlerResult, Method} from '../../lib/interfaces';
import SimpleMessageHandler from "../../lib/message-handlers/simple";
import { NOOP } from "../../lib/constants";
import { HandlerResult, Method } from '../../lib/interfaces';
import SimpleMessageHandler from '../../lib/message-handlers/simple';
import { NOOP } from '../../lib/constants';

describe('SimpleMessageHandler class', () => {
let registeredMethodA: Method;
Expand Down Expand Up @@ -78,7 +78,7 @@ describe('SimpleMessageHandler class', () => {
args: [arg0, arg1]
};
const messageHandler = new SimpleMessageHandler();
const res = await messageHandler.process(mockHandlerResult);
const res = await messageHandler.process({}, mockHandlerResult);

expect(res).toBeDefined();
expect(res).toEqual(expectedResult);
Expand All @@ -93,7 +93,7 @@ describe('SimpleMessageHandler class', () => {
args: ['abc', 1]
};
const messageHandler = new SimpleMessageHandler();
const res = await messageHandler.process(mockHandlerResult);
const res = await messageHandler.process({}, mockHandlerResult);

expect(res).toBeUndefined();
});
Expand All @@ -108,7 +108,7 @@ describe('SimpleMessageHandler class', () => {
args: []
};
const messageHandler = new SimpleMessageHandler();
const res = await messageHandler.process(mockHandlerResult);
const res = await messageHandler.process({}, mockHandlerResult);

expect(res).toEqual('Internal server error');
console.log = originalLog;
Expand All @@ -122,7 +122,7 @@ describe('SimpleMessageHandler class', () => {
args: []
};
const messageHandler = new SimpleMessageHandler();
const res = await messageHandler.process(mockHandlerResult);
const res = await messageHandler.process({}, mockHandlerResult);

expect(res).toEqual(mockHandlerResult.data);
});
Expand Down
1 change: 0 additions & 1 deletion src/__tests__/websocket-server.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { register } from '../lib/decorators';
import express from 'express';
import http from 'http';
import url from 'url';
import mock = jest.mock;

async function sleep(ms: number): Promise<void> {
return new Promise<void>(resolve => {
Expand Down
10 changes: 9 additions & 1 deletion src/lib/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ export type HandlerResult = {
args: MethodArgs;
};

/**
* Describes a message handler including functions to handle incoming requests as well as
* processing of the handled result
*/
export interface MessageHandler {
/**
* Should implement message parsing, request validation, method validation and param validation
Expand All @@ -62,8 +66,12 @@ export interface MessageHandler {
/**
* Should implement execution of method and return undefined or data to reply to client
*
* @param context {any} - context of the calling class to properly handle 'this' in the function call
* @param handlerResult {HandlerResult} - message handler result
* @returns {WebSocket.Data | undefined | Promise<WebSocket.Data | undefined>}
*/
process(handlerResult: HandlerResult): WebSocket.Data | undefined | Promise<WebSocket.Data | undefined>;
process(
context: any,
handlerResult: HandlerResult,
): WebSocket.Data | undefined | Promise<WebSocket.Data | undefined>;
}
5 changes: 3 additions & 2 deletions src/lib/message-handlers/json-rpc-2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,19 @@ class JSONRPC2MessageHandler implements MessageHandler {
/**
* Function to process handler result. Should call rpc and return a JSON RPC 2 conform response
*
* @param context {any} - context of the calling class to properly handle 'this' in the function call
* @param handlerResult {HandlerResult} - handler result from same message handler
* @returns {Promise<WebSocket.Data | undefined>}
*/
async process(handlerResult: HandlerResult): Promise<WebSocket.Data | undefined> {
async process(context: any, handlerResult: HandlerResult): Promise<WebSocket.Data | undefined> {
const { error, data, func, args } = handlerResult;
const isNotification = !data.hasOwnProperty('requestId');
const requestId = handlerResult.data.requestId ?? null;

let jsonRpc2Response;
if (!error) {
try {
const executionResult = await func(...args);
const executionResult = await func.call(context, ...args);
// only build response if request wasn't a notification
if (!isNotification) jsonRpc2Response = buildResponse(false, requestId, executionResult);
} catch (err) {
Expand Down
28 changes: 25 additions & 3 deletions src/lib/message-handlers/simple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,31 @@ import { NOOP } from '../constants';
/**
* Minimalist message handler
*
* - Incoming messages must be of type string or Buffer
* - After reading the string or Buffer, the RPC must be an object
* - The object must have the "method" key
* - The value of the "method" field must be of type string
* - The object can have the "params" key (it can also be omitted)
* <br/>
*
* @implements {MessageHandler}
*
* @example Valid message with positional parameters
* {
* "method": "sum",
* "params": [1, 2]
* }
* @example Valid message with named parameters
* {
* "method": "sum",
* "params": { "b": 1, "a": 2 }
* }
* @example Valid message with named parameters omitted
* {
* "method": "sum"
* }
*/
class SimpleMessageHandler implements MessageHandler {

/**
* Handles an incoming message
*
Expand All @@ -35,15 +56,16 @@ class SimpleMessageHandler implements MessageHandler {
/**
* Function to process handler result. Should call rpc and return data to be sent to clients
*
* @param context {any} - context of the calling class to properly handle 'this' in the function call
* @param handlerResult {HandlerResult} - handler result from same message handler
* @returns {Promise<WebSocket.Data | undefined>}
*/
async process(handlerResult: HandlerResult): Promise<WebSocket.Data | undefined> {
async process(context: any, handlerResult: HandlerResult): Promise<WebSocket.Data | undefined> {
const { error, data, func, args } = handlerResult;
let response;
if (!error) {
try {
response = await func(...args);
response = await func.call(context, ...args);
} catch (err) {
console.log(err);
response = 'Internal server error';
Expand Down
3 changes: 1 addition & 2 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ export interface ErrorType<T extends Error> extends Function {
* @param Err {ErrorType} - Error to be thrown if assertion fails
*/
export function assertStringOrBuffer(val: any, Err: ErrorType<Error>): asserts val is string | Buffer {
if (!Buffer.isBuffer(val) && typeof val !== 'string')
throw new Err(`Message must be of type 'string' or 'Buffer'`);
if (!Buffer.isBuffer(val) && typeof val !== 'string') throw new Err(`Message must be of type 'string' or 'Buffer'`);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/lib/websocket-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export abstract class WebSocketServer {
protected async _onMessage(ws: WebSocket, message: WebSocket.Data): Promise<void> {
try {
const handlerResult = this._messageHandler.handle(message, this._namespaceMethods);
const res = await this._messageHandler.process(handlerResult);
const res = await this._messageHandler.process(this, handlerResult);
if (res) this._sendMessage(ws, res);
} catch (err) {
// do nothing or potentially log error
Expand Down

0 comments on commit fffdae2

Please sign in to comment.