mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-01-12 07:12:54 +01:00
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>
455 lines
13 KiB
JavaScript
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;
|