@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 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 => connection.send(topic,data))
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
  };
@@ -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
- exportContextStore(scope,ctx,store,result,function(err) {
126
- if (err) {
127
- // TODO: proper error reporting
128
- if (!errorReported) {
129
- errorReported = true;
130
- runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key,error:"unexpected_error"}, opts.req);
131
- var err = new Error();
132
- err.code = "unexpected_error";
133
- err.status = 400;
134
- return reject(err);
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
- return;
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 {
@@ -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: ">=8.9.0"})+" *");
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
+ }
@@ -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
- events.emit("runtime-event",{id:"node/added",retain:false,payload:info.nodes});
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
@@ -7,5 +7,6 @@ module.exports = {
7
7
  getPluginsByType: registry.getPluginsByType,
8
8
  getPluginList: registry.getPluginList,
9
9
  getPluginConfigs: registry.getPluginConfigs,
10
+ getPluginConfig: registry.getPluginConfig,
10
11
  exportPluginSettings: registry.exportPluginSettings
11
12
  }
@@ -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.1.8",
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": "3.1.8",
20
- "@node-red/util": "3.1.8",
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",