npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@dasnoo/arsocket-server

v0.0.38

Published

A dead easy websocket framework

Downloads

5

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

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

visual

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:

  1. Create arsocket-test directory
  2. Create public directory inside arsocket-test
  3. initialize package.json run npm init -y
  4. initialize typescript run tsc --init --target "ES2016"
  5. 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

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.

visual

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);
		}