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) orsocketNok
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