just-another-http-api
Version:
A framework built on top of fastify aimed at removing the need for any network or server configuration.
124 lines (104 loc) • 4.44 kB
JavaScript
const crypto = require ( 'crypto' );
const { redis } = require ( 'eip-cloud-services' );
class WebsocketGroup {
constructor ( groupName, destroy, messageReceivedHandler, connectionClosedHandler ) {
this.timeOfCreation = Date.now ();
this.groupName = groupName;
this.destroy = destroy;
this.groupUuid = crypto.randomUUID ().substring ( 0, 8 );
this.connections = new Map ();
this.messageReceivedHandler = messageReceivedHandler || ( async () => {} );
this.connectionClosedHandler = connectionClosedHandler;
this.pingConnections = setInterval ( this.#pingConnections.bind ( this ), 30000 );
}
async initialize () {
try {
await redis.subscribe ( `${this.groupName}_individualMessage`, this.#handleIndividualMessage.bind ( this ) );
await redis.subscribe ( `${this.groupName}_broadcast`, this.#broadcastMessageToClients.bind ( this ) );
await redis.subscribe ( `${this.groupName}_messageReceived`, this.#handleUserMessage.bind ( this ) );
}
catch ( error ){
console.error ( error );
}
}
async #handleUserMessage ( jsonMessage ) {
const userMessage = JSON.parse ( jsonMessage );
await this.messageReceivedHandler ( userMessage );
}
#broadcastMessageToClients ( message ) {
this.connections.forEach ( conn => {
conn.send ( typeof message === 'string' ? message : JSON.stringify ( message ) );
} );
}
#handleIndividualMessage ( individualMessage ) {
const { connectionId, message } = JSON.parse ( individualMessage );
if(this.connections.has(connectionId)) {
this.connections.get(connectionId).send(message);
}
}
#pingConnections () {
this.connections.forEach ( ( connection, connectionId ) => {
if(!connection.isAlive) {
this.connections.delete ( connectionId );
this.#clean(connectionId);
}
else{
connection.isAlive = false;
connection.ping ();
}
} );
}
getConnections () {
return this.connections;
}
individualMessage ( connectionId, message ) {
redis.publish ( `${this.groupName}_individualMessage`, JSON.stringify ( { connectionId, message } ) );
}
broadcastMessage ( message ) {
redis.publish ( `${this.groupName}_broadcast`, JSON.stringify ( message ) );
}
addNewConnection ( connection, connectionId = crypto.randomUUID () ) {
connection.isAlive = true;
this.connections.set ( connectionId, connection );
connection.on ( 'message', async message => {
const userMessage = {
groupName: this.groupName,
connectionId,
message: message.toString ()
};
await redis.publish ( `${this.groupName}_messageReceived`, JSON.stringify ( userMessage ) );
} );
connection.on ( 'close', () => {
this.connections.delete ( connectionId );
this.#clean(connectionId);
} );
connection.on ( 'pong', () => {
connection.isAlive = true;
} );
connection.on ( 'error', error => {
console.error ( 'WebSocket error:', error );
this.connections.delete ( connectionId );
this.#clean(connectionId);
} );
return { connectionId, groupName: this.groupName, connection };
}
async #clean (connectionId) {
if ( this.connectionClosedHandler !== null && typeof this.connectionClosedHandler === 'function') {
try{
await this.connectionClosedHandler ( {groupName:this.groupName, connectionId} );
}
catch (error) {
console.error('Error in connectionClosedHandler:', error);
}
}
if ( this.connections.size === 0 ) {
clearInterval ( this.pingConnections );
this.connections.clear ();
await redis.unsubscribe ( `${this.groupName}_individualMessage` );
await redis.unsubscribe ( `${this.groupName}_broadcast` );
await redis.unsubscribe ( `${this.groupName}_messageReceived` );
this.destroy(this.groupName);
}
}
}
module.exports = WebsocketGroup;