import {uuid_v4} from './utility'; const _FWS = WebSocket; export class FlitterSocketTransaction { connection: any; type: string; id: string; resolved = false; socket: any; outgoing: any; incoming: any; connectionId: string; sent = false; received = false; // tslint:disable-next-line:variable-name _status = 200; // tslint:disable-next-line:variable-name _message = ''; endpoint = ''; json = ''; constructor(data, conn) { this.connection = conn; this.type = data.type; this.id = data.transaction_id ? data.transaction_id : conn.uuid(); this.socket = conn.socket; this.outgoing = data.outgoing ? data.outgoing : {}; this.incoming = data.incoming ? data.incoming : {}; this.connectionId = conn.id; } resolve() { this.resolved = true; } status(code: number = null) { if ( code ) { this._status = code; return this; } return this._status; } message(msg: string = null) { if ( msg ) { this._message = msg; return this; } return this._message; } send(data: any = null) { if ( this.type === 'response' ) { if ( this.resolved ) { throw new Error(`Transaction can only be sent once per request. (ID: ${this.id})`); } const obj = { status: this._status, transaction_id: this.id, type: 'response', ... (this._message && {message: this._message}), ... (data && {data}), ... ((!data && this.outgoing) && {data: this.outgoing}) }; this.json = JSON.stringify(obj); this.resolve(); } else if ( this.type === 'request' ) { if ( this.sent ) { throw new Error(`Request can only be sent once per Transaction. (ID: ${this.id})`); } const obj = { endpoint: this.endpoint, transaction_id: this.id, type: 'request', ... (data && {data}), ... ((!data && this.outgoing) && {data: this.outgoing}) }; this.json = JSON.stringify(obj); } this.sent = true; return this.socket.send(this.json); } } export class FlitterSocketClientServerTransaction extends FlitterSocketTransaction { // tslint:disable-next-line:variable-name _handler: any; constructor(data, conn) { if ( data.data ) { data.outgoing = data.data; } super(data, conn); this.type = 'request'; if ( data.endpoint ) { this.endpoint = data.endpoint; } } handler(fn) { if ( !fn ) { return this._handler; } this._handler = fn; return this; } receipt(data) { this.received = true; return this._handler(this, this.socket, data); } } export class FlitterSocketServerClientTransaction extends FlitterSocketTransaction { constructor(data, conn) { if ( data.data ) { data.incoming = data.data; } super(data, conn); this.type = 'response'; if ( data.endpoint ) { this.endpoint = data.endpoint; } } set(key, value = null) { if ( value && typeof this.outgoing === 'object' ) { this.outgoing[key] = value; } else { this.outgoing = key; } return this; } } export class FlitterSocketConnection { url: string; open = false; activeTransactions: any = {}; id: string; // tslint:disable-next-line:variable-name _controller: any = {}; closeResolves: any[] = []; closeCallbacks: any[] = []; openResolves: any[] = []; openCallbacks: any[] = []; socket: any; constructor(url) { this.url = url; this.open = false; this.activeTransactions = {}; this.id = this.uuid(); this._controller = {}; this._create_socket(); this.closeResolves = []; this.closeCallbacks = []; this.openResolves = []; this.openCallbacks = []; } on_close(callback = false) { if ( !callback ) { return new Promise(resolve => { this.closeResolves.push(resolve); }); } else { this.closeCallbacks.push(callback); } } on_open(callback = false) { if ( !callback ) { return new Promise(resolve => { this.openResolves.push(resolve); }); } else { this.openCallbacks.push(callback); } } _create_socket() { this.socket = new _FWS(this.url); this.socket.onopen = (e) => { this.open = true; this.openResolves.forEach(r => r(this)); this.openCallbacks.forEach(c => c(this)); this.openResolves = []; this.openCallbacks = []; }; this.socket.onmessage = this._on_message.bind(this); this.socket.onclose = (e) => { this.open = false; this.closeResolves.forEach(r => r(this)); this.closeCallbacks.forEach(c => c(this)); this.closeResolves = []; this.closeCallbacks = []; }; } _on_message(event) { const data = this.validate_message(event.data); if ( !data ) { return; } if ( data.type === 'response' ) { if ( Object.keys(this.activeTransactions).includes(data.transaction_id) ) { const t = this.activeTransactions[data.transaction_id]; t.receipt(data.data); if ( t.resolved ) { delete this.activeTransactions[t.id]; } } else { throw new Error('Response: transaction ID not found. It\'s possible that the transaction was already resolved.'); } } else if ( data.type === 'request' ) { const t = new FlitterSocketServerClientTransaction(data, this); this.activeTransactions[t.id] = t; this._controller[t.endpoint](t, this.socket); if ( t.resolved ) { delete this.activeTransactions[t.id]; } } } request(endpoint, data, handler) { const t = new FlitterSocketClientServerTransaction({data, endpoint}, this); t.handler(handler); this.activeTransactions[t.id] = t; t.send(); } async asyncRequest(endpoint, data): Promise<[any, any, any]> { return new Promise((res, rej) => { try { this.request(endpoint, data, (...args: any) => { res(args); }); } catch (e: unknown) { rej(e); } }); } validate_message(msg) { let fail = false; let validTid = true; let error = ''; let code = 400; // check if valid JSON if ( !this.is_json(msg) ) { fail = true; error = 'Incoming message must be valid FSP JSON object.'; validTid = false; } let data; if ( !fail ) { data = JSON.parse(msg); } // check for required fields: transaction_id, type if ( !fail && !Object.keys(data).includes('transaction_id') ) { fail = true; error = 'Incoming message must include universally-unique transaction_id.'; validTid = false; } if ( !fail && (!Object.keys(data).includes('type') || !(['request', 'response'].includes(data.type))) ) { fail = true; error = 'Incoming message must include valid type, which may be one of: request, response.'; } // if request, check for required fields: endpoint if ( !fail && data.type === 'request' && !Object.keys(data).includes('endpoint') ) { fail = true; error = 'Incoming request message must include a valid endpoint.'; } // if request, check if transaction_id is unique if ( !fail && data.type === 'request' && Object.keys(this.activeTransactions).includes(data.transaction_id) ) { fail = true; error = 'Incoming request message must have a universally-unique, NON-EXISTENT transaction_id.'; validTid = false; } // if request, check for valid endpoint if ( !fail && data.type === 'request' && !(typeof this._controller[data.endpoint] === 'function') ) { fail = true; error = 'The requested endpoint does not exist or is invalid.'; code = 404; } // if response, check if transaction_id exists if ( !fail && data.type === 'response' && !Object.keys(this.activeTransactions).includes(data.transaction_id)) { fail = true; error = 'The specified transaction_id does not exist. It\'s possible that this transaction has already resolved.'; } if ( fail ) { const sendData = { transaction_id: validTid ? data.transaction_id : 'unknown', message: error, status: code, type: 'response' }; this.send_raw(sendData); } else { return data; } } send_raw(data) { this.socket.send(JSON.stringify(data)); } uuid() { return uuid_v4(); } controller(set: any = false) { if ( !set ) { return this._controller; } this._controller = set; return this; } is_json(str) { try { JSON.parse(str); return true; } catch (e) { return false; } } }