@studiowebux/socket

5.0.0 • Public • Published

Introduction

This module uses Socket.IO & Socket.io-redis
Also, The cookie package is used for the authentication.

It offers 4 possibilities

  • Adds a function to check if the user is authenticated to access the resources.
  • Adds a function to configure the Redis adaptor, this feature allows to connect multiple instances and/or processes together to create a cluster.
  • Based on the configuration file, it links the actions automatically in each specified namespaces. The configuration file is flexible to meet most of cases.
  • Expose the Socket.IO directly and use it with the native implementation.

Take note that no explanation about socket.IO are given in this document, please consult the official documentation for that : Socket.IO

The examples/ directory has a frontend (made with VueJS) and some backend to do tests and to understand the module and its possibilities.

For more details (EN/FR) : Wiki

Installation

npm install --save @studiowebux/socket

NPM

Usage

Configuration

The configuration is separated in 3 parts,

authentication

Key Value Description Plus d’info
namespaces An Array that contains the namespaces for which the authentication is enabled NOTE, adding the namespace 'default' will apply the authentication for ALL namespaces Middleware
accessTokenKey A String that contains the cookie name that has the JWT in it. The JWT token must be stored in the cookies
isAuthenticated A String a path or the actual function require("path_to_function") path.join(__dirname, ".", "isAuth.js")
or
require(path.join(__dirname, ".", "isAuth.js"))
This function allows to validate is the user is connected, ** You must provide this function, by default there is no implementation **

redis

Key Value Description
host Redis Host By default: 127.0.0.1
port Redis Port By default : 6379
password Redis Password By default : no password

namespaces

Key Value Description
Namespace name (for example : "authentication", "default", "whatever") An array that contains the paths to the actions /the_actions_directory/user/{find.js, findOne.js, create.js, update.js, remove.js}

Using this path: ‘/absolute_path/user’ in the array, will automatically load the 5 actions using the same format: ‘findUser’ , ‘findOneUser’, ‘createUser.’, ‘updateUser’, ‘removeUser’

Using this path: ‘/absolute_path/user/find.js’ in the array, will automatically and only add ‘findUser’.
the namespace named ‘default’ is this one : '/', all other namespaces are named like that : ‘/namespace_name’
recursionAllowed This option allows to scan the children directories and to automatically add the actions found in those /user/{find.js, findOne.js, create.js, update.js, remove.js}
and
/user/profile/{find.js, findOne.js, create.js, update.js, remove.js}

using this directory : ‘/absolute_path/user’, will automatically add the actions within the ‘profile’ directory, then they will be named like that : ‘findUserProfile’
ignoreFirstDirectory It allows to add all actions using the top level path (/my_project/actions), but it will remove the parent directory name (actions) to keep something coherent. /actions/user/{find.js, findOne.js, create.js, update.js, remove.js}
and
/actions/user/profile/{find.js, findOne.js, create.js, update.js, remove.js}

, If this option is set to FALSE, it will return names like ‘findActionsUser’, but by enabling this option the name will be ‘findUser’ and 'findUserProfile’, ...

The available options:

const opts = {
  authentication: {
    namespaces: ["profile", "default"],
    accessTokenKey: "accessToken", // The cookie key name
    isAuthenticated: require(path.join(__dirname, ".", "isAuth.js")), // the function to check if the user if authenticated
  },
  redis: {
    host: process.env.REDIS_HOST || "127.0.0.1",
    port: process.env.REDIS_PORT || "6379",
    password: process.env.REDIS_PASSWORD || "",
  },
  recursionAllowed: false, // To allow the recursion within directory within the actions directories.
  ignoreFirstDirectory: false, // To keep the user, message, etc. in the event name
  namespaces: {
    default: [
      path.join(__dirname, "actions", "user"),
      path.join(__dirname, "actions", "message"),
      path.join(__dirname, "actions", "_ReservedEvents"),
    ],
    profile: [
      path.join(__dirname, "actions", "profile"),
      path.join(__dirname, "actions", "profile", "private", "superPrivate"), // With the recursionAllowed set to 'false' you can specify specific path within a path
      path.join(__dirname, "actions", "profile", "private"), // With the recursionAllowed set to 'false' you can specify specific path within a path
    ],
    general: [path.join(__dirname, "actions", "message", "find.js")], // to attach a specific function
  },
};

