noflo-runtime-base
Version:
Base library for building NoFlo runtimes
322 lines (290 loc) • 9.31 kB
JavaScript
const noflo = require('noflo');
const {
EventEmitter,
} = require('events');
function sendToInport(port, event, payload) {
const socket = noflo.internalSocket.createSocket();
port.attach(socket);
switch (event) {
case 'begingroup': socket.beginGroup(payload); break;
case 'endgroup': socket.endGroup(payload); break;
case 'data': socket.send(payload); break;
default: {
// Ignored
}
}
port.detach(socket);
}
function findPort(network, name, inPort) {
let internal;
if (!network || !network.graph) { return null; }
if (inPort) {
internal = network.graph.inports[name];
} else {
internal = network.graph.outports[name];
}
if (!(internal != null ? internal.process : undefined)) { return null; }
const node = network.getNode(internal.process);
if (!node) {
return null;
}
const { component } = node;
if (!component) {
return null;
}
if (inPort) {
return component.inPorts.ports[internal.port];
}
return component.outPorts.ports[internal.port];
}
function portToPayload(pub, internal, network, inPort) {
const def = {
id: pub,
type: 'all',
description: (internal.metadata != null ? internal.metadata.description : undefined),
addressable: false,
required: false,
};
const port = findPort(network, pub, inPort);
// Network has been prepared but isn't running yet so
// we don't have full component info
if (!port) { return def; }
def.type = port.getDataType() || 'all';
if (typeof port.getSchema === 'function' ? port.getSchema() : undefined) { def.schema = port.getSchema(); }
def.description = (internal.metadata != null ? internal.metadata.description : undefined) || port.getDescription() || '';
def.addressable = port.isAddressable();
def.required = port.isRequired();
return def;
}
function portsPayload(name, network) {
let internal;
const payload = {
graph: name,
inPorts: [],
outPorts: [],
};
if (!(network != null ? network.graph : undefined)) { return payload; }
Object.keys(network.graph.inports).forEach((pub) => {
internal = network.graph.inports[pub];
payload.inPorts.push(portToPayload(pub, internal, network, true));
});
Object.keys(network.graph.outports).forEach((pub) => {
internal = network.graph.outports[pub];
payload.outPorts.push(portToPayload(pub, internal, network, false));
});
return payload;
}
class RuntimeProtocol extends EventEmitter {
constructor(transport) {
super();
this.transport = transport;
this.outputSockets = {}; // graphName -> publicPort -> noflo.Socket
this.mainGraph = null;
this.transport.network.on('removenetwork', (network, name) => {
this.subscribeOutdata(name, network, false);
this.subscribeOutPorts(name, network);
this.subscribeExportedPorts(name, network, false);
return this.sendPorts(name, null);
});
}
registerNetwork(name, network) {
this.subscribeExportedPorts(name, network, true);
this.subscribeOutPorts(name, network);
this.sendPorts(name, network);
if (network.isStarted()) {
// processes don't exist until started
this.subscribeOutdata(name, network, true);
}
network.once('start', () => {
// processes don't exist until started
this.subscribeOutdata(name, network, true);
});
}
send(topic, payload, context) {
return this.transport.send('runtime', topic, payload, context);
}
sendAll(topic, payload) {
return this.transport.sendAll('runtime', topic, payload);
}
sendError(err, context) {
return this.send('error', err, context);
}
receive(topic, payload, context) {
switch (topic) {
case 'getruntime': return this.getRuntime(payload, context);
case 'packet':
return this.sendPacket(payload)
.then(() => {
this.send('packetsent', {
port: payload.port,
event: payload.event,
graph: payload.graph,
payload: payload.payload,
}, context);
}, (err) => {
this.sendError(err, context);
});
default: return this.send('error', new Error(`runtime:${topic} not supported`), context);
}
}
getRuntimeDefinition() {
let {
type,
} = this.transport.options;
if (!type) {
if (noflo.isBrowser()) {
type = 'noflo-browser';
} else {
type = 'noflo-nodejs';
}
}
const payload = {
type,
version: this.transport.version,
};
// Add project metadata if available
if (this.transport.options.id) { payload.id = this.transport.options.id; }
if (this.transport.options.label) { payload.label = this.transport.options.label; }
if (this.transport.options.namespace) { payload.namespace = this.transport.options.namespace; }
if (this.transport.options.repository) {
payload.repository = this.transport.options.repository;
}
if (this.transport.options.repositoryVersion) {
payload.repositoryVersion = this.transport.options.repositoryVersion;
}
return payload;
}
getRuntime(request, context) {
const payload = this.getRuntimeDefinition();
const {
capabilities,
} = this.transport.options;
const secret = request ? request.secret : null;
payload.allCapabilities = capabilities;
payload.capabilities = capabilities.filter(
(capability) => this.transport.canDo(capability, secret),
);
if (this.mainGraph) {
payload.graph = this.mainGraph;
}
this.send('runtime', payload, context);
// send port info about currently set up networks
return (() => {
const result = [];
Object.keys(this.transport.network.networks).forEach((name) => {
const network = this.transport.network.getNetwork(name);
result.push(this.sendPorts(name, network, context));
});
return result;
})();
}
sendPorts(name, network, context) {
const payload = portsPayload(name, network);
this.emit('ports', payload);
if (!context) {
return this.sendAll('ports', payload);
}
return this.send('ports', payload, context);
}
setMainGraph(id) {
this.mainGraph = id;
}
// XXX: should send updated runtime info?
subscribeExportedPorts(name, network, add) {
const sendExportedPorts = () => this.sendPorts(name, network);
const dependencies = [
'addInport',
'addOutport',
'removeInport',
'removeOutport',
];
dependencies.forEach((d) => {
network.graph.removeListener(d, sendExportedPorts);
});
if (add) {
const result = [];
dependencies.forEach((d) => {
result.push(network.graph.on(d, sendExportedPorts));
});
}
}
subscribeOutPorts(name, network, add) {
const portRemoved = () => this.subscribeOutdata(name, network, false);
const portAdded = () => this.subscribeOutdata(name, network, true);
const {
graph,
} = network;
graph.removeListener('addOutport', portAdded);
graph.removeListener('removeOutport', portRemoved);
if (add) {
graph.on('addOutport', portAdded);
graph.on('removeOutport', portRemoved);
}
}
subscribeOutdata(graphName, network, add) {
// Unsubscribe all
if (!this.outputSockets[graphName]) { this.outputSockets[graphName] = {}; }
let graphSockets = this.outputSockets[graphName];
Object.keys(graphSockets).forEach((pub) => {
const socket = graphSockets[pub];
socket.removeAllListeners('ip');
});
graphSockets = {};
if (!add) { return; }
// Subscribe new
Object.keys(network.graph.outports).forEach((pub) => {
const internal = network.graph.outports[pub];
const socket = noflo.internalSocket.createSocket();
graphSockets[pub] = socket;
const {
component,
} = network.processes[internal.process];
if (!(component != null ? component.outPorts[internal.port] : undefined)) {
throw new Error(`Exported outport ${internal.port} in node ${internal.process} not found`);
}
component.outPorts[internal.port].attach(socket);
let event;
socket.on('ip', (ip) => {
switch (ip.type) {
case 'openBracket':
event = 'begingroup';
break;
case 'closeBracket':
event = 'endgroup';
break;
default:
event = ip.type;
}
this.emit('packet', {
port: pub,
event,
graph: graphName,
payload: ip.data,
});
this.sendAll('packet', {
port: pub,
event,
graph: graphName,
payload: ip.data,
});
});
});
}
sendPacket(payload) {
return new Promise((resolve, reject) => {
const network = this.transport.network.getNetwork(payload.graph);
if (!network) {
reject(new Error(`Cannot find network for graph ${payload.graph}`));
return;
}
const port = findPort(network, payload.port, true);
if (!port) {
reject(new Error(`Cannot find internal port for ${payload.port}`));
return;
}
sendToInport(port, payload.event, payload.payload);
resolve();
});
}
}
module.exports = RuntimeProtocol;