Socket

Description

The SocketWrapper class is a generic TypeScript wrapper for Socket.IO that provides structured namespace management, middleware support, and client connection handling with typed metadata support.

export type SocketConnectionNok = { ok:false, message:string }
export type SocketConnectionOk<Metadata = any> = { ok:true, newMetadata?: Metadata }
export type SocketConnectionMiddleware<Metadata extends Record<string, any> = any> = (self:SocketWrapper, client:Socket, commId?:string, metadata?:Metadata) => Promise<SocketConnectionOk<Metadata> | SocketConnectionNok>
export type SocketWrapperConstructor<Metadata extends Record<string, any> = any> =
{
    socketNamespace:string,
    clientConnectionKey?:string,
    connectionMiddleware?:SocketConnectionMiddleware<Metadata>
    afterClientConnect?: (self:SocketWrapper, client:Socket, metadata?:Metadata) => Promise<void>
    onClientDisconnect?:(self:SocketWrapper, client:Socket, ...params:any[]) => Promise<void>
    listeners?:Record<string, (self:SocketWrapper, client:Socket, metadata:any, ...params:any[]) => Promise<void>>
}

Generics

The SocketWrapper class expects a non-required generic wich is the metadata object expect at the moment of connection (at the same time, the object expected to be returned by the socketOk() call inside the connectionMiddleware function)

Properties

socketNamespace:string

The namespace of the socket, as you can connect your sockets with the related io package for client, resulting in something like the io('http://127.0.0.1:3000/myNamespace')

connectionMiddleware?:(self:SocketWrapper, client:Socket, commId?:string, metadata?:Metadata) => Promise<SocketConnectionOk<Metadata> | SocketConnectionNok>

This method lets you define a custom middleware that can refuse or accept your client connections.

  • self : an instance of the SocketWrapper class you're definining. You can use its related helper methods.

  • client : the actual client connecting

  • commId : a custom communicative id passed by the query of the client when connecting

  • metadata : additional metadata passed by the query in the metadata field when connecting.

To correctly use this method, return a socketOk (with new metadata) or socketNok with an error message.

clientConnectionKey?:string,

When clients will connect, it will be updated a connectedClients instance inside the SocketWrapper.

This usually sets the received client.id as the key, but if you create a custom middleware returning a custom metadata, you can use the value of that metadata object key.

You can specifiy that key here in this prop.

afterClientConnect?: (self:SocketWrapper, client:Socket, metadata?:Metadata) => Promise<void>

A method triggering after a client connects.

Same params as the connectionMiddleware method

onClientDisconnect?:(self:SocketWrapper, client:Socket, ...params:any[]) => Promise<void>

A method triggering on client disconnection.

Same params as the connectionMiddleware method

listeners?:Record<string, (self:SocketWrapper, client:Socket, metadata:any, ...params:any[]) => Promise<void>>

An object with a key labelling a specific event, and the value an async callback function that manages that event.

Example

This is the code found in the npx create-expresso-macchiato template.

- Client Connection

Here is a typical example on how a client should connect to the devUser namespace on your expresso-macchiato server on http://127.0.0.1:3000

import { io } from 'socket.io-client';

io('http://127.0.0.1:3000/devUser', {
    transports: ['websocket'],
    timeout: 5000,
    reconnectionAttempts: 3,
    query: {
        commId: window.localStorage.getItem('token')
    }
});

- Socket Wrapper on Server

Here is the same namespace instanciated on the server, using a middleware returning a new metadata object.

When a client connects, the middleware tries to authenticate it through the jwe token.

If the user is not authenticated, it will be raised an error and the connection refused, otherwise it will be returned a newMetadata object SocketMiddlewareFinalMetadata .

The client will be saved in the connectedClients, with their ids as key so they can be easily accessed for db related operations. (thanks to the clientConnectionKey prop, referring to a key of the object returned by the middleware)

export const devUserSocket = new SocketWrapper<SocketMiddlewareFinalMetadata>({
    socketNamespace: "devUser",
    clientConnectionKey: "userId",
    connectionMiddleware:authMiddleware,
    listeners:
    {
        "sayStuff": async (self, _, metadata:SocketMiddlewareFinalMetadata, otherText:string) =>
        {
            self.broadcastExceptClient(metadata.userId, "sayingStuff", { message: `User ${metadata.userName} says: ${otherText}` });
            self.sendToClient(metadata.userId, "sayingStuff", { message: `You said: ${otherText} and everyone received it` });
        }
    }
})
export type SocketMiddlewareFinalMetadata = { userId:string, userName:string, userEmail:string }
export const authMiddleware:SocketConnectionMiddleware =  async (self, client, commId, metadata) =>
{
    if (!commId) return socketNok("Unauthorized");
    const { id } = await tokenInstance.authorize(commId)

    const user = await User.findOneBy({ id: Equal(id) });
    if (!user) return socketNok("User not found");

    return socketOk({ userId:user.id, userEmail:user.email, userName:user.name });
}

Last updated