Functions

constructor(opts, app, log = console)

Initialize the socket (io), it requires an HTTP/HTTPS server or an Express instance

Documentation to use HTTP/HTTPS : https://socket.io/docs/#Using-with-Node-http-server
Documentation to use Express : https://socket.io/docs/#Using-with-Express

const app = // express or HTTP/HTTPS server //

const WebuxSocket = require("@studiowebux/socket");
const webuxSocket = new WebuxSocket(opts, app, console);

The app parameter must be set to a HTTP/HTTPS server or an Express Instance
The log parameter allows to use a custom logger, by default this is set to console.

AddRedis(): Void

  • It allows to add a redis connection to keep the client connections while using a cluster.
  • Redis is configured “automatically” based on the coniguration.
  • If no configuration is defined, the default behavior will be 127.0.0.1:6379 without password.

Run your Redis instance with Docker

docker run --rm --name redis -p 6379:6379 redis

To use this function :

webuxSocket.AddRedis();

You can easily configure a cluster to get load balancing and redondancy for your backends using this function.

AddAuthentication(): Callback(error, user)

  • It allows to add an authentication to check if the user is connected before establishing the connection with the socket.
  • The authentication can be configured per namespace, the configuration allows to secure specific namespaces easily or the whole application by specifying the 'default' namespace.

For more details, io.use

The authentication functon must be provided by you, you can link the function using the path or the actual function (with require())

If the user isn't connected, the backend will return an error and the connection will not be established.

The error is returned on gotErreur

To use the function:

webuxSocket.AddAuthentication();

Start(): Void

This function allows to

  1. Start the socket.IO instance
  2. Automatically configure the actions and namespaces based on the configuration file/variable
webuxSocket.Start();

If the automatic configuration doesn't meet your requirements, you can use the Standalone() function to access the native socket.IO implementation.

Initialize(server): Object

This function initializes the socket.io instance using a server and it returns a io instance.

const app = require("express")();
const server = require("http").Server(app);
// OR
const app = require("http").createServer(handler); // handler not defined here ...

const io = webuxSocket.Initialize(server);

Standalone(): Object

This function allows to

  1. Return the Socket.IO instance
  2. Still possible to use the Redis and Authentication function

This function let you use this module along with the native implementation of Socket.IO

To get more information about Socket.IO, please consult the official documentation Socket.IO Documentation

// Using default namespace
webuxSocket.Standalone().on("connection", (socket) => {
  console.debug(`webux-socket - Socket ${socket.id} connected.`);

  socket.on("disconnect", () => {
    console.debug(`webux-socket - Socket ${socket.id} disconnected.`);
  });

  socket.emit("userFound", [1, 2, 3, 4, 5]);
});

// Using namespace
webuxSocket
  .Standalone()
  .of("/profile")
  .on("connection", (socket) => {
    console.debug(`webux-socket - Socket ${socket.id} connected.`);

    socket.on("disconnect", () => {
      console.debug(`webux-socket - Socket ${socket.id} disconnected.`);
    });

    socket.emit("profileFound", [5, 4, 3, 2, 1, 0]);
  });

Quick Start

How to use the reserved events

How to do something like that socket.on('disconnect', (socket)=>{}) and others.

Here is the list of reserved keywords:

"error",
"connect",
"disconnect",
"disconnecting",
"newListener",
"removeListener",
"ping",
"pong";

Events usage

You must create a directory named '_ReservedEvents', then add the event files like that :

./actions/
  ./_ReservedEvents
    disconnect.js
    connect.js
    etc...
  ./user
    ...
  ./message
    ...

Then in the configuration file,

const opts = {
  recursionAllowed: true, // can be true or false for this example
  ignoreFirstDirectory: false, // must be set to false for this example
  namespaces: {
    default: [
      path.join(__dirname, "actions", "user"),
      path.join(__dirname, "actions", "message"),
      path.join(__dirname, "actions", "_ReservedEvents"), // It will load the events
    ],
  },
};

