@dasnoo/arsocket-server
v0.0.38
Published
A dead easy websocket framework
Downloads
48
Maintainers
Readme
AR Socket
A dead easy websocket framework that acts as a layer on top of commonly used websocket technologies to furnish a standardized API.
Table of Contents
- Prerequisites
- Why use AR Socket
- Features
- Architecture
- Learn by example
- Documentation Server
- Documentation Client
- In development
- Delving into the source code
Prerequisites
Why use AR Socket
Which websocket library should you use ? socket.io, sockjs, uws ? What if you want to change the underlying library ? Will you have to change your codebase ? The answer to these questions is simple, AR Socket. AR Socket is a wrapper around commonly used websocket libraries. As such there is a single, simple, standardized API that controls them all.
For example if you'd like to use engine.io, you could use AR Socket instead and if in the future you'd like to change µWebSocket, your code base doesn't have to change.
Features
- Simple reactive API
- Receive / send messages to client / server
- Middleware support
- Broadcast messages
- Room system
- Typescript
- Integrates well with Angular
Architecture
Every message from the client to the server is called an Action
. Every message from the server to the client(s) is called a Reaction
. That's where the name AR Socket comes from. Action Reaction Socket
. The client sends an action to the server, which reacts.
Both action and reaction contain a type and a payload. As such a typical Action
or Reaction
object would be this:
const actionOrReaction = { type: 'NEW_MESSAGE', payload: 'hey' };
There is one central object, the Socket
, to which you can subscribe
to the different Action/Reaction
.
Client example:
const socket = new Socket();
socket.select('NEW_MESSAGE')
.subscribe(payload => console.log(`server says ${payload}`));
Server example:
// only difference with client is that
// the subscribe method on the server side receive an object { payload, connection }.
const socket = new Socket();
socket.select('NEW_MESSAGE')
.subscribe( ({ payload }) => console.log(`client says ${payload}`));
That's it! If you understood the above you already understand the framework.
Learn by example:
We are gonna set up a simple project to get the hang of it quickly. For this project we will
use typescript. You can install/update it by running the command npm i -g typescript@latest
.
We are using webpack npm i -g webpack
1 setting up example project:
Let's create a new project (we are using type script here)
Here is a command line to get started:
mkdir arsocket-test && cd arsocket-test && mkdir public && npm init -y && tsc --init --target "ES2016" && npm i @dasnoo/arsocket-server
Or you can do it manually:
- Create arsocket-test directory
- Create public directory inside arsocket-test
- initialize package.json run
npm init -y
- initialize typescript run
tsc --init --target "ES2016"
- Install arsocket-server
npm i @dasnoo/arsocket-server
2. Creating the server:
Create app.ts
and put the code below in it
import { Socket } from '@dasnoo/arsocket-server';
const socket = new Socket({staticDir: './public'});
socket.addRoutes([{type: 'NEW_MESSAGE', handler: onMessage}]);
function onMessage(payload: any){
socket.broadcast({type: 'NEW_MESSAGE', payload});
}
3. Note
The routing code is equivalent to the previously presented:
socket.select('NEW_MESSAGE').subscribe(onMessage);
However addRoute is more recommended.
4. Adding the Client
let's create index.html and put it in the public directory:
<!DOCTYPE html>
<html lang="en">
<head>
<title>ARSocket</title>
</head>
<body>
<input id="inp" type="text" placeholder="Type here">
<input id="send" type="submit" value="Send">
<div id="msgs"></div>
<script src="/chat.bundle.js"></script>
</body>
</html>
and chat.ts:
import { Socket } from '@dasnoo/arsocket-client';
const inp = document.getElementById('inp') as HTMLInputElement;
const sendBtn = document.getElementById('send') as HTMLElement;
const msgs = document.getElementById('msgs') as HTMLElement;
// creating socket
const socket = new Socket();
socket.init();
// selecting entering messages
socket.select('NEW_MESSAGE').subscribe(onNewMessage)
function onNewMessage(payload: any){
msgs.innerHTML += payload + '<br/>';
}
// sending message on button click
sendBtn.addEventListener('click', evt => {
socket.send('NEW_MESSAGE', inp.value)
});
5. Run the app:
Let's compile and run
tsc && webpack ./public/chat.js ./public/chat.bundle.js && node app
You can now visit http://localhost:3000 to find your billion dollar app.
Documentation server:
Installation:
npm i @dasnoo/arsocket-server
Server Api:
Routes
Most of your interaction with the socket should be through routes. As such, it makes sens to present those first.
export interface Route{
type: string;
handler: (payload?: any, connection?: Connection) => any;
middlewares?: Array<Middleware>;
}
- Type is the type of Action the route is interested in. Example: 'NEW_MESSAGE'
- handler is the function that is gonna deal with the action
- middlewares: is an array of function that are going to be executed before the handler
Usage example:
const routes: Array<Route> = [
{ type: "NEW_MESSAGE", handler: onMessage},
{ type: "JOIN", handler: onJoin, middlewares: [authorize]}
];
You'd add those route like so :
socket.addRoutes(routes)
The handler, the important bit:
What you return from your handler is going to dictate what is going to happen. There is 3 scenarios:
- a Promise : When the promise resolves, the value it resolved to goes to the two cases below
- undefined : Nothing happens
- anything else : The server sends a response back to the client
Middlewares
Middlewares are executed before the handler if they return true or a Promise then the next middleware / handler is executed. If alternatively any of them returns false the flow is stopped.
The socket:
The interface of Socket should be self explanatory.
export interface ISocket {
readonly socket: any;
readonly userContainer: UserContainer;
// addd route handlers
addRoutes: (routerConfig: Array<Route>) => Socket;
// sends reaction to one specific connection.
send: (connection: Connection, reaction: Reaction<any>) => Socket;
// sends reaction to all with omition of the specified by omit. Only in room if specified
broadcast: (reaction: Reaction<any>, omit?: Connection, roomname?: string) => Socket;
// addroutes should be used instead, this is for convenience
select: (type: string) => Observable<{payload: any, connection: Connection}>;
addUserToRoom: (roomname: string, conn: Connection) => Socket;
removeUserFromRoom: (roomname: string, conn: Connection) => Socket
}
You can specify SocketOpts
in the constructor of the socket class:
export class Socket implements ISocket{
constructor(opts: SocketOpts = {}) {
//..
}
}
The options:
Here are the default options:
export const DEFAULT_OPTS: SocketOpts = {
path: 'arsocket',
logLevel: 'debug',
port: 3000,
staticDir: '',
customServer: false,
engine: Engines.uws
};
- path is the prefix to which the client is going to connect to.
- Custom server, is whether you supplied your own http server
- Engine is the underlying engine used. The default is uws.
Documentation client:
Installation:
npm i @dasnoo/arsocket-client
Client Api:
The client side api is very simple let's take a look at it :
export interface ISocket {
readonly socket: any;
init:(options: SocketOpts) => Socket | undefined;
send:(type: string, payload?: any, metadata?: {}) => Observable<any>;
select:(type: string) => Observable<any>
}
- socket is the actual socket given by the engine
- send is the method used to send messages to the server
- select is used to select messages from the server
The options:
The options should be self explanatory.
export const DEFAULT_OPTS_CLIENT: SocketOpts = {
host: 'localhost',
port: 3000,
path: 'arsocket',
secure: false,
engine: Engines.websocket,
reconnectionDelay: 200,
reconnectionDelayMax: 5000
};
This will connect to a socket on ws://localhost:3000/arsocket, if it doesn't manage to connect it will retry 200ms after that then 400ms then 800ms until it's capped at 5000ms.
Extending the socket:
One can extend the socket to give it additional functionalities. Here is an example of a socket that will send a JWT token when the connection hasn't been authenticated on the server :
export class AuthSocket extends Socket implements ISocket{
private isAuthenticatedOnServer: boolean;
private token: string | undefined;
constructor(){
super();
this.select('auth')
.subscribe(r => this.onAuth(r));
}
send(type: string, payload?: any, metadata: any = {}){
if(!this.isAuthenticatedOnServer && this.token){
metadata.auth = { token: this.token };
}
return super.send(type, payload, metadata);
}
onAuth(r){
this.isAuthenticatedOnServer = true;
}
}
With that one can create a middleware on the server to check if metadata.auth
is present and
if so authenticate the connection with the token.
In development
- Redis adaptater to scale accross process
- Tests
Delving into the source code
Prerequisite
Since the library makes use of Rxjs, knowing what's an Observable is a good idea.
Visual
The Bridge overview
The Bridge
is used when the Socket
object must interact with the underlying engine. Since there are many different websocket engine out there, we need a Bridge
.
Bridge class server side:
export class Bridge{
// engine specific implementation of the bridge
engineBridge: any;
setEngine(engine: string){
switch(engine){
case "engine.io":
this.engineBridge = new EngineIOBridge(); break;
case 'uws':
this.engineBridge = new UwsBridge(); break;
// etc..
default:
throw Error(`${engine} hasn't been implemented yet.`);
}
}
onConnection(socket, fn: Function){
return this.engineBridge.onConnection(socket, fn);
}
// etc ..
}
Thus when the socket wants to interacts with the underlying engine, it does so via the Bridge
. The bridge in turn talks to the correct bridge implementation. This is the only point in the source code where there is interactions with the engines.
If an engine specific bridge is not implemented yet, it's easy to write one in 5 minutes. You just have to implement the IBridge
interface (if you are using typescript) or copy the other bridges if you are using regular javascript. Just take a look at SockJSBridge
. It's that simple.
Event handler
The Event handler is responsible for pushing events. Take a look at the code below where the event handler adds event to the _connection Rx.Subject.
export class SocketEventHandler{
// subject for pushing events
private _connection = new Subject<Connection>();
// observable to subscribe
public connection$ = this._connection.asObservable();
constructor(private bridge: Bridge){}
addEventHandlers(socket:any){
this.bridge.onConnection(socket, (engineConnection: any) => {
const connectionWrapper: Connection = { engineConnection };
this._connection.next(connectionWrapper);
});
}
}
The router
Here is a simplified version of the router, to get the basic idea.
export class Router{
constructor(private evtHandler: SocketEventHandler, private routeConfig: Array<Route> ){
// 1. we add the routes to object for easy access
this.addRoutes(routeConfig)
// 2. on action we route to the correct handler
evtHandler.action$.subscribe(a => this.route(a));
}
private addRoutes(routeConfig){
routeConfig.forEach(r => this.addRoute(r));
}
private addRoute(route: Route){
this.routes[route.type] = route;
}
private route(a: ActionEvent){
const r = this.routes[a.action.type];
if(r)
r.handler(a.action.payload, a.connection);
}