UNPKG

alm

Version:

The best IDE for TypeScript

284 lines (239 loc) 10.2 kB
// This code is designed to be used by both the server and the client // Very similar to workerlib but designed to work with socket.io // The client is the requestor (hence parent like) and the server is the responder (hence child like) // client == parent // server == child /** * Allcast and Broadcast both come through as a message of the following type */ export var anycastMessageName = 'anycast'; export * from "../common/events"; import * as SocketIO from 'socket.io'; // Lets get the types straight: export type ServerSocket = SocketIO.Socket; export type ClientSocket = SocketIOClient.Socket; // Parent makes queries<T> // Child responds<T> export interface Message<T> { message: string; id: string; data?: T; error?: { method: string; message: string; stack: string; details: any; }; /** Is this message a request or a response */ isRequest: boolean; } export interface CastMessage<T> { message: string; data?: T; } /** Query Response function */ export interface QRFunction<Query, Response> { (query: Query): Promise<Response>; } /** Query Response function for use by server */ export interface QRServerFunction<Query, Response, Client> { (query: Query, client?: any): Promise<Response>; } /** Creates a Guid (UUID v4) */ function createId(): string { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } export class RequesterResponder { /** Must be implemented in children */ protected getSocket: { (): { on?: Function; emit?: <T>(messageType: string, message: Message<T>) => any } } startListening() { try { if (!this.getSocket()) { console.log('You started listening without a socket!'); return; } let socket = this.getSocket(); socket.on('error', (err) => { }); socket.on('message', (message: Message<any>) => { if (message.isRequest) { this.processRequest(message); } else { this.processResponse(message); } }); } catch (err) { console.log('Socket : terminal error in listening', err); } } ///////////////////////////////// REQUESTOR ///////////////////////// private currentListeners: { [message: string]: { [id: string]: PromiseDeferred<any> } } = {}; /** Only relevant when we only want the last of this type */ private currentLastOfType: { [message: string]: { data: any; defer: PromiseDeferred<any>; } } = {}; private pendingRequests: { [id: string]: string; // The name of message } = Object.create(null); public pendingRequestsChanged = (pending: string[]) => null; /** process a message from the server */ protected processResponse(m: any) { var parsed: Message<any> = m; if (!parsed.message || !parsed.id) { console.log('SERVER ERR: Invalid JSON data from server:', m); } else if (!this.currentListeners[parsed.message] || !this.currentListeners[parsed.message][parsed.id]) { console.log('SERVER ERR: No one was listening:', parsed.message, parsed.data); } else { // Alright nothing *weird* happened delete this.pendingRequests[parsed.id] this.pendingRequestsChanged(Object.keys(this.pendingRequests).map(k=>this.pendingRequests[k])); if (parsed.error) { this.currentListeners[parsed.message][parsed.id].reject(parsed.error); console.log(parsed.error); console.log(parsed.error.stack); } else { this.currentListeners[parsed.message][parsed.id].resolve(parsed.data); } delete this.currentListeners[parsed.message][parsed.id]; // If there is current last one queued then that needs to be resurrected if (this.currentLastOfType[parsed.message]) { let last = this.currentLastOfType[parsed.message]; delete this.currentLastOfType[parsed.message]; let lastPromise = this.sendToServerHeart(last.data, parsed.message); lastPromise.then((res) => last.defer.resolve(res), (rej) => last.defer.reject(rej)); } } } private sendToServerHeart = (data, message) => { // If we don't have a server exit if (!this.getSocket()) { console.log('SEND ERR: no server when you tried to send :', message); return <any>Promise.reject(new Error("No socket active to recieve message: " + message)); } // Initialize if this is the first call of this type if (!this.currentListeners[message]) this.currentListeners[message] = {}; // Create an id unique to this call and store the defered against it var id = createId(); const promise = new Promise((resolve,reject)=>{ this.currentListeners[message][id] = { resolve, reject, promise }; }); // Send data to worker this.pendingRequests[id] = message; this.pendingRequestsChanged(Object.keys(this.pendingRequests).map(k=>this.pendingRequests[k])); this.getSocket().emit('message', { message: message, id: id, data: data, isRequest: true }); return promise; } /** * Send all the member functions to IPC */ sendAllToSocket<TWorker>(contract: TWorker): TWorker { var toret = {} as TWorker; Object.keys(contract).forEach(key => { toret[key] = this.sendToSocket(contract[key], key); }); return toret; } /** * Takes a sync named function * and returns a function that will execute this function by name using IPC * (will only work if the process on the other side has this function as a registered responder) */ sendToSocket<Query, Response>(func: QRFunction<Query, Response>, name:string): QRFunction<Query, Response> { var message = name; return (data) => this.sendToServerHeart(data, message); } /** * If there are more than one pending then we only want the last one as they come in. * All others will get the default value */ sendToSocketOnlyLast<Query, Response>(func: QRFunction<Query, Response>, defaultResponse: Response, name:string): QRFunction<Query, Response> { return (data) => { var message = name; // If we don't have a child exit if (!this.getSocket()) { console.log('SEND ERR: no socket when you tried to send :', message); return <any>Promise.reject(new Error("No worker active to recieve message: " + message)); } // Allow if this is the only call of this type if (!Object.keys(this.currentListeners[message] || {}).length) { return this.sendToServerHeart(data, message); } else { // Note: // The last needs to continue once the current one finishes // That is done in our response handler // If there is already something queued as last. // Then it is no longer last and needs to be fed a default value if (this.currentLastOfType[message]) { this.currentLastOfType[message].defer.resolve(defaultResponse); } // this needs to be the new last const promise = new Promise<Response>((resolve,reject)=>{ this.currentLastOfType[message] = { data: data, defer: {promise,resolve,reject} } }); return promise; } }; } ////////////////////////////////// RESPONDER //////////////////////// /** Client is an optionl service provided to the responders to call back into the requestor */ public client: any; private responders: { [message: string]: (query: any, client?: any) => Promise<any> } = {}; protected processRequest = (m: any) => { var parsed: Message<any> = m; if (!parsed.message || !this.responders[parsed.message]) { // TODO: handle this error scenario. Either the message is invalid or we do not have a registered responder return; } var message = parsed.message; var responsePromise: Promise<any>; try { responsePromise = this.responders[message](parsed.data, this.client); } catch (err) { responsePromise = Promise.reject({ method: message, message: err.message, stack: err.stack, details: err.details || {} }); } responsePromise .then((response) => { this.getSocket().emit('message', { message: message, /** Note: to process a request we just pass the id as we recieve it */ id: parsed.id, data: response, error: null, isRequest: false }); }) .catch((error) => { this.getSocket().emit('message', { message: message, /** Note: to process a request we just pass the id as we recieve it */ id: parsed.id, data: null, error: error, isRequest: false }); }); } private addToResponders<Query, Response>(func: (query: Query) => Promise<Response>,name: string) { this.responders[name] = func; } registerAllFunctionsExportedFromAsResponders(aModule: any) { Object.keys(aModule) .filter((funcName) => typeof aModule[funcName] == 'function') .forEach((funcName) => this.addToResponders(aModule[funcName], funcName)); } }