Lightweight WebSocket lib with socket.io-like event handling, requests, and channels

Overview

ws-wrapper

Lightweight and isomorphic Web Socket lib with socket.io-like event handling, Promise-based requests, and channels.

What?

Much like Socket.io, this library provides a protocol and API that sits on top of native WebSockets. Rather than passing raw messages through the WebSocket via WebSocket.send(), this library provides an RPC-like API that allows you to pass JSON data over WebSockets and trigger event handlers on the remote end. There is also a Promise-based request/response API, as well.

This library is isomorphic, so it can wrap WebSockets on the client (i.e. browser) or on a Node.js server using the ws library. You can get even fancier on the server side and utilize the ws-server-wrapper library (recommended).

Why?

Because lightweight is sometimes what you want. This library and its dependencies weigh under 3 KB when minified and gzipped!

This lib might be useful if you want some socket.io functionality (i.e. namespaces, event handling, etc.), but you don't want all of the engine.io transports. When using this library in conjunction with a library like ws, your real-time web application can be pretty darn lightweight without giving up some nice bare-bones functionality.

Install

npm install ws-wrapper

Usage

WebSocketWrapper is a CommonJS module, so it works in Node.js and in the browser if you use a bundler like Browserify, Webpack, Parcel.js, or module-concat.

Check out the example-app for a sample chat application (recommended).

Note: This module uses ES6 classes. If you need this to work in IE or another old, decrepit browser, try using a code transpiler like Babel.

Note: This module uses JSON.stringify to serialize data over the raw WebSocket connection. This means that serializing circular references is not supported out of the box.

Client-side

// Use a bundler to make the next line of code "work" on the browser
const WebSocketWrapper = require("ws-wrapper");
// Create a new socket
var socket = new WebSocketWrapper(new WebSocket("ws://" + location.hostname) );
// Now use the WebSocketWrapper API... `socket.emit` for example
// See examples below...

Server-side (Node.js)

Use ws-server-wrapper to wrap the WebSocketServer (recommended). See ws-server-wrapper README for more details.

If you don't want to use ws-server-wrapper, you can wrap the WebSocket once a new WebSocket connects like this:

const WebSocketServer = require("ws").Server
	, WebSocketWrapper = require("ws-wrapper");
var wss = new WebSocketServer({port: 3000});
wss.on("connection", (socket) => {
	socket = new WebSocketWrapper(socket);
	// ...
});

Other servers (i.e. Go)

