slavery-js
Version:
A simple clustering app that allows you to scale an application on multiple thread, containers or machines
301 lines (272 loc) • 11.2 kB
text/typescript
import { io } from "socket.io-client";
import { Socket } from 'socket.io';
import log from '../utils/log.js';
import type Listener from './types/Listener.js';
class Connection {
/*
* this class is manager for the socket instance
* it takes either a socket or a host and port to create the socket
* if it takes the host and port it will consider that connection is a server
* if it takes a socket it will consider that connection is a client
*
* it manages conenction, the listeners and the available emitters */
private socket: Socket | any;
private request_id: number = 0;
// this node information
public name?: string;
public id?: string;
public listeners: Listener[] = [];
public type: 'client' | 'server';
public host?: string;
public port?: number;
// is connected or not
public isConnected: boolean;
// ot target of the socket
public socketId: string;
public targetType: 'client' | 'server';
public targetName?: string;
public targetId?: string;
public targetListeners: Listener[] = [];
public targetHost?: string;
public targetPort?: number;
// callbacks
private onConnectCallback: Function;
public onDisconnectCallback: Function;
public onSetListenersCallback: Function;
/*
* @param Node: Node
* @param socket: Socket
* @param host: string
* @param port: number
* @param id: string
* @param name: string
* */
constructor({ socket, host, port, id, name, listeners, timeout,
onConnect, onDisconnect, onSetListeners } : {
id?: string, socket?: Socket, host?: string,
port?: number, name?: string, listeners?: Listener[],
timeout?: number, onConnect?: Function, onDisconnect?: Function,
onSetListeners?: Function
}) {
// callbacks
this.onConnectCallback = onConnect || (() => {});
this.onDisconnectCallback = onDisconnect || (() => {});
this.onSetListenersCallback = onSetListeners || (() => {});
// set listeners
if(listeners) this.listeners = listeners;
// if get a socket to connect to the server
if (socket && name) {
this.type = 'server';
this.name = name;
this.targetType = 'client';
// the socket
this.socket = socket;
// get the id of client
this.targetId = socket.handshake.auth.id;
//log('[Connection][server] targetId: ', this.targetId)
// since we are already getting the socket
this.isConnected = true;
// if get a host and port to connect to the server
} else if (host && port && id) {
this.type = 'client';
this.targetType = 'server';
// use the id
this.id = id;
this.socket = io(`ws://${host}:${port}`, {
auth: { id },
timeout: timeout || 1000 * 60 // 1 minute
});
// since we are not connected yet
this.isConnected = false;
} else
throw new Error('Connection must have either a socket and a name or a host and port');
// set the socket id
this.socketId = this.socket.id;
// initialize the listeners
this.initilaizeListeners()
}
private initilaizeListeners(): void {
/* this function inizializes the default listeners for the socket */
// set the object listeners
this.listeners.forEach( l => {
this.socket.removeAllListeners(l.event);
this.socket.on(l.event, this.respond(l.event, async (parameters:any) => {
//log(`[${this.id}] [Connection][initilaizeListeners] got event: ${l.event} from ${this.targetName}: `, parameters)
return await l.callback(parameters);
}));
});
// if target is asking for connections
this.socket.on("_listeners", this.respond("_listeners", () => this.getListeners()));
// if the target is sending a their listeners
this.socket.on("_set_listeners", this.respond("_set_listeners", (listeners: string[]) => {
//log(`[${this.id}] [Connection][Node] got liteners from ${this.targetName}: `, listeners)
this.targetListeners =
listeners.map(event => ({ event, callback: () => {} }));
this.onSetListenersCallback(this.targetListeners);
return 'ok';
}));
// if target is asking for name
this.socket.on("_name", this.respond("_name", () => this.name));
// if target is asking for id
this.socket.on("_id", () => this.id);
// on connected
this.socket.on("connect", async () => {
//log(`[connection][${this.socket.id}] is connected, querying target name and listeners:`)
// ask for listeners
this.targetName = await this.queryTargetName();
this.targetListeners = await this.queryTargetListeners();
//log(`[connection][${this.socket.id}] target name: ${this.targetName}, target listeners: ${this.targetListeners}`)
this.isConnected = true;
this.onConnectCallback(this);
});
// if it disconnects
this.socket.on("reconnect", async (attempt: number) => {
//log(`[connection][${this.socket.id}] is reconnected, attempt: ${attempt}`)
this.targetName = await this.queryTargetName();
this.targetListeners = await this.queryTargetListeners();
this.isConnected = true;
this.onConnectCallback(this);
});
// if it disconnects
this.socket.on("diconnect", () => {
//log(`[connection][${this.socket.id}] is disconnected`)
this.isConnected = false;
this.onDisconnectCallback(this);
});
}
public async connected(): Promise<boolean> {
return new Promise((resolve, reject) => {
let interval: NodeJS.Timeout;
let timeout: NodeJS.Timeout
// set interval to check for connection
interval = setInterval(() => {
if(this.isConnected) {
clearInterval(interval);
clearTimeout(timeout);
resolve(true);
}
}, 100); // 100 ms
// set timeout to reject if no connection
timeout = setTimeout(() => {
clearInterval(interval);
reject(false);
}, 1000 * 60 ); // 1 minute
})
}
public getType(){
return this.type;
}
public on(event: string, callback: Function): void {
this.socket.on(event, callback);
}
public emit(event: string, data: any): void {
this.socket.emit(event, data);
}
public getName(): string | undefined {
return this.name;
}
// this is the id of the conenction?
public getId(): string | undefined {
return this.id;
}
public getTargetName(): string | undefined {
return this.targetName;
}
// this is the id of the target client
public getTargetId(): string | undefined {
return this.targetId;
}
public async setListeners(listeners: Listener[]): Promise<void> {
// set the listeners on the socket
listeners.forEach( l => {
this.listeners.push(l);
// remove the listener if it exists
this.socket.removeAllListeners(l.event);
// add new listener
this.socket.on(l.event, this.respond(l.event, async (parameters:any) => {
// run the callback defined in the listner
return await l.callback(parameters);
}));
});
// update the listeners on the target
if(this.type === 'server'){
//let response =
await this.query('_set_listeners', listeners.map(listener => listener.event));
//if(response === 'ok') log('[Connection]<setListeners> listeners set successfully')
}
}
public addListeners(listeners: Listener[]): void {
// make sure we are not adding the same listener
const eventMap = new Map(this.listeners.map(l => [l.event, l]));
// add the listeners
listeners.forEach( l => eventMap.set(l.event, l) );
// set the listeners in the connection
this.listeners = Array.from(eventMap.values());
// set the listeners on the socket
this.setListeners(this.listeners);
}
public getTargetListeners(): Listener[] {
return this.targetListeners;
}
public onSetListeners(callback: Function): void {
this.onSetListenersCallback = callback;
}
public onConnect(callback: Function): void {
this.onConnectCallback = callback;
}
public onDisconnect(callback: Function): void {
this.onDisconnectCallback = callback;
}
private queryTargetListeners(): Promise<Listener[]> {
// query the target listeners
return this.query('_listeners');
}
private queryTargetName(): Promise<string> {
// query the target name
return this.query('_name');
}
// this function need to be awaited
public query(event: string, data?: any): Promise<any> {
/* this function makes the query to the socket and waits for the response */
return new Promise((resolve, reject) => {
// set a time out
let timeout = setTimeout( () => { reject('timeout') }, 1000 * 60); // 1 minute
// get request id
let request_id = ++this.request_id;
if (this.request_id >= Number.MAX_SAFE_INTEGER - 1) this.request_id = 0;
// send the query
this.socket.emit(event, {data, request_id: request_id});
this.socket.on(event + `_${request_id}_response`, (response: any) => {
//log('[Connection][Query] got response from ', this.targetType, 'event: ', event, 'response: ', response)
// clear the timeout
clearTimeout(timeout);
// clear the listener
this.socket.removeAllListeners(event + `_${request_id}_response`);
// resolve the response
resolve(response);
});
});
}
public send = this.query;
private respond(event: string, callback: Function) {
/* this is a wrapper function to respond to a query */
return async (parameters: any) => {
let data = parameters.data;
let request_id = parameters.request_id;
let response = await callback(data);
this.socket.emit(event + `_${request_id}_response`, response);
}
}
public getListeners(): any[] {
if(this.type === 'server')
return this.listeners;
else if(this.type === 'client')
return this.socket._callbacks;
else
throw new Error('Connection type not recognized');
}
public close(): void {
this.socket.disconnect();
}
}
export default Connection;