@node-red/runtime 3.1.8 → 4.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/api/comms.js +33 -4
- package/lib/api/context.js +55 -19
- package/lib/api/plugins.js +19 -0
- package/lib/index.js +3 -1
- package/lib/multiplayer/index.js +119 -0
- package/lib/nodes/index.js +5 -1
- package/lib/plugins.js +1 -0
- package/locales/en-US/runtime.json +1 -0
- package/package.json +3 -3
package/lib/api/comms.js
CHANGED
|
@@ -36,7 +36,7 @@ var connections = [];
|
|
|
36
36
|
const events = require("@node-red/util").events;
|
|
37
37
|
|
|
38
38
|
function handleCommsEvent(event) {
|
|
39
|
-
publish(event.topic,event.data,event.retain);
|
|
39
|
+
publish(event.topic,event.data,event.retain,event.session,event.excludeSession);
|
|
40
40
|
}
|
|
41
41
|
function handleStatusEvent(event) {
|
|
42
42
|
if (!event.status) {
|
|
@@ -74,13 +74,17 @@ function handleEventLog(event) {
|
|
|
74
74
|
publish("event-log/"+event.id,event.payload||{});
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
function publish(topic,data,retain) {
|
|
77
|
+
function publish(topic, data, retain, session, excludeSession) {
|
|
78
78
|
if (retain) {
|
|
79
79
|
retained[topic] = data;
|
|
80
80
|
} else {
|
|
81
81
|
delete retained[topic];
|
|
82
82
|
}
|
|
83
|
-
connections.forEach(connection =>
|
|
83
|
+
connections.forEach(connection => {
|
|
84
|
+
if ((!session || connection.session === session) && (!excludeSession || connection.session !== excludeSession)) {
|
|
85
|
+
connection.send(topic,data)
|
|
86
|
+
}
|
|
87
|
+
})
|
|
84
88
|
}
|
|
85
89
|
|
|
86
90
|
|
|
@@ -109,6 +113,10 @@ var api = module.exports = {
|
|
|
109
113
|
*/
|
|
110
114
|
addConnection: async function(opts) {
|
|
111
115
|
connections.push(opts.client);
|
|
116
|
+
events.emit('comms:connection-added', {
|
|
117
|
+
session: opts.client.session,
|
|
118
|
+
user: opts.client.user
|
|
119
|
+
})
|
|
112
120
|
},
|
|
113
121
|
|
|
114
122
|
/**
|
|
@@ -126,6 +134,9 @@ var api = module.exports = {
|
|
|
126
134
|
break;
|
|
127
135
|
}
|
|
128
136
|
}
|
|
137
|
+
events.emit('comms:connection-removed', {
|
|
138
|
+
session: opts.client.session
|
|
139
|
+
})
|
|
129
140
|
},
|
|
130
141
|
|
|
131
142
|
/**
|
|
@@ -157,5 +168,23 @@ var api = module.exports = {
|
|
|
157
168
|
* @return {Promise<Object>} - resolves when complete
|
|
158
169
|
* @memberof @node-red/runtime_comms
|
|
159
170
|
*/
|
|
160
|
-
unsubscribe: async function(opts) {}
|
|
171
|
+
unsubscribe: async function(opts) {},
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @param {Object} opts
|
|
175
|
+
* @param {User} opts.user - the user calling the api
|
|
176
|
+
* @param {CommsConnection} opts.client - the client connection
|
|
177
|
+
* @param {String} opts.topic - the message topic
|
|
178
|
+
* @param {String} opts.data - the message data
|
|
179
|
+
* @return {Promise<Object>} - resolves when complete
|
|
180
|
+
*/
|
|
181
|
+
receive: async function (opts) {
|
|
182
|
+
if (opts.topic) {
|
|
183
|
+
events.emit('comms:message:' + opts.topic, {
|
|
184
|
+
session: opts.client.session,
|
|
185
|
+
user: opts.user,
|
|
186
|
+
data: opts.data
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
}
|
|
161
190
|
};
|
package/lib/api/context.js
CHANGED
|
@@ -68,6 +68,7 @@ var api = module.exports = {
|
|
|
68
68
|
* @param {String} opts.store - the context store
|
|
69
69
|
* @param {String} opts.key - the context key
|
|
70
70
|
* @param {Object} opts.req - the request to log (optional)
|
|
71
|
+
* @param {Boolean} opts.keysOnly - whether to return keys only
|
|
71
72
|
* @return {Promise} - the node information
|
|
72
73
|
* @memberof @node-red/runtime_context
|
|
73
74
|
*/
|
|
@@ -102,6 +103,15 @@ var api = module.exports = {
|
|
|
102
103
|
if (key) {
|
|
103
104
|
store = store || availableStores.default;
|
|
104
105
|
ctx.get(key,store,function(err, v) {
|
|
106
|
+
if (opts.keysOnly) {
|
|
107
|
+
if (Array.isArray(v)) {
|
|
108
|
+
resolve({ [store]: { format: `array[${v.length}]`}})
|
|
109
|
+
} else if (typeof v === 'object') {
|
|
110
|
+
resolve({ [store]: { keys: Object.keys(v), format: 'Object' } })
|
|
111
|
+
} else {
|
|
112
|
+
resolve({ [store]: { keys: [] }})
|
|
113
|
+
}
|
|
114
|
+
}
|
|
105
115
|
var encoded = util.encodeObject({msg:v});
|
|
106
116
|
if (store !== availableStores.default) {
|
|
107
117
|
encoded.store = store;
|
|
@@ -118,32 +128,58 @@ var api = module.exports = {
|
|
|
118
128
|
stores = [store];
|
|
119
129
|
}
|
|
120
130
|
|
|
131
|
+
|
|
121
132
|
var result = {};
|
|
122
133
|
var c = stores.length;
|
|
123
134
|
var errorReported = false;
|
|
124
135
|
stores.forEach(function(store) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
errorReported
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
136
|
+
if (opts.keysOnly) {
|
|
137
|
+
ctx.keys(store,function(err, keys) {
|
|
138
|
+
if (err) {
|
|
139
|
+
// TODO: proper error reporting
|
|
140
|
+
if (!errorReported) {
|
|
141
|
+
errorReported = true;
|
|
142
|
+
runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key,error:"unexpected_error"}, opts.req);
|
|
143
|
+
var err = new Error();
|
|
144
|
+
err.code = "unexpected_error";
|
|
145
|
+
err.status = 400;
|
|
146
|
+
return reject(err);
|
|
147
|
+
}
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
result[store] = { keys }
|
|
151
|
+
c--;
|
|
152
|
+
if (c === 0) {
|
|
153
|
+
if (!errorReported) {
|
|
154
|
+
runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key},opts.req);
|
|
155
|
+
resolve(result);
|
|
156
|
+
}
|
|
135
157
|
}
|
|
158
|
+
})
|
|
159
|
+
} else {
|
|
160
|
+
exportContextStore(scope,ctx,store,result,function(err) {
|
|
161
|
+
if (err) {
|
|
162
|
+
// TODO: proper error reporting
|
|
163
|
+
if (!errorReported) {
|
|
164
|
+
errorReported = true;
|
|
165
|
+
runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key,error:"unexpected_error"}, opts.req);
|
|
166
|
+
var err = new Error();
|
|
167
|
+
err.code = "unexpected_error";
|
|
168
|
+
err.status = 400;
|
|
169
|
+
return reject(err);
|
|
170
|
+
}
|
|
136
171
|
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
c--;
|
|
140
|
-
if (c === 0) {
|
|
141
|
-
if (!errorReported) {
|
|
142
|
-
runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key},opts.req);
|
|
143
|
-
resolve(result);
|
|
172
|
+
return;
|
|
144
173
|
}
|
|
145
|
-
|
|
146
|
-
|
|
174
|
+
c--;
|
|
175
|
+
if (c === 0) {
|
|
176
|
+
if (!errorReported) {
|
|
177
|
+
runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key},opts.req);
|
|
178
|
+
resolve(result);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
147
183
|
})
|
|
148
184
|
}
|
|
149
185
|
} else {
|
package/lib/api/plugins.js
CHANGED
|
@@ -65,6 +65,25 @@ var api = module.exports = {
|
|
|
65
65
|
runtime.log.audit({event: "plugins.configs.get"}, opts.req);
|
|
66
66
|
return runtime.plugins.getPluginConfigs(opts.lang);
|
|
67
67
|
},
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Gets the editor content for one registered plugin
|
|
71
|
+
* @param {Object} opts
|
|
72
|
+
* @param {User} opts.user - the user calling the api
|
|
73
|
+
* @param {User} opts.user - the user calling the api
|
|
74
|
+
* @param {Object} opts.req - the request to log (optional)
|
|
75
|
+
* @return {Promise<NodeInfo>} - the plugin information
|
|
76
|
+
* @memberof @node-red/runtime_plugins
|
|
77
|
+
*/
|
|
78
|
+
getPluginConfig: async function(opts) {
|
|
79
|
+
if (/[^0-9a-z=\-\*]/i.test(opts.lang)) {
|
|
80
|
+
throw new Error("Invalid language: "+opts.lang)
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
runtime.log.audit({event: "plugins.configs.get"}, opts.req);
|
|
84
|
+
return runtime.plugins.getPluginConfig(opts.module, opts.lang);
|
|
85
|
+
},
|
|
86
|
+
|
|
68
87
|
/**
|
|
69
88
|
* Gets all registered module message catalogs
|
|
70
89
|
* @param {Object} opts
|
package/lib/index.js
CHANGED
|
@@ -22,6 +22,7 @@ var storage = require("./storage");
|
|
|
22
22
|
var library = require("./library");
|
|
23
23
|
var plugins = require("./plugins");
|
|
24
24
|
var settings = require("./settings");
|
|
25
|
+
const multiplayer = require("./multiplayer");
|
|
25
26
|
|
|
26
27
|
var express = require("express");
|
|
27
28
|
var path = require('path');
|
|
@@ -135,6 +136,7 @@ function start() {
|
|
|
135
136
|
.then(function() { return storage.init(runtime)})
|
|
136
137
|
.then(function() { return settings.load(storage)})
|
|
137
138
|
.then(function() { return library.init(runtime)})
|
|
139
|
+
.then(function() { return multiplayer.init(runtime)})
|
|
138
140
|
.then(function() {
|
|
139
141
|
if (settings.available()) {
|
|
140
142
|
if (settings.get('instanceId') === undefined) {
|
|
@@ -154,7 +156,7 @@ function start() {
|
|
|
154
156
|
log.info(log._("runtime.version",{component:"Node.js ",version:process.version}));
|
|
155
157
|
if (settings.UNSUPPORTED_VERSION) {
|
|
156
158
|
log.error("*****************************************************************");
|
|
157
|
-
log.error("* "+log._("runtime.unsupported_version",{component:"Node.js",version:process.version,requires: ">=
|
|
159
|
+
log.error("* "+log._("runtime.unsupported_version",{component:"Node.js",version:process.version,requires: ">=18"})+" *");
|
|
158
160
|
log.error("*****************************************************************");
|
|
159
161
|
events.emit("runtime-event",{id:"runtime-unsupported-version",payload:{type:"error",text:"notification.errors.unsupportedVersion"},retain:true});
|
|
160
162
|
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
let runtime
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Active sessions, mapped by multiplayer session ids
|
|
5
|
+
*/
|
|
6
|
+
const sessions = new Map()
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Active connections, mapping comms session to multiplayer session
|
|
10
|
+
*/
|
|
11
|
+
const connections = new Map()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
function getSessionsList() {
|
|
15
|
+
return Array.from(sessions.values()).filter(session => session.active)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = {
|
|
19
|
+
init: function(_runtime) {
|
|
20
|
+
runtime = _runtime
|
|
21
|
+
runtime.events.on('comms:connection-removed', (opts) => {
|
|
22
|
+
const existingSessionId = connections.get(opts.session)
|
|
23
|
+
if (existingSessionId) {
|
|
24
|
+
connections.delete(opts.session)
|
|
25
|
+
const session = sessions.get(existingSessionId)
|
|
26
|
+
session.active = false
|
|
27
|
+
session.idleTimeout = setTimeout(() => {
|
|
28
|
+
sessions.delete(existingSessionId)
|
|
29
|
+
}, 30000)
|
|
30
|
+
runtime.events.emit('comms', {
|
|
31
|
+
topic: "multiplayer/connection-removed",
|
|
32
|
+
data: { session: existingSessionId }
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
runtime.events.on('comms:message:multiplayer/connect', (opts) => {
|
|
37
|
+
let session
|
|
38
|
+
if (!sessions.has(opts.data.session)) {
|
|
39
|
+
// Brand new session
|
|
40
|
+
let user = opts.user
|
|
41
|
+
if (!user || user.anonymous) {
|
|
42
|
+
user = user || { anonymous: true }
|
|
43
|
+
user.username = `Anon ${Math.floor(Math.random()*100)}`
|
|
44
|
+
}
|
|
45
|
+
session = {
|
|
46
|
+
session: opts.data.session,
|
|
47
|
+
user,
|
|
48
|
+
active: true
|
|
49
|
+
}
|
|
50
|
+
sessions.set(opts.data.session, session)
|
|
51
|
+
connections.set(opts.session, opts.data.session)
|
|
52
|
+
runtime.log.trace(`multiplayer new session:${opts.data.session} user:${user.username}`)
|
|
53
|
+
} else {
|
|
54
|
+
// Reconnected connection - keep existing state
|
|
55
|
+
connections.set(opts.session, opts.data.session)
|
|
56
|
+
// const existingConnection = connections.get(opts.data.session)
|
|
57
|
+
session = sessions.get(opts.data.session)
|
|
58
|
+
session.active = true
|
|
59
|
+
runtime.log.trace(`multiplayer reconnected session:${opts.data.session} user:${session.user.username}`)
|
|
60
|
+
clearTimeout(session.idleTimeout)
|
|
61
|
+
}
|
|
62
|
+
// Tell existing sessions about the new connection
|
|
63
|
+
runtime.events.emit('comms', {
|
|
64
|
+
topic: "multiplayer/connection-added",
|
|
65
|
+
excludeSession: opts.session,
|
|
66
|
+
data: session
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// Send init info to new connection
|
|
70
|
+
const initPacket = {
|
|
71
|
+
topic: "multiplayer/init",
|
|
72
|
+
data: getSessionsList(),
|
|
73
|
+
session: opts.session
|
|
74
|
+
}
|
|
75
|
+
// console.log('<<', initPacket)
|
|
76
|
+
runtime.events.emit('comms', initPacket)
|
|
77
|
+
})
|
|
78
|
+
runtime.events.on('comms:message:multiplayer/disconnect', (opts) => {
|
|
79
|
+
const existingSessionId = connections.get(opts.session)
|
|
80
|
+
connections.delete(opts.session)
|
|
81
|
+
sessions.delete(existingSessionId)
|
|
82
|
+
|
|
83
|
+
runtime.events.emit('comms', {
|
|
84
|
+
topic: "multiplayer/connection-removed",
|
|
85
|
+
data: { session: existingSessionId, disconnected: true }
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
runtime.events.on('comms:message:multiplayer/location', (opts) => {
|
|
89
|
+
// console.log('>>>', opts.user, opts.data)
|
|
90
|
+
|
|
91
|
+
const sessionId = connections.get(opts.session)
|
|
92
|
+
const session = sessions.get(sessionId)
|
|
93
|
+
|
|
94
|
+
if (opts.user) {
|
|
95
|
+
if (session.user.anonymous !== opts.user.anonymous) {
|
|
96
|
+
session.user = opts.user
|
|
97
|
+
runtime.events.emit('comms', {
|
|
98
|
+
topic: 'multiplayer/connection-added',
|
|
99
|
+
excludeSession: opts.session,
|
|
100
|
+
data: session
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
session.location = opts.data
|
|
106
|
+
|
|
107
|
+
const payload = {
|
|
108
|
+
session: sessionId,
|
|
109
|
+
workspace: opts.data.workspace,
|
|
110
|
+
node: opts.data.node
|
|
111
|
+
}
|
|
112
|
+
runtime.events.emit('comms', {
|
|
113
|
+
topic: 'multiplayer/location',
|
|
114
|
+
data: payload,
|
|
115
|
+
excludeSession: opts.session
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
}
|
package/lib/nodes/index.js
CHANGED
|
@@ -173,7 +173,11 @@ function installModule(module,version,url) {
|
|
|
173
173
|
if (info.pending_version) {
|
|
174
174
|
events.emit("runtime-event",{id:"node/upgraded",retain:false,payload:{module:info.name,version:info.pending_version}});
|
|
175
175
|
} else {
|
|
176
|
-
|
|
176
|
+
if (!info.nodes.length && info.plugins.length) {
|
|
177
|
+
events.emit("runtime-event",{id:"plugin/added",retain:false,payload:info.plugins});
|
|
178
|
+
} else {
|
|
179
|
+
events.emit("runtime-event",{id:"node/added",retain:false,payload:info.nodes});
|
|
180
|
+
}
|
|
177
181
|
}
|
|
178
182
|
return info;
|
|
179
183
|
});
|
package/lib/plugins.js
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"removing-modules": "Removing modules from config",
|
|
26
26
|
"added-types": "Added node types:",
|
|
27
27
|
"removed-types": "Removed node types:",
|
|
28
|
+
"removed-plugins": "Removed plugins:",
|
|
28
29
|
"install": {
|
|
29
30
|
"invalid": "Invalid module name",
|
|
30
31
|
"installing": "Installing module: __name__, version: __version__",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@node-red/runtime",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0-beta.2",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "./lib/index.js",
|
|
6
6
|
"repository": {
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
}
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@node-red/registry": "
|
|
20
|
-
"@node-red/util": "
|
|
19
|
+
"@node-red/registry": "4.0.0-beta.2",
|
|
20
|
+
"@node-red/util": "4.0.0-beta.2",
|
|
21
21
|
"async-mutex": "0.4.0",
|
|
22
22
|
"clone": "2.1.2",
|
|
23
23
|
"express": "4.19.2",
|