No such libraries exist yet. :( Please create one, and let me know about it! I'll give you beer!

Event Handling

It's what you'd expect of an event handler API.

Call on or once to bind an event handler to the wrapper or to a channel. Call emit to send an event.

Server-side Example (without using ws-server-wrapper):

const WebSocketServer = require("ws").Server
	, WebSocketWrapper = require("ws-wrapper");
var wss = new WebSocketServer({port: 3000});
var sockets = new Set();
wss.on("connection", (socket) => {
	var socket = new WebSocketWrapper(socket);
	sockets.add(socket);
	socket.on("msg", function(from, msg) {
		// `this` refers to the WebSocketWrapper instance
		console.log(`Received message from ${from}: ${msg}`);
		// Relay message to all clients
		sockets.forEach((socket) => {
			socket.emit("msg", from, msg);
		});
	});
	socket.on("disconnect", () => {
		sockets.delete(socket);
	});
});

Client-side Example:

// Use a bundler to make the next line of code "work" on the browser
const WebSocketWrapper = require("ws-wrapper");
// Establish connection
var socket = new WebSocketWrapper(
	new WebSocket("ws://" + location.host)
);
// Add "msg" event handler
socket.on("msg", function(from, msg) {
	console.log(`Received message from ${from}: ${msg}`);
});
// Emit "msg" event
socket.emit("msg", "my_name", "This is a test message");

Channels

Just like in socket.io, you can "namespace" your events using channels. When sending messages to multiple channels, the same WebSocket connection is reused, but the events are logically separated into their appropriate channels.

By default, calling emit directly on a WebSocketWrapper instance will send the message over the "default" channel. To send a message over a channel named "foo", just call socket.of("foo").emit("eventName", "yourData").

Request / Response

Event handlers can return values or Promises to respond to requests. The response is sent back to the remote end.

The example below shows the client requesting data from the server, but ws-wrapper also allows servers to request data from the client.

Server-side Example (without using ws-server-wrapper):

const fs = require("fs")
	, WebSocketServer = require("ws").Server
	, WebSocketWrapper = require("ws-wrapper");
var wss = new WebSocketServer({port: 3000});
var sockets = new Set();
wss.on("connection", (socket) => {
	socket = new WebSocketWrapper(socket);
	sockets.add(socket);
	socket.on("userCount", () => {
		// Return value is sent back to the client
		return sockets.size;
	});
	socket.on("readFile", (path) => {
		// We can return a Promise that eventually resolves
		return new Promise((resolve, reject) => {
			// `path` should obviously be sanitized, but just go with it...
			fs.readFile(path, (err, data) => {
				// `err` or `data` are now sent back to the client
				if(err)
					reject(err);
				else
					resolve(data.toString("utf8") );
			});
		});
	});
	socket.on("disconnect", () => {
		sockets.delete(socket);
	});
});

Client-side Example:

// Assuming WebSocketWrapper is somehow available to this scope...
var socket = new WebSocketWrapper(
	new WebSocket("ws://" + location.host)
);
var p = socket.request("userCount");
// `p` is a Promise that will resolve when the server responds...
p.then((count) => {
	console.log("User count: " + count);
}).catch((err) => {
	console.error("An error occurred while getting the user count:", err);
});
socket.request("readFile", "/etc/issue").then((data) => {
	console.log("File contents:", data);
}).catch((err) => {
	console.error("Error reading file:", err);
});

API

Class: WebSocketWrapper

A WebSocketWrapper simply wraps around a WebSocket to give you well-deserved functionality. :)

socket = new WebSocketWrapper(webSocketInstance[, options]);

Constructs a new WebSocketWrapper, and binds it to the native WebSocket instance.

  • webSocketInstance - the native WebSocket instance
  • options
    • debug - set to true to print debugging messages to console.log
    • errorToJSON - function to serialize Errors over the WebSocket. In Node.js, the default is to send only the message property of the Error (for security reasons). Errors that occur on the browser include all properties.
    • requestTimeout - maximum delay in ms. that the WebSocketWrapper will wait until rejecting the Promise of a pending request. Defaults to null, which means that there will be no timeout. This option is recommended for servers because clients who do not fulfill pending requests can cause memory leaks.

Events

  • Event: "open" / "connect"
    • event - The (worthless) event from the native WebSocket instance
  • Event: "error"
    • event - The Error event from the native WebSocket instance
  • Event: "message"
    • event - The Message event from the native WebSocket instance
    • data - The message data (same as event.data)
  • Event: "close" / "disconnect"
    • event - The Close event from the native WebSocket instance
    • wasOpen - true if the "open" event was fired on the native WebSocket instance before the "close" event was fired.

Note: The "special" events listed above are not sent over the WebSocket.