That way the default namespace will have a listener on the disconnect and connect events.
You can use the same pattern to create custom events per namespaces, or simply reuse the events in multiple namespaces.

That means that you can create multiple folders named \_ReservedEvents and link each of them per namespaces.

and/or

Use the same directory to all namespaces

Examples

_ReservedEvents/disconnect.js

const socket = (client, io) => {
  return () => {
    console.debug(`Socket ${client.id} disconnected.`);
  };
};

module.exports = { socket };

_ReservedEvents/connect.js

This function doesn't need the return (){ ... } like the disconnect event

const socket = (client, io) => {
  console.debug(`Socket ${client.id} connected.`);
};

module.exports = { socket };

The function to check the authentication

This function must be adapted to your project, here is an example with JWT,

  1. Create a file named isAuth.js
"use strict";

const jwt = require("jsonwebtoken");

function isAuth(accessToken) {
  return new Promise((resolve, reject) => {
    jwt.verify(accessToken, "HARDCODED_JWT_SECRET", (err, user) => {
      if (err || !user) {
        return reject(err || new Error("No user found"));
      }
      return resolve(user);
    });
  });
}

module.exports = isAuth;
  1. update the isAuthenticated key in the configuration,

    You can use the path to the function or use the require("path_to_the_function") directly,

const opts = {
  authentication: {
    namespaces: ["profile", "default"],
    accessTokenKey: "accessToken", // The cookie key name
    isAuthenticated: require(path.join(__dirname, ".", "isAuth.js")), // the function to check if the user if authenticated (OR -> isAuthenticated: path.join(__dirname, ".", "isAuth.js"))
  },
};

What to remember when creating this function:

  1. Must return a promise
  2. Must return an object with the user payload when Success
  3. Must return a new Error() in case of Failure

The Action File

This file must have a specific structure to be implemented with the Start() function

user/find.js

// helper
const timeout = (ms) => new Promise((res) => setTimeout(res, ms));

// action
// Application Logic
const find = (body) => {
  return new Promise(async (resolve, reject) => {
    console.log(body);
    console.log("Start retrieving entries");
    console.log("then wait 2 seconds");
    await timeout(2000);
    return resolve({ msg: "Success !", users: ["1", "2", "3", "4", "5"] });
  });
};

// route
// For REST API
const route = async (req, res, next) => {
  try {
    const obj = await find(req.body);
    if (!obj) {
      return next(new Error("No user found."));
    }
    return res.status(201).json(obj);
  } catch (e) {
    next(e);
  }
};

// socket
// socket.on("eventName", (body,fn){})
const socket = (socket, io) => {
  return async (body, fn) => {
    try {
      const obj = await find(body).catch((e) => {
        throw e;
      });
      if (!obj) {
        throw new Error("No user found");
      }
      fn(true); // Returns a callback

      socket.emit("userFound", obj); // to only the client
      //io.emit("userFound", obj); // to everyone
    } catch (e) {
      socket.emit("gotError", e.message);
    }
  };
};

module.exports = {
  find,
  route,
  socket,
};

The action file is separated in 3 sections

  • Controller / Action / Module / Logic / Whatever
    This section allows to do some stuffs with the database, an operation with the data or more. This is the application logic.
  • Route
    This section allows to use a REST API Call

    (This section doesn't apply to this module, this is used for the REST API, if you only use the Socket.IO implementation, you can safely remove it)

  • Socket
    This section allows to return the function use by socket.on

The socket and io parameters are automatically passed using the function Start()
The body and fn paramters are available to configure the function as needed.

Callbacks (Acknowledgements)

This is possible to return a callback, you have to use the last parameter like in the example above (the fn),
For more details : Acknowledgements

Emits

The information is available here : Emit Cheatsheet

Rooms

The information is available here : Rooms and namespaces

Videos and other resources

Contribution

Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

license

SEE LICENSE IN license.txt

Readme

Keywords

none

Package Sidebar

Install

npm i @studiowebux/socket

Weekly Downloads

93

Version

5.0.0

License

SEE LICENSE IN license.txt

Unpacked Size

35.1 kB

Total Files

10

Last publish

Collaborators

  • tgingras