slavery-js
Version:
A simple clustering app that allows you to scale an application on multiple thread, containers or machines
396 lines (353 loc) • 17.6 kB
text/typescript
import Network, { Listener, Connection } from '../network/index.js';
import { ServiceClient } from '../service/index.js';
import type { ServiceAddress } from './types/index.js';
import { await_interval, execAsyncCode, log } from '../utils/index.js';
import { serializeError, deserializeError } from 'serialize-error';
/*
* this class will basicaly connect to all of the services given to it by the primary service.
* 1.- attempt to make a conenction the primary service passed by the server
* 2.- it will take a list of services
/* this calss will make a slave type which will be an a child of the Node class
* this is the class that will be used to run the the client of a node
* like other classes it will work as both
* the server connection to the client and the client conenction to the server.
* this class will have a list of methods that will be converted to listeners
* and a list of listeners that will be converted to methods
*/
type NodeStatus = 'idle' | 'working' | 'error';
class Node {
public mode: 'client' | 'server' | undefined = undefined;
public id: string | undefined = undefined;
public status: NodeStatus = 'idle';
public listeners: Listener[] = [];
public lastUpdateAt: number = Date.now();
public network: Network | undefined = undefined;
public servicesConnected: boolean = false;
// fields when the class is client handler on a service
public statusChangeCallback: ((status: NodeStatus, node: Node) => void) | null = null;
// stash changes functions
public stashSetFunction: (({ key, value }: { key: string, value: any }) => any) | null = null;
public stashGetFunction: ((key: string) => any) | null = null;
// fields when the class is a service handler on a node
public services: ServiceAddress[] = [];
public doneMethods: { [key: string]: boolean } = {};
public methods: { [key: string]: (parameter?: any, self?: Node) => any } = {};
constructor(){}
/* this function will work on any mode the class is on */
public getId = () => this.id;
public getStatus = () => this.status;
public lastHeardOfIn = () => Date.now() - this.lastUpdateAt;
public isIdle = () => this.status === 'idle';
public isWorking = () => this.status === 'working';
public isError = () => this.status === 'error';
private updateLastHeardOf = () => this.lastUpdateAt = Date.now();
private updateStatus = (status: NodeStatus) => this.status = status;
public untilFinish = async () => { // await until the node is idle
await await_interval(() => this.isIdle(), 1000)
.catch(() => { throw new Error('The node is not idle') })
return true;
}
public run = async (method: string, parameter: any) => {
if(this.mode === 'client') return await this.run_client({ method, parameter });
else if(this.mode === 'server') return await this.run_server({ method, parameter });
else throw new Error('The mode has not been set');
}
public exec = async (method: string, code: string) => {
if(this.mode === 'client') return await this.exec_client(code);
else if(this.mode === 'server') return await this.exec_server(code);
else throw new Error('The mode has not been set');
}
public setServices = async (services: ServiceAddress[]) => {
if(this.mode === 'client') return await this.setServices_client(services);
else if(this.mode === 'server') return await this.setServices_server(services);
else throw new Error('The mode has not been set');
}
public exit = async () => {
if(this.mode === 'client') return await this.exit_client();
else if(this.mode === 'server') return await this.exit_server();
else throw new Error('The mode has not been set');
}
public ping = async () => {
if(this.mode === 'client') return await this.ping_client();
else if(this.mode === 'server') return await this.ping_server();
else throw new Error('The mode has not been set');
}
/* this functions will set the Node.ts as a client handler for the server */
public setNodeConnection(connection: Connection, network: Network){
if(this.mode !== undefined && this.mode !== null )
throw new Error('The node mode has already been set');
// set the mode as a server client hander
this.mode = 'server';
// get the node id from the conenction
this.id = connection.getTargetId();
// set the network
this.network = network;
// define the listners which we will be using to talk witht the client node
if(this.stashSetFunction === null || this.stashGetFunction === null )
throw new Error('The stash functions have not been set');
// set the listeners
this.listeners = [// this callbacks will run when we recive this event from the client node
{ event: '_set_status', parameters: ['status'], callback: this.handleStatusChange.bind(this) },
{ event: '_ping', parameters: [], callback: () => '_pong' },
{ event: '_set_stash', parameters: ['key', 'value'], callback: this.stashSetFunction },
{ event: '_get_stash', parameters: ['key'], callback: this.stashGetFunction },
]
// register the listeners on the connection
connection.setListeners(this.listeners);
}
public setStatusChangeCallback(callback: (status: NodeStatus, node: Node) => void){
this.statusChangeCallback = callback;
}
public setStashFunctions({ set, get }: { set: (key: string, value: any) => any, get: (key: string) => any }){
this.stashSetFunction = ({ key, value }: { key: string, value: any }) => set(key, value);
this.stashGetFunction = get;
}
public handleStatusChange(status: NodeStatus){
// set status as status and call the callback
this.updateStatus(status);
this.statusChangeCallback && this.statusChangeCallback(status, this);
}
public lastHeardOf(){
// this function will be called when the client node tells us that it is working
this.updateLastHeardOf();
return this.lastHeardOfIn();
}
private async run_server({method, parameter}: {method: string, parameter: any}){
// this function will send the node a method to be run in the client
// set the status to working
this.handleStatusChange('working');
let res = await this.send('_run', { method, parameter });
// set the status to idle
this.handleStatusChange('idle');
// if there is an error
if(res.isError === true)
res.error = deserializeError(res.error);
// return the result
return res
}
private async exec_server(code: string){
// this function will send the node a code to be run in the client
// set the status to working
this.handleStatusChange('working');
let res = await this.send('_exec', code);
// set the status to idle
this.handleStatusChange('idle');
// if there is an error
if(res.isError === true)
res.error = deserializeError(res.error);
// return the result
return res
}
private async setServices_server(services: ServiceAddress[]){
// this function will send send a list of services to the client node
let res = await this.send('_set_services', services);
return res;
}
public async ping_server(){
// this function will ping the client node
let res = await this.send('_ping');
if(res === 'pong') this.updateLastHeardOf();
return true;
}
public async exit_server(){
// this function tell the node client to exit
let res = await this.send('_exit', null)
// we catch the timeout erro scince the client node will exit
.catch((error) => { if(error === 'timeout') return true; else throw error; });
return res
}
public async registerServices(service: ServiceAddress[]){
// for every service we need to send the service address to the client node
let services = service.map(service => new Promise(async (resolve) => {
let result = await this.send('_connect_service', service);
resolve(result);
}));
// await until they are all connected
return await Promise.all(services);
}
public async send(method: string, parameter: any = null){
// fucntion for sending a method to the client node
if(this.network === undefined) throw new Error('The network has not been set');
if(this.id === undefined) throw new Error('The id has not been set');
if(this.mode === undefined) throw new Error('The mode has not been set');
// get the connection of which we will send the method
let connection: Connection | undefined = undefined;
if(this.mode === 'server')
connection = this.network.getNode(this.id);
else if(this.mode === 'client')
connection = this.network.getService('master');
if(connection === undefined)
throw new Error('Could not get the conenction from the network');
// send the method to the node
return await connection.send(method, parameter);
}
/* this function will be called when the client node tells us that it is working */
public async connectToMaster(host: string, port: number){
// conenct the master process which will tell us what to do
// create an id for the node
this.id = this.id || Math.random().toString(36).substring(4);
this.network = new Network({name: 'node', id: this.id});
// form the conenction with the master
this.network.connect({ host, port, as: 'master' });
// set the mode as a client
this.mode = 'client';
// set the listeners which we will us on the and the master can call on
this.listeners = [
{ event: '_run', parameters: ['method', 'parameter'], callback: this.run_client.bind(this) },
{ event: '_exec', parameters: ['code_string'], callback: this.exec_client.bind(this) },
{ event: '_set_services', parameters: ['services'], callback: this.setServices_client.bind(this) },
{ event: '_is_idle', parameters: [], callback: this.isIdle.bind(this) },
{ event: '_is_busy', parameters: [], callback: this.isBusy.bind(this) },
{ event: '_has_done', parameters: ['method'], callback: this.hasDone.bind(this) },
{ event: '_ping', parameters: [], callback: () => 'pong' },
{ event: '_exit', parameters: [], callback: this.exit_client.bind(this) }
];
// register the listeners on the network
this.network.registerListeners(this.listeners);
}
private async run_client({method, parameter}: {method: string, parameter: any}){
// this function will be called by the a service or another node to run a function
// wait until services are connected, with timeout of 10 seconds
await await_interval(() => this.servicesConnected, 10000).catch(() => {
throw new Error(`[Node][${this.id}] Could not connect to the services`);
})
try {
// set the status to working
this.updateStatus('working');
// get the services that we have connected to
let services = this.services.map(
(s: ServiceAddress) => new ServiceClient(s.name, this.network as Network)
).reduce((acc: any, s: ServiceClient) => {
acc[s.name] = s;
return acc;
}, {})
// run method
const result = await this.methods[method](parameter, { ...services, slave: this, self: this });
// set has done method
this.doneMethods[method] = true;
// return the result
return { result, isError: false };
} catch(error){ // serilize the error
this.updateStatus('error');
// return the error
return { error: serializeError(error), isError: true };
} finally {
// set the status to idle
this.updateStatus('idle');
}
}
private async exec_client(code_string: string){
/* this function will execute some passed albitrary code */
// check if the code_string is a string
if(typeof code_string !== 'string')
return { isError: true, error: serializeError(new Error('Code string is not a string')) }
// await until service is connected
await await_interval(() => this.servicesConnected, 10000).catch(() => {
throw new Error(`[Service] Could not connect to the services`);
})
let services = this.services.map(
(s: ServiceAddress) => new ServiceClient(s.name, this.network as Network)
).reduce((acc: any, s: ServiceClient) => {
acc[s.name] = s;
return acc;
}, {})
let parameter = { ...services, master: this, self: this };
try {
// run the albitrary code
let result = await execAsyncCode(code_string, parameter);
return { result: result, isError: false };
} catch(e) {
return { isError: true, error: serializeError(e) }
}
}
public async _startup(){
// this function should not be here, and Node class should be self contained
// thus this class need an outside class to call it, after it has set up its
// addMethods and setServices and connectToMaster functions have run.
if(this.methods['_startup'] !== undefined)
await this.run_client({ method: '_startup', parameter: null });
}
// this function will communicate with the master node and set the stash in that moment
public setStash = async (key: any, value: any = null) => await this.send('_set_stash', { key, value });
public getStash = async (key: string = '') => await this.send('_get_stash', key);
public addMethods(methods: { [key: string]: (parameter: any) => any }){
// we add the methods to this class
this.methods = methods;
// populate methods done
for(let method in methods)
this.doneMethods[method] = false;
}
private async setServices_client(services: ServiceAddress[]){
// we get the list of services that we need to connect to
this.services = services;
// connect to the services
for(let service of services){
let res = await this.connectService(service);
if(!res)
console.error('Could not connect to the service, ', service.name);
else
log(`[Node][${this.id}] Connected to the service, ${service.name}`);
}
this.servicesConnected = true;
return true
}
public async connectService({ name, host, port }: ServiceAddress){
/* this is the client inplementation.
* it will connect to the service and create methods
* for every listener that the service has */
if(!host || !port)
throw new Error('The service information is not complete');
// check if there is a service already running on the port and host
if(this.network === undefined)
throw new Error('The network has not been set');
return await this.network.connect({name, host, port});
}
private async ping_client(){
// this function will ping the master node
let res = await this.send('_ping');
if(res === '_pong') this.updateLastHeardOf();
return true;
}
private async exit_client(){
// before we bail we must be nice enough to close our connections
setTimeout(async () => {
// if there is a _cleanup method defined
if(this.methods['_cleanup'] !== undefined)
await this.run_client({ method: '_cleanup', parameter: null });
// we close the connections we have,
if(this.network !== undefined) this.network.close();
// then we exit the process
process.exit(0);
}, 1000);
return true
}
public getListeners(){
if(this.network === undefined) throw new Error('The network has not been set');
if(this.id === undefined) throw new Error('The id has not been set');
let listeners = [];
let connection: Connection | undefined = undefined;
if(this.mode === 'server'){
connection = this.network.getNode(this.id);
listeners = connection.getListeners();
}else if(this.mode === 'client'){
connection = this.network.getNode('master');
listeners = connection.getListeners();
if(connection === undefined)
throw new Error('Could not get the conenction from the network');
}
return listeners;
}
public hasDone(method: string){
return this.doneMethods[method] || false;
}
/* method synonims */
public isBusy = this.isWorking;
public hasFinished = this.hasDone;
public hasError = this.isError;
public toFinish = this.untilFinish;
public set = this.setStash;
public get = this.getStash;
public stash = this.setStash;
public unstash = this.getStash;
}
export default Node;