The EventEmitter-like API looks like this:

  • socket.on(eventName, listener) Adds the listener function to the end of the listeners array for the event named eventName. When an event or request matching the eventName is received by the WebSocket, the listener is called.

    Values returned by the listener callback are used to respond to requests (see socket.request). If the return value of the listener is a Promise, the response to the request will be sent once the Promise is resolved or rejected; otherwise, the return value of the listener is sent back to the remote end immediately.

    If the inbound message is a simple event (see socket.emit), the return value of the listener is ignored. It is also "safe" for the listener to return a Promise even if the inbound message is a "simple" event. If the returned Promise is rejected, an unhandled rejection will not occur; rather, the result of the Promise is just ignored.

    If the listener throws an Error, this Error will propagate up the stack as expected, and if the inbound message was a request, the Error is sent back to the remote end as a response rejection.

  • socket.once(eventName, listener) Adds a one time listener function for the event named eventName.

  • socket.removeListener(eventName, listener) Removes the specified listener from the listener array for the event named eventName.

  • socket.removeAllListeners([eventName]) Removes all listeners, or those of the specified eventName.

  • socket.eventNames() Returns an array listing the events for which the emitter has registered listeners.

  • socket.listeners(eventName) Returns a copy of the array of listeners for the event named eventName.

  • socket.emit(eventName[, ...args]) Sends an event down the WebSocket with the specified eventName calling all listeners for eventName on the remote end, in the order they were registered, passing the supplied arguments to each.

  • socket.request(eventName[, ...args]) Sends a request down the WebSocket with the specified eventName and returns a Promise that will resolve once the remote event listener responds.

    Note: While it is common design for only one event listener to exist on the remote end, all listeners for eventName on the remote end are called, in the order they were registered, passing the supplied arguments to each. Since Promises can only be resolved or rejected once, only the data from the first event listener is used to generate the response for this request.

    Note: If a request is sent, but there is no remote event listener to respond to the request, a response rejection is immediately sent back by the remote end.

  • socket.timeout(tempTimeoutInMs) Temporarily sets the requestTimeout to tempTimeoutInMs for the next request only. This returns socket to allow chaining. Typical usage:

     // The next request will be rejected if there is no response for 5 secs.
     let promise = socket.timeout(5 * 1000).request("readFile", "/etc/issue");

The above EventEmitter functions like on and once are chainable (as appropriate).

Channel API:

  • socket.of(channelName) Returns the channel with the specified channelName. Every channel has the same EventEmitter-like API as described above for sending and handling channel-specific events and requests. A channel also has a read-only name property.

Other methods and properties:

By default, the WebSocketWrapper provides a queue for data to be sent. Once the WebSocket is open, this queue is flushed until the connection is lost. The following methods allow one to re-bind a new WebSocket or clear the send queue.

  • socket.abort() Clears the send queue for this WebSocketWrapper and rejects all Promises for pending requests.
  • socket.bind(nativeWebSocket) Binds this WebSocketWrapper to a new WebSocket. This can be useful when socket reconnection logic needs to be implemented. Instead of creating a new WebSocketWrapper each time a WebSocket is disconnected, one can simply bind a new WebSocket to the WebSocketWrapper. In this way, data queued to be sent while the connection was dead will be sent over the new WebSocket passed to the bind function.
  • socket.isConnecting - checks the native WebSocket readyState and is true if and only if the state is CONNECTING.
  • socket.isConnected - checks the native WebSocket readyState is true if and only if the state is CONNECTED.
  • socket.send(data) If connected, calls the native WebSocket's send method; otherwise, the data is added to the WebSocketWrapper's send queue.
  • socket.disconnect() Closes the native WebSocket
  • socket.set(key, value) Saves user data specific to this WebSocketWrapper
  • socket.get(key) Retrieves user data. See socket.set(key, value) above.

WebSocketWrapper.MAX_SEND_QUEUE_SIZE The maximum number of items allowed in the send queue. If a user tries to send more messages than this number while a WebSocket is not connected, errors will be thrown. Defaults to 10; changes affect all WebSocketWrapper instances.

Protocol

All data passed over the native WebSocket should be valid JSON, but this is not a hard requirement. ws-wrapper will try to parse a JSON string and determine the message type based on the properties in the parsed Object.

