@ralphwetzel/node-red-context-monitor 1.0.0 → 1.1.0

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/README.md CHANGED
@@ -11,7 +11,14 @@ This node allows to setup the reference to a context, then sends a message when
11
11
 
12
12
  It sends a dedicated message on a separate port in case it detects that the value of the context was changed.
13
13
 
14
- The message sent will carry the current value of the context as `msg.payload`. Monitoring details will be provided as `msg.monitoring`.
14
+ The message sent will carry the current value of the context as `msg.payload`, the context key as `msg.topic`.
15
+
16
+ Monitoring details will be provided as `msg.monitoring`:
17
+ * The monitoring setup: `scope` & `key` always, `flow` (id) / `node` (id) if applicable.
18
+ * The id of the node that wrote to the context as `source`.
19
+
20
+ The message sent off the change port carries an additional property in `msg.monitoring`:
21
+ * The value overwritten as `previous`.
15
22
 
16
23
  It is possible to monitor an infinite number of contexts with each instance of this node.
17
24
 
@@ -38,4 +45,38 @@ To monitor a `Node` scope context, set the scope to `Node`, then select flow & n
38
45
  <img alt="node" src="https://raw.githubusercontent.com/ralphwetzel/node-red-context-monitor/main/resources/node.png"
39
46
  style="min-width: 474px; width: 474px; align: center; border: 1px solid lightgray;"/>
40
47
 
