@rosepetal/node-red-contrib-utils 1.1.1

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.
@@ -0,0 +1,100 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('rp-promise-reader', {
3
+ category: 'RP Utils',
4
+ color: '#a6bbcf',
5
+ defaults: {
6
+ name: { value: "rp-promise-reader", validate: function(v) { return v.length > 0; }, required: false },
7
+ fieldToRead: { value: "promises", validate: function(v) { return v !== ""; }, required: true }
8
+ },
9
+ inputs: 1,
10
+ outputs: 1,
11
+ icon: 'font-awesome/fa-clock-o',
12
+ label: function() {
13
+ return this.name || "promise reader";
14
+ },
15
+
16
+ oneditprepare: function() {
17
+ $('#node-input-fieldToRead').typedInput({
18
+ type: 'msg',
19
+ types: ['msg']
20
+ });
21
+ }
22
+
23
+ });
24
+ </script>
25
+
26
+ <script type="text/html" data-template-name="rp-promise-reader">
27
+ <div class="form-row">
28
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
29
+ <input type="text" id="node-input-name" placeholder="Name">
30
+ </div>
31
+
32
+ <div class="form-row">
33
+ <label for="node-input-fieldToRead"><i class="fa fa-folder"></i> Field to read</label>
34
+ <input type="text" id="node-input-fieldToRead" placeholder="e.g., post.async">
35
+ </div>
36
+ </script>
37
+
38
+
39
+
40
+ <script type="text/html" data-help-name="rp-promise-reader">
41
+ <p><strong>The Promise Reader node</strong> resolves arrays of promises and aggregates their results into the message object. This node is essential for handling asynchronous operations from nodes like the inferencer, which use promise-based processing for concurrent inference requests.</p>
42
+
43
+ <h3>Purpose</h3>
44
+ <p>When nodes perform asynchronous operations (like sending inference requests to multiple servers), they return promises instead of immediate results. The Promise Reader collects these promises, waits for all to complete using <code>Promise.all()</code>, then merges the resolved data back into the message flow.</p>
45
+
46
+ <h3>Parameters</h3>
47
+ <ul>
48
+ <li><strong>Name:</strong> Display name for the node instance.</li>
49
+ <li><strong><a id="promise-field-param">Field to read</a>:</strong> Message field path containing the promise array. Defaults to <code>promises</code> (reads from <code>msg.promises</code>). Can use dot notation for nested fields like <code>inference.promises</code>.</li>
50
+ </ul>
51
+
52
+ <h3>Input Requirements</h3>
53
+ <p>The specified message field must contain an <strong>array</strong> of valid JavaScript promises</li>
54
+
55
+ <h3>Processing Behavior</h3>
56
+ <ol>
57
+ <li><strong>Promise Collection:</strong> Extracts promise array from specified message field</li>
58
+ <li><strong>Parallel Resolution:</strong> Uses <code>Promise.all()</code> to wait for all promises simultaneously</li>
59
+ <li><strong>Result Aggregation:</strong> Merges resolved data into the message object using <code>Object.assign()</code></li>
60
+ <li><strong>Performance Tracking:</strong> Aggregates timing information from all resolved promises</li>
61
+ <li><strong>Cleanup:</strong> Removes the original promise array from the message</li>
62
+ </ol>
63
+
64
+ <h3>Output Format</h3>
65
+ <p>Resolved promise data is merged directly into the message object:</p>
66
+ <pre>{
67
+ // Original message fields preserved
68
+ payload: {...},
69
+
70
+ // Merged results from resolved promises
71
+ detections: [...], // Inference results
72
+ confidence: 0.95, // Aggregated confidence
73
+
74
+ // Performance metrics
75
+ performance: {
76
+ "rp-promise-reader": {
77
+ startTime: 1640995200000,
78
+ endTime: 1640995201500,
79
+ milliseconds: 1500
80
+ }
81
+ }
82
+ // Original promises array removed
83
+ }</pre>
84
+
85
+ <h3>Integration Examples</h3>
86
+ <ul>
87
+ <li><strong>After Inferencer:</strong> Use default field <code>promises</code> to resolve inference results from multiple concurrent servers</li>
88
+ <li><strong>Custom Async Nodes:</strong> Specify custom field paths like <code>custom.asyncResults</code> for specialized use cases</li>
89
+ </ul>
90
+
91
+ <h3>Error Handling</h3>
92
+ <ul>
93
+ <li><strong>Invalid Input:</strong> Warns if field doesn't contain an array and passes message unchanged</li>
94
+ <li><strong>Non-Promise Elements:</strong> Errors if array contains non-promise objects</li>
95
+ <li><strong>Promise Rejection:</strong> Logs warnings for rejected promises but continues processing others</li>
96
+ <li><strong>Missing Field:</strong> Warns and passes message unchanged if specified field doesn't exist</li>
97
+ </ul>
98
+
99
+ <p><strong>Performance Tip:</strong> This node waits for the slowest promise to complete. For optimal performance, ensure all upstream async operations have appropriate timeouts.</p>
100
+ </script>
@@ -0,0 +1,118 @@
1
+ module.exports = function(RED) {
2
+ function PromiseReaderNode(config) {
3
+ RED.nodes.createNode(this, config);
4
+ var node = this;
5
+
6
+ function deleteNestedProperty(obj, path) {
7
+ const parts = path.split('.');
8
+ const last = parts.pop();
9
+ const ref = parts.reduce((acc, part) => acc && acc[part], obj);
10
+
11
+ if (ref && last in ref) {
12
+ delete ref[last];
13
+ }
14
+ }
15
+
16
+ function deepMerge(target, source) {
17
+ const result = { ...target };
18
+ for (const key in source) {
19
+ if (source.hasOwnProperty(key)) {
20
+ if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key]) &&
21
+ typeof result[key] === 'object' && result[key] !== null && !Array.isArray(result[key])) {
22
+ result[key] = deepMerge(result[key], source[key]);
23
+ } else {
24
+ result[key] = source[key];
25
+ }
26
+ }
27
+ }
28
+ return result;
29
+ }
30
+
31
+ function extractPerformanceEntries(obj, entries = []) {
32
+ if (!obj || typeof obj !== 'object') return entries;
33
+
34
+ if (obj.startTime && obj.endTime) {
35
+ entries.push(obj);
36
+ } else {
37
+ for (const key in obj) {
38
+ if (obj.hasOwnProperty(key)) {
39
+ extractPerformanceEntries(obj[key], entries);
40
+ }
41
+ }
42
+ }
43
+ return entries;
44
+ }
45
+
46
+ node.on('input', function(msg) {
47
+ const promisesPath = config.fieldToRead || 'promises';
48
+ const fieldPath = promisesPath.split('.');
49
+ const promises = fieldPath.reduce((obj, key) => obj && obj[key], msg);
50
+
51
+ if (!promises || !Array.isArray(promises)) {
52
+ node.warn(`msg.${promisesPath} must be an array of promises`);
53
+ node.send(msg)
54
+ return;
55
+ }
56
+ const allArePromises = promises.every(promise =>
57
+ promise instanceof Promise ||
58
+ (promise && typeof promise.then === 'function' && typeof promise.catch === 'function')
59
+ );
60
+ if (!allArePromises) {
61
+ node.error(`All elements in msg.${promisesPath} must be promises`);
62
+ return;
63
+ }
64
+
65
+ let aggregatedPerformance = {
66
+ startTime: null,
67
+ endTime: null
68
+ };
69
+
70
+ Promise.all(promises)
71
+ .then(resolvedValues => {
72
+ resolvedValues.forEach(value => {
73
+ if (value && value.performance) {
74
+ const performanceEntries = extractPerformanceEntries(value.performance);
75
+ performanceEntries.forEach(entry => {
76
+ if (aggregatedPerformance.startTime === null || entry.startTime < aggregatedPerformance.startTime) {
77
+ aggregatedPerformance.startTime = entry.startTime;
78
+ }
79
+ if (aggregatedPerformance.endTime === null || entry.endTime > aggregatedPerformance.endTime) {
80
+ aggregatedPerformance.endTime = entry.endTime;
81
+ }
82
+ });
83
+ }
84
+
85
+ if (value && typeof value === 'object') {
86
+ Object.keys(value).forEach(key => {
87
+ if (msg[key] === undefined) {
88
+ msg[key] = value[key];
89
+ } else if (typeof msg[key] === 'object' && msg[key] !== null && !Array.isArray(msg[key]) &&
90
+ typeof value[key] === 'object' && value[key] !== null && !Array.isArray(value[key])) {
91
+ msg[key] = deepMerge(msg[key], value[key]);
92
+ } else {
93
+ msg[key] = value[key];
94
+ }
95
+ });
96
+ }
97
+ });
98
+
99
+ if (aggregatedPerformance.startTime !== null && aggregatedPerformance.endTime !== null) {
100
+ aggregatedPerformance.milliseconds = new Date(aggregatedPerformance.endTime) - new Date(aggregatedPerformance.startTime);
101
+ } else {
102
+ aggregatedPerformance.milliseconds = null;
103
+ }
104
+
105
+ msg.performance = msg.performance || {};
106
+ msg.performance[node.name] = aggregatedPerformance;
107
+
108
+ deleteNestedProperty(msg, promisesPath);
109
+
110
+ node.send(msg);
111
+ })
112
+ .catch(e => node.warn(e));
113
+ });
114
+ }
115
+
116
+
117
+ RED.nodes.registerType("rp-promise-reader", PromiseReaderNode);
118
+ };
@@ -0,0 +1,85 @@
1
+ <script type="text/javascript">
2
+ (function() {
3
+ RED.nodes.registerType('rp-block-detect', {
4
+ category: 'RP Utils',
5
+ color: '#F4C95D',
6
+ defaults: {
7
+ name: { value: "" },
8
+ intervalMs: { value: 1000, required: true, validate: RED.validators.number() },
9
+ thresholdMs: { value: 120, required: true, validate: RED.validators.number() },
10
+ consecutive: { value: 1, required: true, validate: RED.validators.number() },
11
+ cooldown: { value: 30000, required: true, validate: RED.validators.number() },
12
+ showStats: { value: true }
13
+ },
14
+ inputs: 0,
15
+ outputs: 0,
16
+ icon: "font-awesome/fa-shield",
17
+ label: function() {
18
+ return this.name || "block detect";
19
+ },
20
+ oneditprepare: function() {
21
+ $("#node-input-showStats").prop("checked", this.showStats !== false);
22
+ },
23
+ oneditsave: function() {
24
+ this.showStats = $("#node-input-showStats").is(":checked");
25
+ }
26
+ });
27
+ })();
28
+ </script>
29
+
30
+ <script type="text/x-red" data-template-name="rp-block-detect">
31
+ <div class="form-row">
32
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
33
+ <input type="text" id="node-input-name" placeholder="Event loop watchdog">
34
+ </div>
35
+
36
+ <div class="form-row">
37
+ <label for="node-input-thresholdMs"><i class="fa fa-bolt"></i> Threshold (ms)</label>
38
+ <input type="number" id="node-input-thresholdMs" min="1" step="10">
39
+ </div>
40
+
41
+ <div class="form-row">
42
+ <label for="node-input-intervalMs"><i class="fa fa-clock-o"></i> Sample interval (ms)</label>
43
+ <input type="number" id="node-input-intervalMs" min="100" step="100">
44
+ </div>
45
+
46
+ <div class="form-row">
47
+ <label for="node-input-consecutive"><i class="fa fa-repeat"></i> Consecutive hits</label>
48
+ <input type="number" id="node-input-consecutive" min="1" step="1">
49
+ </div>
50
+
51
+ <div class="form-row">
52
+ <label for="node-input-cooldown"><i class="fa fa-hourglass-half"></i> Cooldown (ms)</label>
53
+ <input type="number" id="node-input-cooldown" min="1000" step="1000">
54
+ </div>
55
+
56
+ <div class="form-row">
57
+ <label></label>
58
+ <input type="checkbox" id="node-input-showStats" checked>
59
+ <label for="node-input-showStats" style="width: auto;">Show live delay metrics</label>
60
+ </div>
61
+ </script>
62
+
63
+ <script type="text/x-red" data-help-name="rp-block-detect">
64
+ <p>Continuously monitors Node-RED's event loop for blocking work that would slow down flows.</p>
65
+
66
+ <h3>How it works</h3>
67
+ <p>The node samples <code>monitorEventLoopDelay</code> (or falls back to timer drift) and raises an alert when the measured delay exceeds the configured threshold for a number of consecutive samples.</p>
68
+
69
+ <h3>Properties</h3>
70
+ <dl class="message-properties">
71
+ <dt>Threshold (ms)<span class="property-type">number</span></dt>
72
+ <dd>Maximum allowed event loop delay before the node treats a sample as blocking.</dd>
73
+ <dt>Sample interval (ms)<span class="property-type">number</span></dt>
74
+ <dd>How often to collect a measurement. Lower values catch spikes sooner but cost a little more CPU.</dd>
75
+ <dt>Consecutive hits<span class="property-type">number</span></dt>
76
+ <dd>Number of back-to-back samples that must exceed the threshold before an alert is emitted.</dd>
77
+ <dt>Cooldown (ms)<span class="property-type">number</span></dt>
78
+ <dd>Minimum time between alerts so your logs do not flood when something stays blocked.</dd>
79
+ <dt>Show live delay metrics<span class="property-type">boolean</span></dt>
80
+ <dd>Toggle live status text that displays the current average and max delay.</dd>
81
+ </dl>
82
+
83
+ <h3>Usage</h3>
84
+ <p>Drop the node anywhere in a flow (it has no wires) to keep a lightweight background watchdog running. Use multiple instances with different thresholds if you want per-flow granularity.</p>
85
+ </script>
@@ -0,0 +1,136 @@
1
+ /**
2
+ * @file Node-RED logic for the block-detect node.
3
+ * Monitors the Node.js event loop delay to detect blocking work.
4
+ */
5
+ const { performance, monitorEventLoopDelay } = require('perf_hooks');
6
+
7
+ module.exports = function(RED) {
8
+ function BlockDetectNode(config) {
9
+ RED.nodes.createNode(this, config);
10
+ const node = this;
11
+
12
+ const intervalMs = sanitizeNumber(config.intervalMs, 1000, 100);
13
+ const thresholdMs = sanitizeNumber(config.thresholdMs, 120, 1);
14
+ const resolutionMs = 20; // Medium resolution for event-loop histogram (ms)
15
+ const consecutive = Math.max(1, Math.round(sanitizeNumber(config.consecutive, 1, 1)));
16
+ const cooldownMs = sanitizeNumber(config.cooldown, 30000, 1000);
17
+ const showStats = config.showStats !== false;
18
+
19
+ let overThresholdCount = 0;
20
+ let lastAlertAt = 0;
21
+ let timer = null;
22
+ let histogram = null;
23
+ let fallbackPrev = performance.now();
24
+ let monitorAvailable = typeof monitorEventLoopDelay === 'function';
25
+
26
+ if (monitorAvailable) {
27
+ histogram = monitorEventLoopDelay({ resolution: resolutionMs });
28
+ histogram.enable();
29
+ } else {
30
+ node.warn('monitorEventLoopDelay not available; falling back to coarse timer drift detection.');
31
+ }
32
+
33
+ node.status({ fill: 'grey', shape: 'ring', text: 'monitoring…' });
34
+
35
+ timer = setInterval(() => {
36
+ const stats = monitorAvailable
37
+ ? readHistogramStats(histogram)
38
+ : readFallbackStats();
39
+
40
+ if (showStats) {
41
+ const color = stats.max >= thresholdMs ? (stats.alert ? 'red' : 'yellow') : 'green';
42
+ const shape = stats.max >= thresholdMs ? 'ring' : 'dot';
43
+ node.status({
44
+ fill: color,
45
+ shape,
46
+ text: `avg ${stats.mean.toFixed(1)}ms | max ${stats.max.toFixed(1)}ms`
47
+ });
48
+ } else {
49
+ if (stats.alert) {
50
+ node.status({ fill: 'red', shape: 'dot', text: 'blocking detected' });
51
+ } else if (stats.max >= thresholdMs) {
52
+ node.status({ fill: 'yellow', shape: 'ring', text: 'high loop delay' });
53
+ } else {
54
+ node.status({ fill: 'green', shape: 'dot', text: 'ok' });
55
+ }
56
+ }
57
+
58
+ if (stats.max >= thresholdMs) {
59
+ overThresholdCount += 1;
60
+ } else {
61
+ overThresholdCount = 0;
62
+ }
63
+
64
+ if (overThresholdCount >= consecutive) {
65
+ const now = Date.now();
66
+ if (now - lastAlertAt >= cooldownMs) {
67
+ emitAlert(stats);
68
+ lastAlertAt = now;
69
+ }
70
+ overThresholdCount = 0;
71
+ }
72
+ }, intervalMs);
73
+
74
+ node.on('close', function() {
75
+ if (timer) {
76
+ clearInterval(timer);
77
+ }
78
+ if (histogram) {
79
+ histogram.disable();
80
+ }
81
+ });
82
+
83
+ function emitAlert(stats) {
84
+ const message =
85
+ `Detected potential blocking: max ${stats.max.toFixed(1)} ms ` +
86
+ `(avg ${stats.mean.toFixed(1)} ms, min ${stats.min.toFixed(1)} ms, threshold ${thresholdMs} ms).`;
87
+
88
+ node.warn(message);
89
+ }
90
+
91
+ function readHistogramStats(hist) {
92
+ const mean = Number.isFinite(hist.mean) ? hist.mean / 1e6 : 0;
93
+ const max = Number.isFinite(hist.max) ? hist.max / 1e6 : 0;
94
+ const min = Number.isFinite(hist.min) ? hist.min / 1e6 : 0;
95
+ hist.reset();
96
+ return {
97
+ mean,
98
+ max,
99
+ min,
100
+ alert: max >= thresholdMs
101
+ };
102
+ }
103
+
104
+ function readFallbackStats() {
105
+ const now = performance.now();
106
+ const delta = now - fallbackPrev;
107
+ fallbackPrev = now;
108
+ const delay = Math.max(0, delta - intervalMs);
109
+ return {
110
+ mean: delay,
111
+ max: delay,
112
+ min: delay,
113
+ alert: delay >= thresholdMs
114
+ };
115
+ }
116
+ }
117
+
118
+ RED.nodes.registerType('rp-block-detect', BlockDetectNode, {
119
+ defaults: {
120
+ name: { value: '' },
121
+ intervalMs: { value: 1000 },
122
+ thresholdMs: { value: 120 },
123
+ consecutive: { value: 1 },
124
+ cooldown: { value: 30000 },
125
+ showStats: { value: true }
126
+ }
127
+ });
128
+ };
129
+
130
+ function sanitizeNumber(value, fallback, min) {
131
+ const num = Number(value);
132
+ if (Number.isFinite(num)) {
133
+ return Math.max(min, num);
134
+ }
135
+ return fallback;
136
+ }
@@ -0,0 +1,201 @@
1
+ <script type="text/javascript">
2
+ (function() {
3
+ function toggleCompleteField($complete, type) {
4
+ const $textInput = $complete.typedInput('input');
5
+ if (type === "full") {
6
+ if ($textInput && $textInput.length) {
7
+ $textInput.prop("disabled", true).addClass("disabled");
8
+ }
9
+ $("#node-input-complete-full-label").show();
10
+ } else {
11
+ if ($textInput && $textInput.length) {
12
+ $textInput.prop("disabled", false).removeClass("disabled");
13
+ }
14
+ $("#node-input-complete-full-label").hide();
15
+ }
16
+ }
17
+
18
+ RED.nodes.registerType('rp-clean-debug', {
19
+ category: 'RP Utils',
20
+ paletteLabel: 'clean debug',
21
+ color: '#E2D96E',
22
+ inputs: 1,
23
+ outputs: 0,
24
+ icon: 'font-awesome/fa-bug',
25
+ align: 'right',
26
+ defaults: {
27
+ name: { value: "" },
28
+ active: { value: true },
29
+ tosidebar: { value: true },
30
+ console: { value: false },
31
+ complete: { value: "payload" },
32
+ targetType: { value: "msg" },
33
+ clean: { value: true }
34
+ },
35
+ label: function() {
36
+ if (this.name) {
37
+ return this.name;
38
+ }
39
+ return this.clean === false ? "debug" : "clean debug";
40
+ },
41
+ labelStyle: function() {
42
+ return this.name ? "node_label_italic" : "";
43
+ },
44
+ button: {
45
+ toggle: "active",
46
+ visible: function() {
47
+ return true;
48
+ },
49
+ onclick: function() {
50
+ var node = this;
51
+ var desiredState = node.active;
52
+
53
+ sendCleanDebugToggle(node, desiredState, function() {
54
+ var historyEvent = {
55
+ t: 'edit',
56
+ node: node,
57
+ changes: {
58
+ active: !node.active
59
+ },
60
+ dirty: node.dirty,
61
+ changed: node.changed,
62
+ callback: function(ev) {
63
+ sendCleanDebugToggle(ev.node, ev.node.active);
64
+ }
65
+ };
66
+
67
+ node.changed = true;
68
+ node.dirty = true;
69
+ RED.nodes.dirty(true);
70
+ RED.history.push(historyEvent);
71
+ RED.view.redraw();
72
+ }, function() {
73
+ node.active = !node.active;
74
+ RED.view.redraw();
75
+ RED.notify("Failed to update Clean Debug state", "error");
76
+ });
77
+ }
78
+ },
79
+ oneditprepare: function() {
80
+ const $complete = $("#node-input-complete");
81
+ $complete.typedInput({
82
+ default: 'msg',
83
+ types: [
84
+ 'msg',
85
+ 'flow',
86
+ 'global',
87
+ 'str',
88
+ 'num',
89
+ 'bool',
90
+ 'json',
91
+ 'bin',
92
+ 'date',
93
+ 'jsonata',
94
+ 'env',
95
+ {
96
+ value: 'full',
97
+ label: 'complete msg object',
98
+ hasValue: false
99
+ }
100
+ ],
101
+ typeField: "#node-input-targetType"
102
+ });
103
+
104
+ toggleCompleteField($complete, $complete.typedInput('type'));
105
+
106
+ $complete.on("change", function() {
107
+ toggleCompleteField($complete, $complete.typedInput('type'));
108
+ });
109
+
110
+ $("#node-input-tosidebar").prop("checked", this.tosidebar !== false);
111
+ $("#node-input-console").prop("checked", this.console === true);
112
+ $("#node-input-clean").prop("checked", this.clean !== false);
113
+ },
114
+ oneditsave: function() {
115
+ this.tosidebar = $("#node-input-tosidebar").is(":checked");
116
+ this.console = $("#node-input-console").is(":checked");
117
+ this.clean = $("#node-input-clean").is(":checked");
118
+ }
119
+ });
120
+ })();
121
+ </script>
122
+
123
+ <script type="text/x-red" data-template-name="rp-clean-debug">
124
+ <div class="form-row">
125
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
126
+ <input type="text" id="node-input-name" placeholder="Name">
127
+ </div>
128
+
129
+ <div class="form-row">
130
+ <label for="node-input-complete"><i class="fa fa-dot-circle-o"></i> Output</label>
131
+ <input type="text" id="node-input-complete" style="width: 70%;">
132
+ <input type="hidden" id="node-input-targetType">
133
+ <span id="node-input-complete-full-label" style="display: none;">complete msg object</span>
134
+ </div>
135
+
136
+ <div class="form-row">
137
+ <label></label>
138
+ <input type="checkbox" id="node-input-tosidebar" checked>
139
+ <label for="node-input-tosidebar" style="width: auto;">Send to debug sidebar</label>
140
+ </div>
141
+
142
+ <div class="form-row">
143
+ <label></label>
144
+ <input type="checkbox" id="node-input-console">
145
+ <label for="node-input-console" style="width: auto;">Also log to console</label>
146
+ </div>
147
+
148
+ <div class="form-row">
149
+ <label></label>
150
+ <input type="checkbox" id="node-input-clean" checked>
151
+ <label for="node-input-clean" style="width: auto;">Clean heavy payloads (images/buffers)</label>
152
+ </div>
153
+ </script>
154
+
155
+ <script type="text/x-red" data-help-name="rp-clean-debug">
156
+ <p>An enhanced drop-in replacement for the core Debug node that filters out heavy binary/image payloads by default to keep the debug sidebar responsive.</p>
157
+
158
+ <h3>Details</h3>
159
+ <p>The Clean Debug node mirrors the behaviour of the standard Debug node while adding an optional cleaning step that replaces Buffers, typed arrays, data URLs and large Base64 strings with short placeholders.</p>
160
+
161
+ <h3>Properties</h3>
162
+ <dl class="message-properties">
163
+ <dt>Output<span class="property-type">typed input</span></dt>
164
+ <dd>Select the message property (or expression) to show in the debug sidebar. Choose <strong>complete msg object</strong> to inspect the whole message.</dd>
165
+ <dt>Send to debug sidebar<span class="property-type">boolean</span></dt>
166
+ <dd>Publish the result to the editor sidebar. Disable if you only want console/status output.</dd>
167
+ <dt>Also log to console<span class="property-type">boolean</span></dt>
168
+ <dd>Write the formatted output to the Node-RED runtime log.</dd>
169
+ <dt>Clean heavy payloads<span class="property-type">boolean</span></dt>
170
+ <dd>When enabled (default), large Buffers, Base64 strings and data URLs are replaced with concise placeholders so they do not flood the UI.</dd>
171
+ <dt>Enabled<span class="property-type">boolean</span></dt>
172
+ <dd>Temporarily disable the node without removing it from your flow.</dd>
173
+ </dl>
174
+
175
+ <h3>Notes</h3>
176
+ <ul>
177
+ <li>Disabling the clean option restores the exact behaviour of the built-in Debug node.</li>
178
+ <li>The cleaning step only affects what is displayed; the original message object is left untouched.</li>
179
+ </ul>
180
+ </script>
181
+
182
+ <script type="text/javascript">
183
+ function sendCleanDebugToggle(node, desiredState, onSuccess, onError) {
184
+ $.ajax({
185
+ url: "rp-clean-debug/" + node.id,
186
+ type: "POST",
187
+ data: JSON.stringify({ active: desiredState }),
188
+ contentType: "application/json; charset=utf-8",
189
+ success: function(resp) {
190
+ if (typeof onSuccess === "function") {
191
+ onSuccess(resp);
192
+ }
193
+ },
194
+ error: function(jqXHR, textStatus, errorThrown) {
195
+ if (typeof onError === "function") {
196
+ onError(jqXHR, textStatus, errorThrown);
197
+ }
198
+ }
199
+ });
200
+ }
201
+ </script>