Files
fluxscape/packages/noodl-runtime/src/editorconnection.js
Michael Cartner b9c60b07dc Initial commit
Co-Authored-By: Eric Tuvesson <eric.tuvesson@gmail.com>
Co-Authored-By: mikaeltellhed <2311083+mikaeltellhed@users.noreply.github.com>
Co-Authored-By: kotte <14197736+mrtamagotchi@users.noreply.github.com>
Co-Authored-By: Anders Larsson <64838990+anders-topp@users.noreply.github.com>
Co-Authored-By: Johan  <4934465+joolsus@users.noreply.github.com>
Co-Authored-By: Tore Knudsen <18231882+torekndsn@users.noreply.github.com>
Co-Authored-By: victoratndl <99176179+victoratndl@users.noreply.github.com>
2024-01-26 11:52:55 +01:00

455 lines
13 KiB
JavaScript

'use strict';
var EventSender = require('./eventsender'),
Services = require('./services/services'),
guid = require('./guid');
const ActiveWarnings = require('./editorconnection.activewarnings');
function EditorConnection(opts) {
var _opts = opts || {};
EventSender.call(this);
this.runtimeType = _opts.runtimeType;
this.platform = _opts.platform;
this.ws =
(_opts.platform && _opts.platform.webSocketClass) || (typeof WebSocket !== 'undefined' ? WebSocket : undefined);
this.wsOptions = (_opts.platform && _opts.platform.webSocketOptions) || undefined;
this.reconnectOnClose = true;
this.enableDebugger = false;
this.lastSendTimestamp = 0;
this.sendQueue = [];
this.sendTimer = undefined;
//used to optimize warnings so we're not sending unneccessary warnings.
//Clan slow down the editor in large projects
this.activeWarnings = new ActiveWarnings();
}
EditorConnection.prototype = Object.create(EventSender.prototype);
EditorConnection.prototype.constructor = EditorConnection;
EditorConnection.prototype.isRunningLocally = function () {
var runningLocallyInBrowser =
(this.platform.isRunningLocally && this.platform.isRunningLocally()) ||
(typeof document !== 'undefined' &&
(document.location.hostname === 'localhost' || document.location.hostname === '127.0.0.1'));
return runningLocallyInBrowser;
};
EditorConnection.prototype.connect = function (address) {
this.socket = this.wsOptions ? new this.ws(address, this.wsOptions) : new this.ws(address);
var self = this;
this.socket.addEventListener('open', function () {
self.clientId = guid();
self.socket.send(
JSON.stringify({
cmd: 'register',
type: 'viewer',
clientId: self.clientId
})
);
self.emit('connected');
});
this.socket.addEventListener('close', function (event) {
if (self.reconnectOnClose) {
self.reconnect(address);
}
console.log('Editor connection closed', event.code, event.reason);
self.emit('connectionClosed');
});
this.socket.addEventListener('error', function (evt) {
console.log('Editor connection error, trying to reconnect');
});
this.socket.addEventListener('message', async (e) => {
// NOTE: When the data is too big it seems to change from string to a blob
const text = typeof e.data === 'string' ? e.data : await e.data.text();
const message = JSON.parse(text);
let content;
if (message.cmd === 'registered') {
//ignore
} else if (message.cmd === 'export') {
content = JSON.parse(message.content);
if (message.type === 'full' && message.target === this.clientId) {
self.emit('exportDataFull', content);
}
} else if (message.cmd === 'hoverStart') {
self.emit('hoverStart', message.content.id);
} else if (message.cmd === 'hoverEnd') {
self.emit('hoverEnd', message.content.id);
} else if (message.cmd === 'refresh') {
self.emit('reload');
} else if (message.cmd === 'debugInspectors') {
if (this.debugInspectorsEnabled) {
content = JSON.parse(message.content);
self.emit('debugInspectorsUpdated', content.inspectors);
}
} else if (message.cmd === 'debuggingEnabled') {
if (self.isRunningLocally()) {
content = JSON.parse(message.content);
self.emit('debuggingEnabledChanged', content.enabled);
}
} else if (message.cmd === 'getConnectionValue') {
if (self.isRunningLocally()) {
content = JSON.parse(message.content);
await self.emit('getConnectionValue', { clientId: content.clientId, connectionId: content.connectionId });
}
} else if (message.cmd === 'modelUpdate') {
await self.emit('modelUpdate', message.content);
} else if (message.cmd === 'publish') {
Services.pubsub.routeMessage(message); // Publish a message from the pubsub service
} else if (message.cmd === 'noodlModules') {
self.emit('noodlModules', JSON.parse(message.content));
} else if (message.cmd === 'mqttUpdate') {
self.emit('mqttUpdate', message.content);
} else if (message.cmd === 'activeComponentChanged') {
self.emit('activeComponentChanged', message.component);
} else {
console.log('Command not implemented', message);
}
});
};
EditorConnection.prototype.reconnect = function (address) {
var self = this;
setTimeout(function () {
self.connect(address);
}, 2000);
};
EditorConnection.prototype.isConnected = function () {
return this.socket !== undefined && this.socket.readyState === this.ws.OPEN;
};
//JSON replacer to make cyclic objects non-cyclic.
//Using this example: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value#examples
const getCircularReplacer = () => {
const seen = new WeakSet();
return (key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return '[Circular]';
}
seen.add(value);
}
return value;
};
};
EditorConnection.prototype.send = function (data) {
const now = this.platform.getCurrentTime();
const dt = now - this.lastSendTimestamp;
//Send objects as json and capture exceptions
const trySend = (msg) => {
try {
this.socket.send(JSON.stringify(msg));
} catch (e) {
if (e.message && e.message.startsWith('Converting circular')) {
//the object is circular, try to address it
try {
this.socket.send(JSON.stringify(msg, getCircularReplacer()));
} catch (e) {
//still failed, give up
console.log('failed to send message to editor', msg, e);
}
} else {
//message couldn't be serialized to json
console.log('failed to send message to editor', msg, e);
}
}
};
//batch messages so they're only sent at most every 200ms
//note that the first message will always be sent immediately, and the ones for 200ms after
//that one will be queued. So initial message response time is as low as possible (for hover etc)
if (dt < 200 || this.sendTimer || !this.isConnected()) {
this.sendQueue.push(data);
if (!this.sendTimer) {
this.sendTimer = setTimeout(() => {
if (this.isConnected() === false) {
return;
}
//send messages in chunks. If we send too many at once the editor UI can freeze for a while
//since it's handling these in the renderer process
const chunkSize = 50;
for (let i = 0; i < this.sendQueue.length; i += chunkSize) {
const chunk = this.sendQueue.slice(i, i + chunkSize);
trySend(chunk);
}
this.sendQueue = [];
this.sendTimer = undefined;
this.lastSendTimestamp = this.platform.getCurrentTime();
}, 100);
}
} else {
this.lastSendTimestamp = now;
trySend(data);
}
};
EditorConnection.prototype.sendInspectId = function (id) {
this.send({
cmd: 'select',
type: 'viewer',
content: JSON.stringify({ id: id })
});
};
EditorConnection.prototype.sendSelectComponent = function (componentName) {
this.send({
cmd: 'select',
type: 'viewer',
content: JSON.stringify({ componentName })
});
};
EditorConnection.prototype.sendPulsingConnections = function (connectionMap) {
var connectionsToPulse = [];
Object.keys(connectionMap).forEach(function (c) {
var connection = connectionMap[c];
connectionsToPulse = connectionsToPulse.concat(connection.connections);
});
this.send({
cmd: 'connectiondebugpulse',
type: 'viewer',
content: JSON.stringify({
connectionsToPulse: connectionsToPulse
})
});
};
EditorConnection.prototype.sendDebugInspectorValues = function (inspectors) {
this.send({
cmd: 'debuginspectorvalues',
type: 'viewer',
content: { inspectors }
});
};
EditorConnection.prototype.sendConnectionValue = function (connectionId, value) {
this.send({
cmd: 'connectionValue',
type: 'viewer',
content: { connectionId, value }
});
};
const dynamicPortsHash = {};
function _detectRename(before, after) {
if (!before || !after) return;
if (before.length !== after.length) return; // Must be of same length
var res = {};
for (var i = 0; i < before.length; i++) {
if (after.indexOf(before[i]) === -1) {
if (res.before) return; // Can only be one from before that is missing
res.before = before[i];
}
if (before.indexOf(after[i]) === -1) {
if (res.after) return; // Only one can be missing,otherwise we cannot match
res.after = after[i];
}
}
return res.before && res.after ? res : undefined;
}
EditorConnection.prototype.sendDynamicPorts = function (id, ports, options) {
var hash = JSON.stringify(ports);
if (dynamicPortsHash[id] === hash) {
// Make sure we don't resend the same port data
return;
}
if (dynamicPortsHash[id] && ports && options && options.detectRenamed) {
var detectRenamed = Array.isArray(options.detectRenamed) ? options.detectRenamed : [options.detectRenamed];
var renamed = [];
detectRenamed.forEach((d) => {
var before = JSON.parse(dynamicPortsHash[id]),
after = [].concat(ports);
// Filter ports with correct prefix and plug
if (d.prefix) {
before = before.filter((p) => p.name.startsWith(d.prefix));
after = after.filter((p) => p.name.startsWith(d.prefix));
}
if (d.plug) {
before = before.filter((p) => p.plug === d.plug);
after = after.filter((p) => p.plug === d.plug);
}
// Remove the prefix
after = after.map((p) => p.name.substring((d.prefix || '').length));
before = before.map((p) => p.name.substring((d.prefix || '').length));
// Find the one that is renamed (if any)
var res = _detectRename(before, after);
if (res) {
renamed.push({
plug: d.plug,
patterns: [(d.prefix || '') + '{{*}}'],
before: res.before,
after: res.after
});
}
});
if (renamed.length > 0) options.renamed = renamed;
delete options.detectRenamed;
}
dynamicPortsHash[id] = hash;
this.send({
cmd: 'instanceports',
type: 'viewer',
content: JSON.stringify({
nodeid: id,
ports: ports,
options: options
})
});
};
EditorConnection.prototype.sendWarning = function (componentName, nodeId, key, warning) {
const isNewWarning = this.activeWarnings.setWarning(nodeId, key, warning);
if (isNewWarning) {
this.send({
cmd: 'showwarning',
type: 'viewer',
content: JSON.stringify({
componentName: componentName,
nodeId: nodeId,
key: key,
warning: warning
})
});
}
};
EditorConnection.prototype.clearWarning = function (componentName, nodeId, key) {
const hasWarning = this.activeWarnings.clearWarning(nodeId, key);
if (hasWarning) {
this.send({
cmd: 'showwarning',
type: 'viewer',
content: JSON.stringify({
componentName: componentName,
nodeId: nodeId,
key: key,
warning: undefined
})
});
}
};
EditorConnection.prototype.clearWarnings = function (componentName, nodeId) {
const hasWarnings = this.activeWarnings.clearWarnings(nodeId);
if (hasWarnings) {
this.send({
cmd: 'clearwarnings',
type: 'viewer',
content: JSON.stringify({
componentName: componentName,
nodeId: nodeId
})
});
}
};
EditorConnection.prototype.sendPatches = function (patches) {
this.send({
cmd: 'patchproject',
type: 'viewer',
content: JSON.stringify(patches)
});
};
EditorConnection.prototype.requestFullExport = function () {
this.send({
cmd: 'register',
type: 'viewer'
});
};
EditorConnection.prototype.requestNoodlModules = function () {
this.send({
cmd: 'getNoodlModules',
type: 'viewer'
});
};
var serviceRequests = {};
EditorConnection.prototype.sendServiceRequest = function (request, callback) {
request.token = guid();
request.clientId = this.clientId;
serviceRequests[request.token] = callback;
this.send(request);
};
EditorConnection.prototype.close = function () {
this.reconnectOnClose = false;
if (this.isConnected() === false) {
return;
}
this.socket.close();
};
EditorConnection.prototype.sendNodeLibrary = function (nodelibrary) {
this.send({
cmd: 'nodelibrary',
type: 'viewer',
runtimeType: this.runtimeType,
content: nodelibrary,
clientId: this.clientId
});
};
EditorConnection.prototype.sendComponentMetadata = function (componentName, key, data) {
this.send({
cmd: 'componentMetadata',
type: 'viewer',
content: JSON.stringify({
componentName,
key,
data
})
});
};
EditorConnection.prototype.sendProjectMetadata = function (key, data) {
this.send({
cmd: 'projectMetadata',
type: 'viewer',
content: JSON.stringify({
key,
data
})
});
};
module.exports = EditorConnection;