41
- > Hint: This node doesn't create a context. It just tries to reference to those already existing. If you're referencing a non-existing context, no harm will happen.
48
+ > Hint: This node doesn't create a context. It just tries to reference to those already existing. If you're referencing a non-existing context, no harm will happen.
49
+
50
+ ### Monitoring objects stored in context
51
+ You may of course define a setup that monitors objects stored in context.
52
+
53
+ If you create a reference to this object (stored in context) and write to its properties, this node issues its messages accordingly.
54
+
55
+ > Disclaimer: Monitoring changes to elements of an `Array` currently is not supported.
56
+
57
+ #### Example:
58
+ Monitoring context definition:
59
+
60
+ <img alt="flow" src="https://raw.githubusercontent.com/ralphwetzel/node-red-context-monitor/main/resources/object_monitor.png"
61
+ style="min-width: 474px; width: 474px; align: center; border: 1px solid lightgray;"/>
62
+
63
+ Code in a `function` node:
64
+
65
+ ``` javascript
66
+ // suppose, test_flow = { prop: "value" }
67
+ let obj = flow.get("test_flow");
68
+ obj.prop = "new";
69
+ ```
70
+
71
+ Message sent by the node:
72
+
73
+ <img alt="flow" src="https://raw.githubusercontent.com/ralphwetzel/node-red-context-monitor/main/resources/object_msg.png"
74
+ style="min-width: 310px; width: 310px; align: center; border: 1px solid lightgray;"/>
75
+
76
+ #### Object property monitoring
77
+ You may define a setup that doesn't monitor the (whole) object, but only one of its properties:
78
+
79
+ <img alt="flow" src="https://raw.githubusercontent.com/ralphwetzel/node-red-context-monitor/main/resources/object_prop.png"
80
+ style="min-width: 474px; width: 474px; align: center; border: 1px solid lightgray;"/>
81
+
82
+ Such a monitor will react _only_, when this property and - if it's an object - its child properties are written to.
package/lib/monitor.js ADDED
@@ -0,0 +1,367 @@
1
+ /*
2
+ node-red-context-monitor by @ralphwetzel
3
+ https://github.com/ralphwetzel/node-red-context-monitor
4
+ License: MIT
5
+ */
6
+
7
+ let RED;
8
+ let monitors;
9
+
10
+ let init = function(_RED, cache) {
11
+ RED = _RED;
12
+ monitors = cache;
13
+ }
14
+
15
+ // *****
16
+ // The function to wrap the NR managed context into our monitoring object
17
+ // This cant' be done w/ a simple new Proxy(context, handler) as Proxy ensures that immutable functions
18
+ // stay immutable - which doesn't support our intentions!
19
+
20
+ // node_id: The node_id as passed to context.get
21
+ // flow_id: The flow_id as passed to context.get
22
+ // ctx: the contet as managed by Node-RED
23
+ // root_id (optional): if global/flow context is re-wrapped, this is the (original) node_id
24
+
25
+ let create_wrapper = function(node_id, flow_id, ctx, root_id) {
26
+
27
+ if (!RED || !monitors) {
28
+ console.log("*** Error while loading node-red-context-monitor:");
29
+ console.log("> Wrapper system not initialized.");
30
+ return ctx;
31
+ }
32
+
33
+ let context = ctx;
34
+
35
+ var context_id = node_id;
36
+ if (flow_id) {
37
+ context_id = node_id + ":" + flow_id;
38
+ }
39
+
40
+ let obj = {};
41
+ let wrapper = new Proxy(obj, {
42
+
43
+ // *** Those 2 are valid only for function objects
44
+
45
+ // apply: function (target, thisArg, argumentsList) {
46
+ // return Reflect.apply(context, thisArg, argumentsList);
47
+ // },
48
+ // construct: function(target, argumentsList, newTarget) {
49
+ // return Reflect.construct(context, argumentsList, newTarget)
50
+ // },
51
+
52
+ // *** 'defineProperty' must reference the wrapper!
53
+
54
+ // defineProperty: function(target, propertyKey, attributes) {
55
+ // return Reflect.defineProperty(context, propertyKey, attributes);
56
+ // },
57
+
58
+ deleteProperty: function(target, propertyKey) {
59
+ return Reflect.deleteProperty(context, propertyKey);
60
+ },
61
+ get: function (target, propertyKey, receiver) {
62
+ if (["set", "get", "keys"].indexOf(propertyKey) > -1) {
63
+ return target[propertyKey];
64
+ } else if (propertyKey == "flow") {
65
+ // create a wrapper for the flow context
66
+ let flow_context = context.flow;
67
+ if (!flow_context) {
68
+ return;
69
+ }
70
+ return create_wrapper(flow_id, undefined, flow_context, node_id);
71
+ } else if (propertyKey == "global") {
72
+ // create a wrapper for global context
73
+ let global_context = context.global;
74
+ if (!global_context) {
75
+ return;
76
+ }
77
+ return create_wrapper('global', undefined, global_context, node_id);
78
+ }
79
+ return Reflect.get(context, propertyKey, receiver);
80
+ },
81
+ getOwnPropertyDescriptor: function (target, propertyKey) {
82
+ return Reflect.getOwnPropertyDescriptor(context, propertyKey);
83
+ },
84
+ getPrototypeOf: function (target){
85
+ Reflect.getPrototypeOf(context);
86
+ },
87
+ has: function (target, propertyKey){
88
+ return Reflect.has(context, propertyKey);
89
+ },
90
+ isExtensible: function (target) {
91
+ return Reflect.isExtensible(context);
92
+ },
93
+ ownKeys: function (target) {
94
+ return Reflect.ownKeys(context);
95
+ },
96
+ preventExtensions: function (target) {
97
+ return Reflect.preventExtensions(context);
98
+ },
99
+ set: function (target, propertyKey, value, receiver) {
100
+ return Reflect.set(context, propertyKey, value, receiver);
101
+ },
102
+ setPrototypeOf: function (target, prototype) {
103
+ return Reflect.setPrototypeOf(context, prototype)
104
+ }
105
+ });
106
+
107
+ Object.defineProperties(wrapper, {
108
+ get: {
109
+ value: function(key, storage, callback) {
110
+
111
+ let create_object_wrapper = function(object, getter_key) {
112
+
113
+ let handler = {
114
+ get: (target, property, receiver) => {
115
+ let getted = Reflect.get(target, property, receiver);
116
+
117
+ // if getted is an object, wrap this (again)
118
+ // to ensure monitoring in case of direct reference access
119
+ if (
120
+ typeof getted === 'object' &&
121
+ !Array.isArray(getted) &&
122
+ getted !== null
123
+ ) {
124
+ let prop_chain = property;
125
+ if (getter_key?.length) {
126
+ prop_chain = getter_key + "." + prop_chain;
127
+ }
128
+ return create_object_wrapper(getted, prop_chain);
129
+ }
130
+ return getted;
131
+ },
132
+ set: function(target, propertyKey, value, receiver) {
133
+ // this is the monitoring function!
134
+ let previous_value = Reflect.get(target, propertyKey, receiver);
135
+ res = Reflect.set(target, propertyKey, value, receiver);
136
+
137
+ let prop_chain = propertyKey;
138
+ if (getter_key?.length) {
139
+ prop_chain = getter_key + "." + prop_chain;
140
+ }
141
+
142
+ trigger(root_id ?? node_id, context_id, key, value, previous_value, prop_chain);
143
+ return res;
144
+ }
145
+ };
146
+
147
+ return new Proxy(object, handler);
148
+
149
+ }
150
+
151
+ let create_array_wrapper = function(object, getter_key) {
152
+
153
+ let handler = {
154
+ get: (target, property, receiver) => {
155
+ let getted = Reflect.get(target, property, receiver);
156
+
157
+ if (
158
+ typeof getted === 'object' &&
159
+ Array.isArray(getted)
160
+ ) {
161
+ let prop_chain = property;
162
+ if (getter_key?.length) {
163
+ prop_chain = getter_key + "." + prop_chain;
164
+ }
165
+ return create_object_wrapper(getted, prop_chain);
166
+ }
167
+ return getted;
168
+ },
169
+ set: function(target, propertyKey, value, receiver) {
170
+ // this is the monitoring function!
171
+ let previous_value = Reflect.get(target, propertyKey, receiver);
172
+ res = Reflect.set(target, propertyKey, value, receiver);
173
+
174
+ let prop_chain = propertyKey;
175
+ if (getter_key?.length) {
176
+ prop_chain = getter_key + "." + prop_chain;
177
+ }
178
+
179
+ trigger(root_id ?? node_id, context_id, key, value, previous_value, prop_chain);
180
+ return res;
181
+ }
182
+ };
183
+
184
+ return new Proxy(object, handler);
185
+
186
+ }
187
+
188
+ let apply_wrapper = function (getted) {
189
+ if (
190
+ typeof getted === 'object' &&
191
+ !Array.isArray(getted) &&
192
+ getted !== null
193
+ ) {
194
+ return create_object_wrapper(getted);
195
+ }
196
+ return getted;
197
+ }
198
+
199
+ if (!callback && typeof storage === 'function') {
200
+ callback = storage;
201
+ storage = undefined;
202
+ }
203
+
204
+ if (callback) {
205
+
206
+ if (typeof callback !== 'function'){
207
+ throw new Error("Callback must be a function");
208
+ }
209
+
210
+ let callback_wrapper = function(err, value) {
211
+ let result = apply_wrapper(value);
212
+ callback(err, result);
213
+ }
214
+
215
+ context.get(key, storage, callback_wrapper);
216
+ return;
217
+ }
218
+
219
+ // be explicit: no (!!) callback here
220
+ let getted_value = context.get(key, storage);
221
+ return apply_wrapper(getted_value);
222
+
223
+ }
224
+ },
225
+ set: {
226
+ value: function(key, value, storage, callback) {
227
+
228
+ // this is the monitoring function!
229
+ let previous_value = context.get(key, storage);
230
+
231
+ if (!callback && typeof storage === 'function') {
232
+ callback = storage;
233
+ storage = undefined;
234
+ }
235
+
236
+ if (callback) {
237
+
238
+ if (typeof callback !== 'function'){
239
+ throw new Error("Callback must be a function");
240
+ }
241
+
242
+ let callback_wrapper = function(err, res) {
243
+ // intercept the callback & report success!
244
+ if (!err) {
245
+ trigger(root_id ?? node_id, context_id, key, value, previous_value);
246
+ }
247
+ return callback(err, res);
248
+ }
249
+
250
+ return context.set(key, value, storage, callback_wrapper);
251
+ }
252
+
253
+ let res = context.set(key, value, storage);
254
+ trigger(root_id ?? node_id, context_id, key, value, previous_value);
255
+ return res;
256
+ }
257
+ },
258
+ keys: {
259
+ value: function(storage, callback) {
260
+ return context.keys(storage, callback);
261
+ }
262
+ }
263
+ });
264
+
265
+ return wrapper;
266
+ }
267
+
268
+ // *****
269
+ // The function to trigger the context-monitoring nodes
270
+
271
+ // root_id: The node.id that wrote to the context
272
+ // context_id: The context (id) that was written to
273
+ // key: The key (in context) that ws written to
274
+ // new_value: The value written
275
+ // previous_value: The value over-written
276
+ // prop_chain: In case the context is a (nested) object, the sequence of property keys.
277
+
278
+ let trigger = function(root_id, context_id, key, new_value, previous_value, prop_chain) {
279
+
280
+ function trigger_receivers(monitoring_nodes, message) {
281
+ monitoring_nodes.forEach(node => {
282
+ let n = RED.nodes.getNode(node.id);
283
+ if (n) {
284
+
285
+ let msg = RED.util.cloneMessage(message);
286
+
287
+ msg.monitoring = {
288
+ "scope": node.data.scope,
289
+ "source": root_id
290
+ }
291
+
292
+ switch (node.data.scope) {
293
+ case "global":
294
+ msg.monitoring.key = node.data.key;
295
+ break;
296
+ case "flow":
297
+ msg.monitoring.flow = node.data.flow;
298
+ msg.monitoring.key = node.data.key;
299
+ break;
300
+ case "node":
301
+ msg.monitoring.flow = node.data.flow;
302
+ msg.monitoring.node = node.data.node;
303
+ msg.monitoring.key = node.data.key;
304
+ break;
305
+ }
306
+
307
+ n.receive(msg);
308
+ }
309
+ });
310
+ }
311
+
312
+ let kc = key;
313
+ if (prop_chain?.length) {
314
+ kc += "." + prop_chain;
315
+ }
316
+
317
+ let keys = [];
318
+ try {
319
+ keys = RED.util.normalisePropertyExpression(kc);
320
+ } catch {}
321
+
322
+ for (let i=keys.length; i>0; i--) {
323
+
324
+ let cidk = context_id + ":" + keys.slice(0,i).join(".");
325
+ let mons = monitors[cidk] ?? [];
326
+
327
+ if (mons.length) {
328
+ let msg = {
329
+ payload: new_value,
330
+ previous: previous_value,
331
+ // do this once already here!
332
+ // ToDo: For non primitives: run a fast comparisor
333
+ changed: new_value != previous_value
334
+ }
335
+
336
+ msg.topic = kc;
337
+
338
+ trigger_receivers(mons, msg);
339
+ }
340
+ }
341
+ }
342
+
343
+ // End: The function ....
344
+ // *****
345
+
346
+ module.exports = {
347
+ init: init,
348
+ create_wrapper: create_wrapper
349
+ }
350
+
351
+ if (process.env.TESTING_CONTEXT_MONITOR) {
352
+
353
+ module.exports["trace"] = function(id) {
354
+
355
+ if (!RED || !monitors) {
356
+ console.log("*** Error while testing node-red-context-monitor:");
357
+ console.log("> Wrapper system not initialized.");
358
+ return;
359
+ }
360
+
361
+ if (id) {
362
+ return monitors[id];
363
+ }
364
+ return monitors;
365
+ }
366
+ }
367
+
package/monitor.html CHANGED
@@ -5,12 +5,47 @@
5
5
  -->