The following message types are defined by ws-wrapper:

  1. Event Dispatch - Identified by an Object with a key but no i key. The channel name is optional.

    {
    	"c": "channel_name",
    	"a": ["event_name", "first_arg", "second_arg", "last_arg"]
    }

    The client or server can send events. Events are nothing more than an event name and some data, passed as arguments to the event handler.

  2. Request - Identified by an Object with a and i keys where i refers to the unique request identifier. The channel name is optional.

    {
    	"i": 123,
    	"c": "channel_name",
    	"a": ["event_name", "first_arg", "second_arg", "last_arg"]
    }

    The client or server can send a Request, which is essentially an Event that needs some sort of server Response.

  3. Response (Resolution) - Identified by an Object with i and d keys where i is the request identifier and d is the response data.

    {
    	"i": 123,
    	"d": {"resolved": "data", "hello": "world"}
    }
  4. Response (Rejection) - Identified by an Object with i and e keys where i is the request identifier and e is the error Object to be used when rejecting the response Promise. If _ is set, the e Object is converted into an Error instance upon receipt.

    {
    	"i": 123,
    	"e": {"message": "error message"},
    	"_": 1
    }

If the message received by the WebSocket is not valid JSON or if the parsed Object does not match one of the above message types, then the message is simply ignored by ws-wrapper. Also if the JSON message contains a ws-wrapper property with the value false, the message will be ignored. This allows other libraries to use the same WebSocket and send messages that will not be processed by ws-wrapper.

Auto-Reconnect

ws-wrapper does not implement auto-reconnect functionality out of the box. For those who want it (almost everyone), I have written some sample code to show how easy it is to add.

How to implement auto-reconnect for ws-wrapper

If someone wants to make an npm package for the auto-reconnect feature, I'd be happy to list it here, but it will probably never be a core ws-wrapper feature.

