From 36f98133bf4b13e8bc5b15e750d3022a432d3cc3 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Sun, 5 Oct 2025 12:18:18 +0100 Subject: [PATCH] Fix UI lock-up when typed arrays are expanded in debug window closes #5283 --- .../editor-client/src/js/ui/utils.js | 7 +- .../core/common/lib/debug/debug-utils.js | 7 +- .../node_modules/@node-red/util/lib/util.js | 27 ++- test/unit/@node-red/util/lib/util_spec.js | 167 ++++++++++++++++++ 4 files changed, 201 insertions(+), 7 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js b/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js index 667fdbfbc..cc570cd13 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js @@ -15,6 +15,7 @@ **/ RED.utils = (function() { + const listOfTypedArrays = ['Int8Array','Uint8Array','Uint8ClampedArray','Int16Array','Uint16Array','Int32Array','Uint32Array','Float32Array','Float64Array','BigInt64Array','BigUint64Array']; window._marked = window.marked; window.marked = function(txt) { @@ -170,6 +171,8 @@ RED.utils = (function() { result = $('').text(value.data); } else if (value.hasOwnProperty('type') && value.type === 'regexp') { result = $('').text(value.data); + } else if (value.hasOwnProperty('type') && value.hasOwnProperty('data') && listOfTypedArrays.includes(value.type)) { + result = $('').text(value.type + '['+value.length+']'); } else { result = $('object'); } @@ -475,7 +478,7 @@ RED.utils = (function() { var isArray = Array.isArray(obj); var isArrayObject = false; - if (obj && typeof obj === 'object' && obj.hasOwnProperty('type') && obj.hasOwnProperty('data') && ((obj.__enc__ && obj.type === 'set') || (obj.__enc__ && obj.type === 'array') || obj.type === 'Buffer')) { + if (obj && typeof obj === 'object' && obj.hasOwnProperty('type') && obj.hasOwnProperty('data') && ((obj.__enc__ && obj.type === 'set') || (obj.__enc__ && obj.type === 'array') || (obj.__enc__ && listOfTypedArrays.includes(obj.type)) || obj.type === 'Buffer')) { isArray = true; isArrayObject = true; } @@ -1299,7 +1302,7 @@ RED.utils = (function() { payload = Infinity; } else if ((format === 'number') && (payload === "-Infinity")) { payload = -Infinity; - } else if (format === 'Object' || /^(array|set|map)/.test(format) || format === 'boolean' || format === 'number' ) { + } else if (format === 'Object' || /^(array|set|map)/.test(format) || format === 'boolean' || format === 'number' || listOfTypedArrays.includes(format)) { payload = JSON.parse(payload); } else if (/error/i.test(format)) { payload = JSON.parse(payload); diff --git a/packages/node_modules/@node-red/nodes/core/common/lib/debug/debug-utils.js b/packages/node_modules/@node-red/nodes/core/common/lib/debug/debug-utils.js index 55216e2c2..c4931c285 100644 --- a/packages/node_modules/@node-red/nodes/core/common/lib/debug/debug-utils.js +++ b/packages/node_modules/@node-red/nodes/core/common/lib/debug/debug-utils.js @@ -437,6 +437,11 @@ RED.debug = (function() { var property = sanitize(o.property?o.property:''); var payload = o.msg; var format = sanitize((o.format||"").toString()); + var baseFormat = format + // If format is "array[12]"/"Float32Array[12]" then strip off the size portion [12] for the decodeObject + if (/\w+\[\d+\]$/.test(baseFormat)) { + baseFormat = format.replace(/\[\d+\]$/,''); + } msg.attr("class", 'red-ui-debug-msg'+(o.level?(' red-ui-debug-msg-level-'+o.level):'')+ (sourceNode?( " red-ui-debug-msg-node-"+sourceNode.id.replace(/\./g,"_")+ @@ -502,7 +507,7 @@ RED.debug = (function() { $(''+name+'').appendTo(metaRow); } - payload = RED.utils.decodeObject(payload,format); + payload = RED.utils.decodeObject(payload, baseFormat); var el = $('').appendTo(msg); var path = o.property||''; diff --git a/packages/node_modules/@node-red/util/lib/util.js b/packages/node_modules/@node-red/util/lib/util.js index e966d53a7..7883b5c30 100644 --- a/packages/node_modules/@node-red/util/lib/util.js +++ b/packages/node_modules/@node-red/util/lib/util.js @@ -877,14 +877,16 @@ function encodeObject(msg,opts) { }); } else { var isArray = Array.isArray(msg.msg); - var needsStringify = isArray; - if (isArray) { - msg.format = "array["+msg.msg.length+"]"; + const isTypedArray = ArrayBuffer.isView(msg.msg); + var needsStringify = isArray || isTypedArray; + var typeName = isArray ? 'array' : constructorName(msg.msg); + if (isArray || isTypedArray) { + msg.format = typeName + "["+msg.msg.length+"]"; if (msg.msg.length > debuglength) { // msg.msg = msg.msg.slice(0,debuglength); msg.msg = { __enc__: true, - type: "array", + type: typeName, data: msg.msg.slice(0,debuglength), length: msg.msg.length } @@ -948,6 +950,16 @@ function encodeObject(msg,opts) { data: value.slice(0,debuglength), length: value.length } + } else if (ArrayBuffer.isView(value) && value.length > debuglength && !Buffer.isBuffer(value)) { // include typed arrays, exclude Buffer + // Note: Buffer is a subclass of Uint8Array + const typeName = constructorName(value); + /** @type {ArrayBufferView} */ + value = { + __enc__: true, + type: typeName, + data: Array.from(value).slice(0,debuglength), + length: value.length + } } else if (typeof value === 'string') { if (value.length > debuglength) { value = value.substring(0,debuglength)+"..."; @@ -978,6 +990,13 @@ function encodeObject(msg,opts) { if (value.length > debuglength) { value.data = value.data.slice(0,debuglength); } + } else if (ArrayBuffer.isView(value)) { + value = { + __enc__: true, + type: "array", + data: Array.from(value), + length: value.length + } } else if (constructorName(value) === "ServerResponse") { value = "[internal]" } else if (constructorName(value) === "Socket") { diff --git a/test/unit/@node-red/util/lib/util_spec.js b/test/unit/@node-red/util/lib/util_spec.js index 3a52939f8..0d1e5271c 100644 --- a/test/unit/@node-red/util/lib/util_spec.js +++ b/test/unit/@node-red/util/lib/util_spec.js @@ -887,6 +887,145 @@ describe("@node-red/util/util", function() { resultJson.should.have.property("length",2) }); + describe('encode typed arrays', function() { + it('encodes Int8Array', function () { + const arr = new Int8Array([1, 2, 3]); + const msg = { msg: arr }; + const result = util.encodeObject(msg); + result.format.should.eql("Int8Array[3]"); + const resultJson = JSON.parse(result.msg); + resultJson.should.have.property("__enc__", true); + resultJson.should.have.property("type", "array"); + resultJson.should.have.property("data").which.eql([1, 2, 3]); + resultJson.should.have.property("length", 3); + }); + it('encodes Uint8Array', function () { + const arr = new Uint8Array([4, 5, 6]); + const msg = { msg: arr }; + const result = util.encodeObject(msg); + result.format.should.eql("Uint8Array[3]"); + const resultJson = JSON.parse(result.msg); + resultJson.should.have.property("__enc__", true); + resultJson.should.have.property("type", "array"); + resultJson.should.have.property("data").which.eql([4, 5, 6]); + resultJson.should.have.property("length", 3); + }); + it('encodes Uint8ClampedArray', function () { + const arr = new Uint8ClampedArray([7, 8, 9]); + const msg = { msg: arr }; + const result = util.encodeObject(msg); + result.format.should.eql("Uint8ClampedArray[3]"); + const resultJson = JSON.parse(result.msg); + resultJson.should.have.property("__enc__", true); + resultJson.should.have.property("type", "array"); + resultJson.should.have.property("data").which.eql([7, 8, 9]); + resultJson.should.have.property("length", 3); + }); + it('encodes Int16Array', function () { + const arr = new Int16Array([10, 11, 12]); + const msg = { msg: arr }; + const result = util.encodeObject(msg); + result.format.should.eql("Int16Array[3]"); + const resultJson = JSON.parse(result.msg); + resultJson.should.have.property("__enc__", true); + resultJson.should.have.property("type", "array"); + resultJson.should.have.property("data").which.eql([10, 11, 12]); + resultJson.should.have.property("length", 3); + }); + it('encodes Uint16Array', function () { + const arr = new Uint16Array([13, 14, 15]); + const msg = { msg: arr }; + const result = util.encodeObject(msg); + result.format.should.eql("Uint16Array[3]"); + const resultJson = JSON.parse(result.msg); + resultJson.should.have.property("__enc__", true); + resultJson.should.have.property("type", "array"); + resultJson.should.have.property("data").which.eql([13, 14, 15]); + resultJson.should.have.property("length", 3); + }); + it('encodes Int32Array', function () { + const arr = new Int32Array([16, 17, 18]); + const msg = { msg: arr }; + const result = util.encodeObject(msg); + result.format.should.eql("Int32Array[3]"); + const resultJson = JSON.parse(result.msg); + resultJson.should.have.property("__enc__", true); + resultJson.should.have.property("type", "array"); + resultJson.should.have.property("data").which.eql([16, 17, 18]); + resultJson.should.have.property("length", 3); + }); + it('encodes Uint32Array', function () { + const arr = new Uint32Array([19, 20, 21]); + const msg = { msg: arr }; + const result = util.encodeObject(msg); + result.format.should.eql("Uint32Array[3]"); + const resultJson = JSON.parse(result.msg); + resultJson.should.have.property("__enc__", true); + resultJson.should.have.property("type", "array"); + resultJson.should.have.property("data").which.eql([19, 20, 21]); + resultJson.should.have.property("length", 3); + }); + it('encodes Float32Array', function () { + const arr = new Float32Array([22.1, 23.2, 24.3]); + const msg = { msg: arr }; + const result = util.encodeObject(msg); + result.format.should.eql("Float32Array[3]"); + const resultJson = JSON.parse(result.msg); + resultJson.should.have.property("__enc__", true); + resultJson.should.have.property("type", "array"); + resultJson.should.have.property("data").which.be.an.Array(); + resultJson.data[0].should.be.approximately(22.1, 0.00001); + resultJson.data[1].should.be.approximately(23.2, 0.00001); + resultJson.data[2].should.be.approximately(24.3, 0.00001); + resultJson.should.have.property("length", 3); + }); + it('encodes Float64Array', function () { + const arr = new Float64Array([25.4, 26.5, 27.6]); + const msg = { msg: arr }; + const result = util.encodeObject(msg); + result.format.should.eql("Float64Array[3]"); + const resultJson = JSON.parse(result.msg); + resultJson.should.have.property("__enc__", true); + resultJson.should.have.property("type", "array"); + resultJson.should.have.property("data").which.be.an.Array(); + resultJson.data[0].should.be.approximately(25.4, 0.00001); + resultJson.data[1].should.be.approximately(26.5, 0.00001); + resultJson.data[2].should.be.approximately(27.6, 0.00001); + resultJson.should.have.property("length", 3); + }); + it('encodes BigInt64Array', function () { + const arr = new BigInt64Array([BigInt(28), BigInt(29), BigInt(30)]); + const msg = { msg: arr }; + const result = util.encodeObject(msg); + result.format.should.eql("BigInt64Array[3]"); + const resultJson = JSON.parse(result.msg); + resultJson.should.have.property("__enc__", true); + resultJson.should.have.property("type", "array"); + // BigInt arrays are stringified in JSON + resultJson.should.have.property("data").which.eql([ + { "__enc__": true, "data": "28", "type": "bigint" }, + { "__enc__": true, "data": "29", "type": "bigint" }, + { "__enc__": true, "data": "30", "type": "bigint" } + ]); + resultJson.should.have.property("length", 3); + }); + it('encodes BigUint64Array', function () { + const arr = new BigUint64Array([BigInt(31), BigInt(32), BigInt(33)]); + const msg = { msg: arr }; + const result = util.encodeObject(msg); + result.format.should.eql("BigUint64Array[3]"); + const resultJson = JSON.parse(result.msg); + resultJson.should.have.property("__enc__", true); + resultJson.should.have.property("type", "array"); + // BigInt arrays are stringified in JSON + resultJson.should.have.property("data").which.eql([ + { "__enc__": true, "data": "31", "type": "bigint" }, + { "__enc__": true, "data": "32", "type": "bigint" }, + { "__enc__": true, "data": "33", "type": "bigint" } + ]); + resultJson.should.have.property("length", 3); + }); + }); describe('encode object', function() { it('object', function() { @@ -966,6 +1105,34 @@ describe("@node-red/util/util", function() { resultJson.aSet.should.have.property("data",["a","b"]); resultJson.aSet.should.have.property("length",2) }); + it('object with typed array property (Int8Array)', function() { + const arr = new Int8Array([1, 2, 3]); + const msg = { msg: { anArray: arr } }; + const result = util.encodeObject(msg); + result.format.should.eql("Object"); + const resultJson = JSON.parse(result.msg); + resultJson.should.have.property("anArray"); + resultJson.anArray.should.have.property("__enc__", true); + resultJson.anArray.should.have.property("type", "array"); + resultJson.anArray.should.have.property("data", [1, 2, 3]); + resultJson.anArray.should.have.property("length", 3); + }); + it('object with typed array property (BigInt64Array)', function() { + const arr = new BigInt64Array([10n, 20n, 30n]); + const msg = { msg: { anArray: arr } }; + const result = util.encodeObject(msg); + result.format.should.eql("Object"); + const resultJson = JSON.parse(result.msg); + resultJson.should.have.property("anArray"); + resultJson.anArray.should.have.property("__enc__", true); + resultJson.anArray.should.have.property("type", "array"); + resultJson.anArray.should.have.property("data", [ + { __enc__: true, data: "10", type: "bigint" }, + { __enc__: true, data: "20", type: "bigint" }, + { __enc__: true, data: "30", type: "bigint" } + ]); + resultJson.anArray.should.have.property("length", 3); + }); it('constructor of IncomingMessage', function() { function IncomingMessage(){}; var msg = { msg:new IncomingMessage() };