6
6
 
7
7
  <script type="text/javascript">
8
+
9
+ function validate_context_key(key) {
10
+ try {
11
+ var parts = RED.utils.normalisePropertyExpression(key);
12
+
13
+ // If there's a changable part (e.g. test[msg.payload])
14
+ // this will become an array type in parts.
15
+ // Those might be ok for other situtions, but not here!
16
+ for (i=0; i<parts.length; i++) {
17
+ let p = parts[i];
18
+ if (Array.isArray(p)) {
19
+ return false;
20
+ }
21
+ }
22
+ return true;
23
+ } catch (err) {
24
+ return false;
25
+ }
26
+ }
27
+
8
28
  RED.nodes.registerType('context-monitor',{
9
29
  category: 'input',
10
30
  color: '#b0b0b0',
11
31
  defaults: {
12
32
  name: {value:""},
13
- monitoring: {value:[]}
33
+ monitoring: {
34
+ value: [],
35
+ validate: function(scopes, opt) {
36
+ let msg;
37
+ if (!scopes || scopes.length === 0) { return true }
38
+ for (let i=0; i<scopes.length; i++) {
39
+ let scope = scopes[i];
40
+ if (!validate_context_key(scope.key)) {
41
+ return RED._("node-red:change.errors.invalid-prop", {
42
+ property: scope.key
43
+ });
44
+ }
45
+ }
46
+ return true;
47
+ }
48
+ }
14
49
  },
15
50
  inputs: 0,
16
51
  outputs: 2,
@@ -41,7 +76,8 @@
41
76
  function add_context(tpl) {
42
77
  let c = {
43
78
  "scope": tpl.scope ?? "global",
44
- "flow": tpl.flow,
79
+ // decode "this flow" marker
80
+ "flow": ("." === tpl.flow) ? node.z : tpl.flow,
45
81
  "node": tpl.node,
46
82
  "key": tpl.key ?? ""
47
83
  }
@@ -157,8 +193,8 @@
157
193
  });
158
194
 
159
195
  let flow_opts = [];
160
- let group_opts = [];
161
- let node_opts = [];
196
+ // let group_opts = [];
197
+ let node_opts;
162
198
 
163
199
  RED.nodes.eachWorkspace( cb => {
164
200
  flow_opts.push({
@@ -194,7 +230,8 @@
194
230
  // if ($(`#context-scope-${index}`).typedInput("value") == "group") {
195
231
  // $(`#context-scope-key-${index}`).prop("disabled", gol < 1);
196
232
  // }
197
-
233
+
234
+ node_opts = [];
198
235
  RED.nodes.eachNode( n => {
199
236
 
200
237
  if (n.z == value) {
@@ -271,6 +308,11 @@
271
308
  data.key = $(this).val();
272
309
  node.dirty = true;
273
310
  }
311
+ $(this).toggleClass("input-error", !validate_context_key($(this).val()));
312
+ });
313
+
314
+ $(`#context-scope-key-${index}`).on( "input", function () {
315
+ $(this).toggleClass("input-error", !validate_context_key($(this).val()));
274
316
  });
275
317
 
276
318
  // initialize the form
@@ -303,7 +345,7 @@
303
345
  }
304
346
  }
305
347
 
306
- $(`#context-scope-key-${index}`).val(data.key);
348
+ $(`#context-scope-key-${index}`).val(data.key).toggleClass("input-error", !validate_context_key(data.key));
307
349
 
308
350
  _initing = false;
309
351
  },
@@ -379,8 +421,12 @@
379
421
  // break;
380
422
  case "node":
381
423
  delete data.group;
382
- }
424
+ }
383
425
 