Comments
  • Option to disable pendingSend globally

    Option to disable pendingSend globally

    It would be good having an option to disable _pendingSend

    I have a game that uses this library and sometimes occurs of some connections fail and throw the error "WebSocket is not connected and send queue is full" and cause restart at my server, currently for me to deal with this I have to edit the module manually on server and comment on the function "send(data, ignoreMaxQueueSize)"

    Pull Request: #14

    opened by FifineHex 14
  • Server fails with error after client emitted event

    Server fails with error after client emitted event

    I faced with strange issue using ws-wrapper. After client emits something server just fails with this error:

    [1] /.../node_modules/ws-wrapper/lib/wrapper.js:247
    [1]             this.send(JSON.stringify(data) );
    [1]                            ^
    [1]
    [1] TypeError: Converting circular structure to JSON
    [1]     at JSON.stringify (<anonymous>)
    [1]     at WebSocketWrapper._sendEvent (/.../node_modules/ws-wrapper/lib/wrapper.js:247:18)
    [1]     at WebSocketWrapper.emit (/.../node_modules/ws-wrapper/lib/channel.js:85:25)
    [1]     at WebSocket.socket.onmessage (/.../node_modules/ws-wrapper/lib/wrapper.js:75:9)
    [1]     at WebSocket.onMessage (/.../node_modules/ws/lib/event-target.js:120:16)
    [1]     at emitOne (events.js:115:13)
    [1]     at WebSocket.emit (events.js:210:7)
    [1]     at Receiver.receiverOnMessage (/.../node_modules/ws/lib/websocket.js:718:20)
    [1]     at emitOne (events.js:115:13)
    [1]     at Receiver.emit (events.js:210:7)
    

    Client (in browser)

      import WebSocketWrapper from 'ws-wrapper'
      const socket = new WebSocketWrapper(new WebSocket('ws://localhost:3000'))
      socket.emit('echo-me', '123')
    

    Server

    const WebSocketServer = require("ws").Server
    const WebSocketWrapper = require("ws-wrapper")
    const wss = new WebSocketServer({port: 3000})
    
    wss.on("connection", (rawSocket) => {
      const socket = new WebSocketWrapper(rawSocket)
    
      socket.on('echo-me', (data) => {
        console.log('Echo', data)
      })
    });
    

    As you can see server just emits nothing.

    I'm trying to isolate issue to give you a cleaner reproduction. For some reason I can't reproduce it with node clients.

    opened by OEvgeny 10
  • onMessage hook?

    onMessage hook?

    First, very nice wrapper! :)

    I'm not sure but it seems to me there isn't a hook to catch all received messages. This would be very useful server side, as it would make rate limiting a lot easier.

    Do you have a suggestion on how to best implement this, so that I don't have to check on every socket.on?

    The reason I ask here and not in ws-server-wrapper, is because ws-server-wrapper extends ws-wrapper (and therefore also the onMessage event).

    enhancement question 
    opened by daniandl 6
  • "a" leading sendEvent data

    Hi, the wrapper is amazing but I have a question about the _sendEvent function (line 249). I'm trying to implement the same wrapper for this flutter websocket plugin and looking at the raw data sent by your wrapper I noticed the key "a" on the object that will be sent to the other end of the socket. Why is that? I tried removing it and stringify the args object directly also works. Moreover removing it will make it easier for my to parse it on flutter.

    Thank you!

    https://github.com/bminer/ws-wrapper/blob/0e544c614efbaf71128c82b9e945ecba272936b3/lib/wrapper.js#L247-L252

    question 
    opened by cTatu 3
  • Protocol mismatch: packets are sent as {

    Protocol mismatch: packets are sent as { "a": { "0": "connect" }} instead of { "a": ["connect"] }

    WebSocketChannel emit and request pass the arguments keyword to _wrapper._sendEvent, but the Array-like arguments object gets JSON.stringify-d into an object instead of an array.

    For example, socket.emit('event-name', 'foo', 'bar') sends:

    { "a": { "0": "event-name", "1": "foo", "2": "bar" } }
    

    while the protocol states it should be:

    { "a": ["event-name", "foo", "bar"] }
    

    The arguments object could be turned into an array to match the protocol, e.g. using Array.from(arguments) or [...arguments] or Array.prototype.slice.call(arguments).

    I only noticed this issue because I decoded the packets in C# instead of JavaScript.

    Thanks for the library, I've used it in several projects already.

    opened by martikaljuve 2
  • Compile to ES5

    Compile to ES5

    React's automatic build script does not work because this project does not provide ES5 code.

    See https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/template/README.md#npm-run-build-fails-to-minify

    Also, providing ES5 support will make this work on IE 10/11.

    opened by kerrickwoyshner 1
  • node app as client

    node app as client

    hi,

    great work there, and it works fine on server as "server". but my case is that my client is not a browser but another node app, so if i try this example here it works https://github.com/websockets/ws#sending-and-receiving-text-data but as soon as i wrap that with ws-wrapper i keep getting a message trying to connect to server.

    thanks in advanced.

    opened by devmondo 1
  • Hooks

    Hooks

    Hey there Love this wrapper, been using it on a few personal projects. I've had to bend it into working with my specific requirements, and I'm about to bend it some more. I want to implement hooks like beforeReceive beforeSend etc (for things like rate limiting, middleware to check auth, etc).

    Is this something you might be interested in having in this wrapper or is that out of scope in your opinion? Feel free to close the issue if it's not for you :)

    Edit: wow my mind is escaping me, looks like I asked this same question almost exactly a year ago... #10 well the question still stands i guess haha

    enhancement question 
    opened by daniandl 9
Owner
Blake Miner
Blake Miner
Simple realtime chat application made by NodeJS, Express, Socket.io and Vanilla Javascript. This project is made to discover socket.io and understand its basic features.

LearnByChat App Simple realtime chat application made with NodeJS, Express, Socket.io and Vanilla Javascript. This project is made to discover socket.

Ayoub Saouidi 1 Dec 19, 2021
A Develop Tool to Test WebSocket, Socket.IO, Stomp, Bayeux, HTTP, TCP, UDP, WebRTC, DNS API.

A Develop Tool to Test WebSocket, Socket.IO, Stomp, Bayeux, HTTP, TCP, UDP, WebRTC, DNS API.

York Yao 24 Sep 6, 2022
Socket.io-Express - A simple express project to learn how to use socket.io.

Socket.io Express A simple express project to learn how to use socket.io ✨ ?? Simple project This is a small project that has been designed to be usab

Zerio 6 Sep 25, 2022
Lightweight WebSocketServer wrapper lib using ws-wrapper to wrap connected WebSockets

ws-server-wrapper Lightweight WebSocketServer wrapper lib using ws-wrapper and ws to wrap connected WebSockets. The only dependency is ws-wrapper itse

Blake Miner 17 May 9, 2022
Simple to use, blazing fast and thoroughly tested WebSocket client and server for Node.js

ws: a Node.js WebSocket library ws is a simple to use, blazing fast, and thoroughly tested WebSocket client and server implementation. Passes the quit

WebSockets 19.2k Jan 4, 2023
A node.js module for websocket server and client

Nodejs Websocket A nodejs module for websocket server and client How to use it Install with npm install nodejs-websocket or put all files in a folder

Guilherme Souza 719 Dec 13, 2022
Standards-compliant WebSocket client and server

faye-websocket This is a general-purpose WebSocket implementation extracted from the Faye project. It provides classes for easily building WebSocket s

null 588 Dec 23, 2022
Create a Real-time Chat App using React and Socket.io

React And Socket.io ChatApp MIT Licence MIT License Copyright (c) 2021 Ali Ahmad Permission is hereby granted, free of charge, to any person obtaining

Ali Ahmad 2 Jan 10, 2022
An example about web socket with .net 3.1 and react

Websocket Example This is a real time app example which is using web socket, dot net core and react. Project Case You are a factory manager and you ha

Samet Yazıcı 5 Jan 11, 2022
Full-Stack Instgram Clone using MERN Stack and Socket.io

Instagram MERN Full-Stack Instgram Clone using MERN Stack and Socket.io Visit Now ?? ??️ Tech Stack Frontend: Backend: Realtime Communication: Cloud S

Jigar Sable 326 Dec 27, 2022
PondSocket is a fast, minimalist and bidirectional socket framework for NodeJS.

PondSocket PondSocket is a fast, minimalist and bidirectional socket framework for NodeJS. Pond allows you to think of each action during a sockets li

Roy Ossai 3 Nov 1, 2022
A WebSocket Implementation for Node.JS (Draft -08 through the final RFC 6455)

WebSocket Client & Server Implementation for Node Overview This is a (mostly) pure JavaScript implementation of the WebSocket protocol versions 8 and

Brian McKelvey 3.6k Dec 30, 2022
WebSocket emulation - Node.js server

SockJS-node SockJS for enterprise Available as part of the Tidelift Subscription. The maintainers of SockJS and thousands of other packages are workin

SockJS 2.1k Dec 29, 2022
The cutest little WebSocket wrapper! 🧦

Sockette The cutest little WebSocket wrapper! ?? Sockette is a tiny (367 bytes) wrapper around WebSocket that will automatically reconnect if the conn

Luke Edwards 2.4k Jan 2, 2023
🔄 iola: Socket client with REST API

?? iola: Socket client with REST API

Pavel Varentsov 113 Jan 3, 2023
WebSocket cat

WebSocket cat

WebSockets 1.6k Jan 2, 2023
How to build a chat using Lambda + WebSocket + API Gateway? (nodejs)

Description Source code for the lambda function from the screencast How to build a chat using Lambda + WebSocket + API Gateway? (nodejs) The reactjs c

Alex 21 Dec 28, 2022
A tiny Nuxt.js module for WebSocket interactions

@deepsource/nuxt-websocket A tiny Nuxt.js module for WebSocket interactions. This module is only compatible with Nuxt v2 at the moment. Setup Add @dee

DeepSource 23 Dec 6, 2022
A websocket-based reverse shell for XSS attacks.

CrossSiteShell A javascript/nodejs "reverse shell" that makes it easier to interact with the victim's browser during XSS attacks. Usage Run the follow

Rafael 13 Oct 7, 2022