UNPKG

noflo-runtime-websocket

Version:

NoFlo runtime for execution on Node.js over WebSockets

157 lines (145 loc) 4.16 kB
const { server: WebSocketServer } = require('websocket'); const Base = require('noflo-runtime-base'); const debugSend = require('debug')('noflo-runtime-websocket:send'); const debugReceive = require('debug')('noflo-runtime-websocket:receive'); function normalizePayload(payload) { if (typeof payload !== 'object' && typeof payload !== 'function') { return payload; } if (payload instanceof Error) { return { message: payload.message, stack: payload.stack, }; } if (Buffer.isBuffer(payload)) { return payload.slice(0, 20); } if (Array.isArray(payload)) { return payload; } if (payload.toJSON) { return payload.toJSON(); } if (payload.toString) { const stringified = payload.toString(); if (stringified === '[object Object]') { try { return JSON.parse(JSON.stringify(payload)); } catch (e) { return stringified; } } return stringified; } return payload; } class WebSocketRuntime extends Base { constructor(options = {}) { super(options); this.connections = []; if (options.catchExceptions) { process.on('uncaughtException', (err) => { this.connections.forEach((connection) => { this.send('network', 'error', err, { connection, }); if (err.stack) { console.error(err.stack); } else { console.error(`Error: ${err.toString()}`); } }); }); } if (options.captureOutput) { this.startCapture(); } } send(protocol, topic, payload, context) { if (!context || !context.connection || !context.connection.connected) { return; } let normalizedPayload = payload; if (payload instanceof Error) { normalizedPayload = normalizePayload(payload); } if (protocol === 'runtime' && topic === 'packet') { // With exported port packets we need to go one deeper normalizedPayload.payload = normalizePayload(normalizedPayload.payload); } debugSend(`${protocol}:${topic}`); context.connection.sendUTF(JSON.stringify({ protocol, command: topic, payload: normalizedPayload, })); super.send(protocol, topic, payload, context); } sendAll(protocol, topic, payload) { this.connections.forEach((connection) => { this.send(protocol, topic, payload, { connection, }); }); } startCapture() { this.originalStdOut = process.stdout.write; process.stdout.write = (string) => { this.connections.forEach((connection) => { this.send('network', 'output', { message: string.replace(/\n$/, ''), type: 'message', }, { connection, }); }); }; } stopCapture() { if (!this.originalStdOut) { return; } process.stdout.write = this.originalStdOut; } } module.exports = function (httpServer, options) { const wsServer = new WebSocketServer({ httpServer, }); const runtime = new WebSocketRuntime(options); const handleMessage = function (message, connection) { if (message.type === 'utf8') { let contents; try { contents = JSON.parse(message.utf8Data); } catch (e) { if (e.stack) { console.error(e.stack); } else { console.error(`Error: ${e.toString()}`); } return; } debugReceive(`${contents.protocol}:${contents.command}`); runtime.receive(contents.protocol, contents.command, contents.payload, { connection, }); } }; wsServer.on('request', (request) => { const subProtocol = (request.requestedProtocols.indexOf('noflo') !== -1) ? 'noflo' : null; const connection = request.accept(subProtocol, request.origin); runtime.connections.push(connection); connection.on('message', (message) => { handleMessage(message, connection); }); connection.on('close', () => { if (runtime.connections.indexOf(connection) === -1) { return; } runtime.connections.splice(runtime.connections.indexOf(connection), 1); }); }); return runtime; };