From 941c04878ab24d57c15b08d11f4e9c27960de849 Mon Sep 17 00:00:00 2001 From: Miguel Angel Date: Wed, 30 Jul 2025 14:03:09 -0400 Subject: [PATCH 1/5] feat: refactor FormButton component to utilize Web Workers --- src/components/renderer/form-button.vue | 61 +++++++++++++++++++++---- src/workers/worker.js | 19 ++++++++ 2 files changed, 70 insertions(+), 10 deletions(-) create mode 100644 src/workers/worker.js diff --git a/src/components/renderer/form-button.vue b/src/components/renderer/form-button.vue index aa3116351..4afae959b 100644 --- a/src/components/renderer/form-button.vue +++ b/src/components/renderer/form-button.vue @@ -10,7 +10,7 @@ :disabled="showSpinner" > - {{ showSpinner ? (!loadingLabel ? 'Loading...': loadingLabel) : label }} + {{ showSpinner ? (!loadingLabel ? "Loading..." : loadingLabel) : label }} @@ -19,17 +19,31 @@ import Mustache from 'mustache'; import { mapActions, mapState } from "vuex"; import { getValidPath } from '@/mixins'; +import Worker from "@/workers/worker.js?worker&inline"; export default { mixins: [getValidPath], - props: ['variant', 'label', 'event', 'eventData', 'name', 'fieldValue', 'value', 'tooltip', 'transientData', 'loading', 'loadingLabel', 'handler'], + props: [ + "variant", + "label", + "event", + "eventData", + "name", + "fieldValue", + "value", + "tooltip", + "transientData", + "loading", + "loadingLabel", + "handler" + ], data() { return { showSpinner: false }; }, computed: { - ...mapState('globalErrorsModule', ['valid']), + ...mapState("globalErrorsModule", ["valid"]), classList() { let variant = this.variant || 'primary'; return { @@ -106,15 +120,42 @@ export default { async runHandler() { if (this.handler) { try { - const data = this.getScreenDataReference(null, (screen, name, value) => { - // Enable the data reference to be updated by the handler - screen.$set(screen.vdata, name, value); + const data = this.getScreenDataReference( + null, + (screen, name, value) => { + // Enable the data reference to be updated by the handler + screen.$set(screen.vdata, name, value); + } + ); + + const rawData = data[Symbol.for("__v_raw")]; + + const worker = new Worker(); + console.log("rawData ==> ", rawData); + worker.postMessage({ + fn: this.handler, + data: rawData, }); - await new Function(['toRaw'], this.handler).apply(data, [(item) => { - return item[Symbol.for('__v_raw')]; - }]); + + worker.onmessage = (e) => { + console.log("Received:", e.data); + if (e.data.error) { + console.error("Worker error:", e.data.error); + } else if (e.data.result) { + console.log("Worker result:", e.data.result); + + // Update the data with the result + Object.keys(e.data.result).forEach(key => { + rawData[key] = e.data.result[key]; + }); + } + }; + + // await new Function(['toRaw'], this.handler).apply(data, [(item) => { + // return item[Symbol.for('__v_raw')]; + // }]); } catch (error) { - console.error('❌ There is an error in the button handler', error); + console.error("❌ There is an error in the button handler", error); } } } diff --git a/src/workers/worker.js b/src/workers/worker.js new file mode 100644 index 000000000..d00b7791d --- /dev/null +++ b/src/workers/worker.js @@ -0,0 +1,19 @@ +// worker.js +self.onmessage = function (e) { + console.log("Worker received:", e.data); + + const { fn, data } = e.data; + + try { + // Execute the handler as a function with data as the context + const func = new Function("data", fn); + // const func = new Function('data', `return (${fn})(data)`) + const result = func(data); + console.log("result ==> ", result); + + self.postMessage({ result }); + } catch (error) { + console.error("Error executing handler:", error); + self.postMessage({ error: error.message }); + } +}; From ee134c43a1065ba4208df1d44699e44ac4bca438 Mon Sep 17 00:00:00 2001 From: Miguel Angel Date: Thu, 31 Jul 2025 14:12:25 -0400 Subject: [PATCH 2/5] fix: remove console logs for cleaner code --- src/components/renderer/form-button.vue | 7 ------- src/workers/worker.js | 3 --- 2 files changed, 10 deletions(-) diff --git a/src/components/renderer/form-button.vue b/src/components/renderer/form-button.vue index 4afae959b..86a67fbb0 100644 --- a/src/components/renderer/form-button.vue +++ b/src/components/renderer/form-button.vue @@ -131,18 +131,15 @@ export default { const rawData = data[Symbol.for("__v_raw")]; const worker = new Worker(); - console.log("rawData ==> ", rawData); worker.postMessage({ fn: this.handler, data: rawData, }); worker.onmessage = (e) => { - console.log("Received:", e.data); if (e.data.error) { console.error("Worker error:", e.data.error); } else if (e.data.result) { - console.log("Worker result:", e.data.result); // Update the data with the result Object.keys(e.data.result).forEach(key => { @@ -150,10 +147,6 @@ export default { }); } }; - - // await new Function(['toRaw'], this.handler).apply(data, [(item) => { - // return item[Symbol.for('__v_raw')]; - // }]); } catch (error) { console.error("❌ There is an error in the button handler", error); } diff --git a/src/workers/worker.js b/src/workers/worker.js index d00b7791d..a985bb771 100644 --- a/src/workers/worker.js +++ b/src/workers/worker.js @@ -1,7 +1,5 @@ // worker.js self.onmessage = function (e) { - console.log("Worker received:", e.data); - const { fn, data } = e.data; try { @@ -9,7 +7,6 @@ self.onmessage = function (e) { const func = new Function("data", fn); // const func = new Function('data', `return (${fn})(data)`) const result = func(data); - console.log("result ==> ", result); self.postMessage({ result }); } catch (error) { From 16d154b02fde66231ec839b556ddd98a66375316 Mon Sep 17 00:00:00 2001 From: Miguel Angel Date: Thu, 31 Jul 2025 15:03:52 -0400 Subject: [PATCH 3/5] feat: enhance worker functionality to support async handlers --- src/components/renderer/form-button.vue | 3 ++- src/workers/worker.js | 30 +++++++++++++++++++------ 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/components/renderer/form-button.vue b/src/components/renderer/form-button.vue index 86a67fbb0..45e3a7cd8 100644 --- a/src/components/renderer/form-button.vue +++ b/src/components/renderer/form-button.vue @@ -131,16 +131,17 @@ export default { const rawData = data[Symbol.for("__v_raw")]; const worker = new Worker(); + // Send the handler code to the worker worker.postMessage({ fn: this.handler, data: rawData, }); + // Listen for the result from the worker worker.onmessage = (e) => { if (e.data.error) { console.error("Worker error:", e.data.error); } else if (e.data.result) { - // Update the data with the result Object.keys(e.data.result).forEach(key => { rawData[key] = e.data.result[key]; diff --git a/src/workers/worker.js b/src/workers/worker.js index a985bb771..44767cf00 100644 --- a/src/workers/worker.js +++ b/src/workers/worker.js @@ -1,16 +1,32 @@ // worker.js -self.onmessage = function (e) { +self.onmessage = async function (e) { const { fn, data } = e.data; try { - // Execute the handler as a function with data as the context - const func = new Function("data", fn); - // const func = new Function('data', `return (${fn})(data)`) - const result = func(data); + // Validate inputs + if (!fn || typeof fn !== 'string') { + throw new Error('Function code must be a string'); + } + + // Check if the code contains await to determine if it's async + const isAsync = fn.includes('await') || fn.includes('Promise'); + + // If the code contains await, wrap it in an async function + const functionBody = isAsync + ? `return (async () => { ${fn} })();` + : fn; + + // Use Function constructor with explicit parameter and body + // eslint-disable-next-line no-new-func + const userFunc = new Function('data', functionBody); + const result = isAsync ? await userFunc(data) : userFunc(data); self.postMessage({ result }); } catch (error) { - console.error("Error executing handler:", error); - self.postMessage({ error: error.message }); + console.error('Error executing handler:', error); + self.postMessage({ + error: error.message, + stack: error.stack + }); } }; From 10abf2e27ae076fb86b93d2b70bf78c8b78104b8 Mon Sep 17 00:00:00 2001 From: Miguel Angel Date: Thu, 31 Jul 2025 15:14:42 -0400 Subject: [PATCH 4/5] feat: implement async code detection in worker --- src/workers/worker.js | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/src/workers/worker.js b/src/workers/worker.js index 44767cf00..ab1eb3fd9 100644 --- a/src/workers/worker.js +++ b/src/workers/worker.js @@ -8,8 +8,8 @@ self.onmessage = async function (e) { throw new Error('Function code must be a string'); } - // Check if the code contains await to determine if it's async - const isAsync = fn.includes('await') || fn.includes('Promise'); + // Check if the code is asynchronous + const isAsync = detectAsyncCode(fn); // If the code contains await, wrap it in an async function const functionBody = isAsync @@ -24,9 +24,40 @@ self.onmessage = async function (e) { self.postMessage({ result }); } catch (error) { console.error('Error executing handler:', error); + self.postMessage({ error: error.message, stack: error.stack }); } }; + +function detectAsyncCode(code) { + // Remove comments and strings to avoid false positives + const cleanCode = code + .replace(/\/\*[\s\S]*?\*\//g, '') // Remove block comments + .replace(/\/\/.*$/gm, '') // Remove line comments + .replace(/"[^"]*"/g, '""') // Replace string content + .replace(/'[^']*'/g, "''") // Replace string content + .replace(/`[^`]*`/g, '``'); // Replace template literals + + // Check for async patterns + const asyncPatterns = [ + /\bawait\b/, // await keyword + /\bPromise\b/, // Promise constructor + /\bfetch\b/, // fetch API + /\bsetTimeout\b/, // setTimeout + /\bsetInterval\b/, // setInterval + /\brequestAnimationFrame\b/, // requestAnimationFrame + /\brequestIdleCallback\b/, // requestIdleCallback + /\bnew\s+Promise/, // new Promise + /\b\.then\s*\(/, // .then() method + /\b\.catch\s*\(/, // .catch() method + /\b\.finally\s*\(/, // .finally() method + /\bPromise\./, // Promise static methods + /\basync\b/, // async keyword (in case it's used) + ]; + + // Check if any async pattern is found + return asyncPatterns.some((pattern) => pattern.test(cleanCode)); +} From 94b3e9fe1cd9c09b20d160388c04628d3eccec49 Mon Sep 17 00:00:00 2001 From: Miguel Angel Date: Fri, 1 Aug 2025 09:52:42 -0400 Subject: [PATCH 5/5] test: update button click handler fixture to return reactive data updates --- tests/e2e/fixtures/button_click_handler.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/e2e/fixtures/button_click_handler.json b/tests/e2e/fixtures/button_click_handler.json index 504cac5a5..6bb22c4a4 100644 --- a/tests/e2e/fixtures/button_click_handler.json +++ b/tests/e2e/fixtures/button_click_handler.json @@ -690,7 +690,7 @@ "name": "button_with_handler", "fieldValue": null, "tooltip": {}, - "handler": "this.form_input_2=\"value changed by handler\";" + "handler": "return {\n form_input_2: \"value changed by handler\",\n}" }, "inspector": [ { @@ -1392,7 +1392,7 @@ "name": "record_list_button_with_handler", "fieldValue": null, "tooltip": {}, - "handler": "this.form_input_1=\"value changed by handler\";console.log('handler executed')" + "handler": "console.log('handler executed');\nreturn {\n form_input_1: \"value changed by handler\",\n}" }, "inspector": [ { @@ -1818,4 +1818,4 @@ ], "screen_categories": [], "scripts": [] -} \ No newline at end of file +}