UNPKG

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
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;