426
+ if (data.flow == node.z) {
427
+ // set a special marker that 'this flow' shall be referenced
428
+ data.flow = ".";
429
+ }
384
430
  })
385
431
 
386
432
  node.monitoring = ctx;
package/monitor.js CHANGED
@@ -5,270 +5,100 @@
5
5
  */
6
6
 
7
7
  const fs = require('fs-extra');
8
- const os = require("os");
9
8
  const path = require("path");
10
-
11
- let error_header = "*** Error while loading node-red-context-monitor:";
9
+ const monitor = require('./lib/monitor.js');
12
10
 
13
11
  // this used to be the cache of ctx triggered @ set
14
12
  // ... that's why it's the set_cache!
15
13
  let set_cache = {};
16
14
 
17
- let _RED;
15
+ // *****
16
+ // Patch support: Scan the require database for the path to a to-be-required file
18
17
 
19
- // ****
20
- // Copied from node-red-mcu-plugin:
21
- // Patch support function: Calculate the path to a to-be-required file
18
+ let require_cache = {
19
+ ".": require.main
20
+ }
22
21
 
23
- function get_require_path(req_path) {
22
+ function scan_for_require_path(req_path) {
24
23
 
25
- let rm = require.main.path;
24
+ let found;
26
25
 
27
- try {
28
- let stat = fs.lstatSync(rm);
29
- if (!stat.isDirectory()) {
30
- console.log(error_header);
31
- console.log("require.main.path is not a directory.");
32
- return;
26
+ if (process.env.NODE_RED_HOME) {
27
+ found = path.join(process.env.NODE_RED_HOME, "..", req_path);
28
+ console.log("@f", found);
29
+ if (fs.existsSync(found)) {
30
+ return found;
33
31
  }
34
- } catch (err) {
35
- console.log(err);
36
- console.log(error_header);
37
- if (err.code == 'ENOENT') {
38
- console.log("require.main.path not found.");
39
- } else {
40
- console.log("Error while handling require.main.path.")
41
- }
42
- return null;
43
32
  }
44
33
 
45
- // split path into segments ... the safe way
46
- rm = path.normalize(rm);
47
- let rms = []
48
- let rmp;
49
- do {
50
- rmp = path.parse(rm);
51
- if (rmp.base.length > 0) {
52
- rms.unshift(rmp.base);
53
- rm = rmp.dir;
54
- }
55
- } while (rmp.base.length > 0)
34
+ let runner = 0;
56
35
 
57
- let rmsl = rms.length;
36
+ while (runner < Object.keys(require_cache).length) {
58
37
 
59
- if (rms.includes("packages")) {
60
- if (rms[rmsl-3]=="packages" && rms[rmsl-2]=="node_modules" && rms[rmsl-1]=="node-red") {
61
- // dev: [...]/node-red/packages/node_modules/node-red
62
- // install: [...]/lib/node_modules/node-red
63
- // pi: /lib/node_modules/node-red/
38
+ let key = Object.keys(require_cache)[runner];
39
+ let entry = require_cache[key];
64
40
 
65
- // dev: [...]/node-red/packages/node_modules/@node-red
66
- // install: [...]/lib/node_modules/node-red/node_modules/@node-red
67
- // pi: /lib/node_modules/node-red/node_modules/@node-red
68
- rms.splice(-2);
41
+ if (entry.id?.includes(req_path)) {
42
+ found = entry.id;
43
+ break;
69
44
  }
45
+
46
+ let cc = entry.children;
47
+ cc.forEach(c => {
48
+ if (!(c.id in require_cache)) {
49
+ require_cache[c.id] = c;
50
+ }
51
+ });
52
+
53
+ runner += 1;
70
54
  }
71
55
 
72
- // compose things again...
73
- req_path = req_path.split("/");
74
- let p = path.join(rmp.root, ...rms, ...req_path);
75
-
76
- if (!fs.existsSync(p)) {
77
- console.log(error_header)
78
- console.log("Failed to calculate correct patch path.");
79
- console.log("Please raise an issue @ our GitHub repository, stating the following information:");
80
- console.log("> require.main.path:", require.main.path);
81
- console.log("> utils.js:", p);
82
- return null;
56
+ if (found) {
57
+ if (!fs.existsSync(found)) {
58
+ console.log("*** Error while loading node-red-context-monitor:")
59
+ console.log("Failed to calculate path to required file.");
60
+ console.log("Please raise an issue @ our GitHub repository, stating the following information:");
61
+ console.log("> scanned for:", req_path);
62
+ console.log("> found:", found);
63
+ return;
64
+ }
83
65
  }
84
66
 
85
- return p;
67
+ console.log(found);
68
+ return found;
86
69
  }
87
70
 
88
71
  // End: "Patch support ..."
89
72
  // *****
90
73
 
91
74
  // *****
92
- // Make available the Context Manager
75
+ // Make available & patch the Context Manager
93
76
 
94
- const context_manager_path = get_require_path("node_modules/@node-red/runtime/lib/nodes/context/index.js");
95
- if (!context_manager_path) return;
96
- const context_manager = require(context_manager_path);
77
+ let context_manager;
97
78
 
98
- // The function to wrap the NR managed context into our monitoring object
99
- // This cant' be done w/ a simple new Proxy(context, handler) as Proxy ensures that immutable functions
100
- // stay immutable - which doesn't support our intentions!
79
+ // When running a test, NODE_RED_HOME is not defined.
80
+ if (process.env.NODE_RED_HOME && !process.env.TESTING_CONTEXT_MONITOR) {
101
81
 
102
- let create_wrapper = function(node_id, flow_id, ctx) {
82
+ let context_manager_path = scan_for_require_path("@node-red/runtime/lib/nodes/context");
83
+ if (context_manager_path) {
103
84
 
104
- let context = ctx;
85
+ context_manager = require(context_manager_path);
105
86
 
106
- var context_id = node_id;
107
- if (flow_id) {
108
- context_id = node_id + ":" + flow_id;
109
- }
110
-
111
- let obj = {};
112
- let wrapper = new Proxy(obj, {
113
-
114
- // *** Those 2 are valid only for function objects
115
-
116
- // apply: function (target, thisArg, argumentsList) {
117
- // return Reflect.apply(context, thisArg, argumentsList);
118
- // },
119
- // construct: function(target, argumentsList, newTarget) {
120
- // return Reflect.construct(context, argumentsList, newTarget)
121
- // },
122
-
123
- // *** 'defineProperty' must reference the wrapper!
124
-
125
- // defineProperty: function(target, propertyKey, attributes) {
126
- // return Reflect.defineProperty(context, propertyKey, attributes);
127
- // },
128
-
129
- deleteProperty: function(target, propertyKey) {
130
- return Reflect.deleteProperty(context, propertyKey);
131
- },
132
- get: function (target, propertyKey, receiver) {
133
- if (["set", "get", "keys"].indexOf(propertyKey) > -1) {
134
- return target[propertyKey];
135
- } else if (propertyKey == "flow") {
136
- // create a wrapper for the flow context
137
- let flow_context = context.flow;
138
- if (!flow_context) {
139
- return;
140
- }
141
- return create_wrapper(flow_id, undefined, flow_context);
142
- } else if (propertyKey == "global") {
143
- // create a wrapper for global context
144
- let global_context = context.global;
145
- if (!global_context) {
146
- return;
147
- }
148
- return create_wrapper('global', undefined, global_context);
149
- }
150
- return Reflect.get(context, propertyKey, receiver);
151
- },
152
- getOwnPropertyDescriptor: function (target, propertyKey) {
153
- return Reflect.getOwnPropertyDescriptor(context, propertyKey);
154
- },
155
- getPrototypeOf: function (target){
156
- Reflect.getPrototypeOf(context);
157
- },
158
- has: function (target, propertyKey){
159
- return Reflect.has(context, propertyKey);
160
- },
161
- isExtensible: function (target) {
162
- return Reflect.isExtensible(context);
163
- },
164
- ownKeys: function (target) {
165
- return Reflect.ownKeys(context);
166
- },
167
- preventExtensions: function (target) {
168
- return Reflect.preventExtensions(context);
169
- },
170
- set: function (target, propertyKey, value, receiver) {
171
- return Reflect.set(context, propertyKey, value, receiver);
172
- },
173
- setPrototypeOf: function (target, prototype) {
174
- return Reflect.setPrototypeOf(context, prototype)
87
+ // patching into getContext (exported as 'get')
88
+ const orig_context_get = context_manager.get;
89
+ context_manager.get = function(nodeId, flowId) {
90
+ let context = orig_context_get(nodeId, flowId);
91
+ return monitor.create_wrapper(nodeId, flowId, context);
175
92
  }
176
- });
177
-
178
- Object.defineProperties(wrapper, {
179
- get: {
180
- value: function(key, storage, callback) {
181
- return context.get(key, storage, callback);
182
- }
183
- },
184
- set: {
185
- value: function(key, value, storage, callback) {
186
- // this is the monitoring function!
187
- let previous_value = context.get(key, storage);
188
- let res = context.set(key, value, storage, callback);
189
- trigger(context_id + ":" + key, value, previous_value);
190
- return res;
191
- }
192
- },
193
- keys: {
194
- value: function(storage, callback) {
195
- return context.keys(storage, callback);
196
- }
93
+
94
+ // patching into getFlowContext
95
+ const orig_get_flow_context = context_manager.getFlowContext;
96
+ context_manager.getFlowContext = function(flowId, parentFlowId) {
97
+ let flow_context = orig_get_flow_context(flowId, parentFlowId);
98
+ return monitor.create_wrapper(flowId, undefined, flow_context);
197
99
  }
198
- });
199
-
200
- return wrapper;
201
- }
202
-
203
- // *****
204
- // The function to trigger the context-monitoring nodes
205
-
206
- let trigger = function(context_key_id, new_value, previous_value) {
207
-
208
- function trigger_receivers(cache, message) {
209
- cache.forEach(node => {
210
- let n = _RED.nodes.getNode(node.id);
211
- if (n) {
212
-
213
- let msg = _RED.util.cloneMessage(message);
214
-
215
- msg.topic = node.data.key;
216
- msg.monitoring = {
217
- "scope": node.data.scope,
218
- }
219
-
220
- switch (node.data.scope) {
221
- case "global":
222
- msg.monitoring.key = node.data.key;
223
- break;
224
- case "flow":
225
- msg.monitoring.flow = node.data.flow;
226
- msg.monitoring.key = node.data.key;
227
- break;
228
- case "node":
229
- msg.monitoring.flow = node.data.flow;
230
- msg.monitoring.node = node.data.node;
231
- msg.monitoring.key = node.data.key;
232
- break;
233
- }
234
-
235
- n.receive(msg);
236
- }
237
- });
238
- }
239
-
240
- let cache = set_cache[context_key_id] ?? [];
241
- let msg = {
242
- payload: new_value,
243
- previous: previous_value
100
+
244
101
  }
245
- trigger_receivers(cache, msg);
246
-
247
- // if (new_value !== previous_value) {
248
- // let cache = change_cache[context_key_id] ?? [];
249
- // let msg = {
250
- // payload: new_value,
251
- // previous: previous_value
252
- // }
253
- // trigger_receivers(cache, msg);
254
- // }
255
- }
256
-
257
- // End: The function ....
258
- // *****
259
-
260
- // patching into getContext (exported as 'get')
261
- const orig_context_get = context_manager.get;
262
- context_manager.get = function(nodeId, flowId) {
263
- let context = orig_context_get(nodeId, flowId);
264
- return create_wrapper(nodeId, flowId, context);
265
- }
266
-
267
- // patching into getFlowContext
268
- const orig_get_flow_context = context_manager.getFlowContext;
269
- context_manager.getFlowContext = function(flowId, parentFlowId) {
270
- let flow_context = orig_get_flow_context(flowId, parentFlowId);
271
- return create_wrapper(flowId, undefined, flow_context);
272
102
  }
273
103
 
274
104
  // End: "Make available ..."
@@ -279,31 +109,53 @@ context_manager.getFlowContext = function(flowId, parentFlowId) {
279
109
 
280
110
  module.exports = function(RED) {
281
111
 
282
- // catch this here!
283
- // necessary for trigger function to map node_id -> node object.
284
- _RED = RED;
112
+ // Catch RED here & provide it to the monitor!
113
+ // It's necessary for trigger function to map node_id -> node object.
114
+ monitor.init(RED, set_cache);
285
115
 
286
116
  function ContextMonitor(config) {
287
117
  RED.nodes.createNode(this,config);
288
118
  var node = this;
289
119
 
290
- node.data = config.monitoring;
120
+ let scopes = config.monitoring ?? [];
121
+ node.data = scopes;
122
+
291
123
  node.monitoring = [];
292
124
 
293
- config.monitoring.forEach( data => {
125
+ scopes.forEach( data => {
294
126
 
295
127
  if (!data.key) return;
296
128
 
129
+ // support for complex keys
130
+ // test["mm"].value becomes test.mm.value
131
+ let key = data.key;
132
+
133
+ try {
134
+ let key_parts = RED.util.normalisePropertyExpression(key);
135
+ for (i=0; i<key_parts.length; i++) {
136
+ if (Array.isArray(key_parts[i])) {
137
+ return;
138
+ }
139
+ }
140
+ key = key_parts.join('.');
141
+ } catch {
142
+ return;
143
+ }
144
+
145
+ if ("." === data.flow) {
146
+ data.flow = node.z;
147
+ }
148
+
297
149
  let ctx = "global";
298
150
  switch (data.scope) {
299
151
  case "global":
300
- ctx = `global:${data.key}`;
152
+ ctx = `global:${key}`;
301
153
  break;
302
154
  case "flow":
303
- ctx = `${data.flow}:${data.key}`;
155
+ ctx = `${data.flow}:${key}`;
304
156
  break;
305
157
  case "node":
306
- ctx = `${data.node}:${data.flow}:${data.key}`;
158
+ ctx = `${data.node}:${data.flow}:${key}`;
307
159
  break;
308
160
  default:
309
161
  return;
@@ -325,9 +177,11 @@ module.exports = function(RED) {
325
177
 
326
178
  // unfold & check if changed
327
179
  let prev = msg.previous;
180
+ let changed = msg.changed;
328
181
  delete msg.previous;
182
+ delete msg.changed;
329
183
 
330
- if (msg.payload !== prev) {
184
+ if (changed) {
331
185
  // if changed, clone & emit @ second output terminal
332
186
  let m = RED.util.cloneMessage(msg);
333
187
  delete m._msgid;
@@ -343,12 +197,18 @@ module.exports = function(RED) {
343
197
  node.on("close",function() {
344
198
  // remove this nodes ctx(s) from the trigger list
345
199
  node.monitoring.forEach( ctx => {
346
- set_cache[ctx] = set_cache[ctx].filter( n => {
347
- return n.id !== node.id;
348
- })
200
+ let sc = set_cache[ctx];
201
+ if (sc) {
202
+ set_cache[ctx] = sc.filter( n => {
203
+ return n.id !== node.id;
204
+ })
205
+ if (set_cache[ctx].length < 1) {
206
+ delete set_cache[ctx];
207
+ }
208
+ }
349
209
  })
350
210
  });
351
211
  }
352
212
 
353
213
  RED.nodes.registerType("context-monitor",ContextMonitor);
354
- }
214
+ }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@ralphwetzel/node-red-context-monitor",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "A Node-RED node to monitor a context.",
5
5
  "main": "monitor.js",
6
6
  "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
7
+ "test": "mocha \"test/**/*_spec.js\""
8
8
  },
9
9
  "repository": {
10
10
  "type": "git",
@@ -27,9 +27,23 @@
27
27
  },
28
28
  "homepage": "https://github.com/ralphwetzel/node-red-context-monitor#readme",
29
29
  "dependencies": {
30
- "fs-extra": "^11.1.1"
30
+ "fs-extra": "^11.1.1",
31
+ "node-red": "^3.1.0"
31
32
  },
32
33
  "engines": {
33
- "node": ">=16.0.0"
34
- }
34
+ "node": ">=14.0.0"
35
+ },
36
+ "devDependencies": {
37
+ "mocha": "^10.2.0",
38
+ "node-red-node-test-helper": "^0.3.2"
39
+ },
40
+ "files": [
41
+ "/examples",
42
+ "/lib",
43
+ "/resources",
44
+ "LICENSE",
45
+ "monitor.*",
46
+ "package.json",
47
+ "README.md"
48
+ ]
35
49
  }
Binary file
Binary file
Binary file
@@ -1,33 +0,0 @@
1
- # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2
- # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages
3
-
4
- name: "NPM Publish"
5
-
6
- on:
7
- release:
8
- types: [created]
9
-
10
- jobs:
11
- build:
12
- runs-on: ubuntu-latest
13
- steps:
14
- - uses: actions/checkout@v3
15
- - uses: actions/setup-node@v3
16
- with:
17
- node-version: 16
18
- # - run: npm ci
19
- # - run: npm test
20
-
21
- publish-npm:
22
- needs: build
23
- runs-on: ubuntu-latest
24
- steps:
25
- - uses: actions/checkout@v3
26
- - uses: actions/setup-node@v3
27
- with:
28
- node-version: 16
29
- registry-url: https://registry.npmjs.org/
30
- # - run: npm ci
31
- - run: npm publish --access public
32
- env:
33
- NODE_AUTH_TOKEN: ${{secrets.npm_access_token}}