mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-01-12 23:32:55 +01:00
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>
This commit is contained in:
454
packages/noodl-runtime/src/editorconnection.js
Normal file
454
packages/noodl-runtime/src/editorconnection.js
Normal file
@@ -0,0 +1,454 @@
|
||||
'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;
|
||||
Reference in New Issue
Block a user