Browse Source

chore: init offscreen page

Ahmad Kholid 1 year ago
parent
commit
ac6b871aa9

+ 3 - 1
jsconfig.json

@@ -4,7 +4,9 @@
     "paths": {
     "paths": {
       "@/*": ["src/*"],
       "@/*": ["src/*"],
       "@business": ["business/dev/*"]
       "@business": ["business/dev/*"]
-    }
+    },
+    "module": "ESNext",
+    "target": "ES6"
   },
   },
   "include": ["src/**/*", "utils/**/*"]
   "include": ["src/**/*", "utils/**/*"]
 }
 }

+ 1 - 0
package.json

@@ -105,6 +105,7 @@
     "@babel/preset-env": "^7.20.2",
     "@babel/preset-env": "^7.20.2",
     "@intlify/vue-i18n-loader": "^4.2.0",
     "@intlify/vue-i18n-loader": "^4.2.0",
     "@tailwindcss/typography": "^0.5.1",
     "@tailwindcss/typography": "^0.5.1",
+    "@types/chrome": "^0.0.267",
     "@vue/compiler-sfc": "^3.3.4",
     "@vue/compiler-sfc": "^3.3.4",
     "archiver": "^5.3.1",
     "archiver": "^5.3.1",
     "autoprefixer": "^10.4.12",
     "autoprefixer": "^10.4.12",

+ 82 - 0
src/background/BackgroundOffscreen.js

@@ -0,0 +1,82 @@
+/* eslint-disable class-methods-use-this */
+import { IS_FIREFOX } from '@/common/utils/constant';
+import { MessageListener } from '@/utils/message';
+import Browser from 'webextension-polyfill';
+
+const OFFSCREEN_URL = Browser.runtime.getURL('/offscreen.html');
+
+class BackgroundOffscreen {
+  /** @type {BackgroundOffscreen} */
+  static #_instance;
+
+  /**
+   * OffscreenService singleton
+   * @returns {BackgroundOffscreen}
+   */
+  static get instance() {
+    if (!this.#_instance) {
+      this.#_instance = new BackgroundOffscreen();
+    }
+
+    return this.#_instance;
+  }
+
+  /** @type {MessageListener} */
+  #messageListener;
+
+  constructor() {
+    this.#messageListener = new MessageListener('offscreen');
+
+    this.on = this.#messageListener.on;
+  }
+
+  /**
+   *
+   * @returns {Promise<boolean>}
+   */
+  async #ensureDocument() {
+    if (IS_FIREFOX) return;
+
+    const isOpened = await this.isOpened();
+    if (isOpened) return;
+
+    await chrome.offscreen.createDocument({
+      url: OFFSCREEN_URL,
+      reasons: [
+        chrome.offscreen.Reason.BLOBS,
+        chrome.offscreen.Reason.CLIPBOARD,
+        chrome.offscreen.Reason.IFRAME_SCRIPTING,
+      ],
+      justification: 'For running the workflow',
+    });
+  }
+
+  /**
+   *
+   * @returns {Promise<boolean>}
+   */
+  async isOpened() {
+    if (IS_FIREFOX) return false;
+
+    const contexts = await chrome.runtime.getContexts({
+      documentUrls: [OFFSCREEN_URL],
+      contextTypes: ['OFFSCREEN_DOCUMENT'],
+    });
+
+    return Boolean(contexts.length);
+  }
+
+  /**
+   *
+   * @param {string} name
+   * @param {*} data
+   * @returns {Promise<*>}
+   */
+  async sendMessage(name, data) {
+    await this.#ensureDocument();
+
+    return this.#messageListener.sendMessage(name, data);
+  }
+}
+
+export default BackgroundOffscreen;

+ 8 - 22
src/background/BackgroundWorkflowTriggers.js

@@ -6,26 +6,8 @@ import {
   registerSpecificDay,
   registerSpecificDay,
   registerWorkflowTrigger,
   registerWorkflowTrigger,
 } from '@/utils/workflowTrigger';
 } from '@/utils/workflowTrigger';
-import BackgroundUtils from './BackgroundUtils';
 import BackgroundWorkflowUtils from './BackgroundWorkflowUtils';
 import BackgroundWorkflowUtils from './BackgroundWorkflowUtils';
 
 
-async function executeWorkflow(workflowData, options) {
-  if (workflowData.isDisabled) return;
-
-  const isMV2 = browser.runtime.getManifest().manifest_version === 2;
-  const context = workflowData.settings.execContext;
-  if (isMV2 || context === 'background') {
-    BackgroundWorkflowUtils.executeWorkflow(workflowData, options);
-    return;
-  }
-
-  await BackgroundUtils.openDashboard('?fromBackground=true', false);
-  await BackgroundUtils.sendMessageToDashboard('workflow:execute', {
-    data: workflowData,
-    options,
-  });
-}
-
 class BackgroundWorkflowTriggers {
 class BackgroundWorkflowTriggers {
   static async visitWebTriggers(tabId, tabUrl, spa = false) {
   static async visitWebTriggers(tabId, tabUrl, spa = false) {
     const { visitWebTriggers } = await browser.storage.local.get(
     const { visitWebTriggers } = await browser.storage.local.get(
@@ -51,7 +33,11 @@ class BackgroundWorkflowTriggers {
       const workflowData = await BackgroundWorkflowUtils.getWorkflow(
       const workflowData = await BackgroundWorkflowUtils.getWorkflow(
         workflowId
         workflowId
       );
       );
-      if (workflowData) executeWorkflow(workflowData, { tabId });
+      if (workflowData) {
+        BackgroundWorkflowUtils.instance.executeWorkflow(workflowData, {
+          tabId,
+        });
+      }
     }
     }
   }
   }
 
 
@@ -118,7 +104,7 @@ class BackgroundWorkflowTriggers {
         if (isAfter) return;
         if (isAfter) return;
       }
       }
 
 
-      executeWorkflow(currentWorkflow);
+      BackgroundWorkflowUtils.instance.executeWorkflow(currentWorkflow);
 
 
       if (!data) return;
       if (!data) return;
 
 
@@ -154,7 +140,7 @@ class BackgroundWorkflowTriggers {
       const workflowData = await BackgroundWorkflowUtils.getWorkflow(
       const workflowData = await BackgroundWorkflowUtils.getWorkflow(
         workflowId
         workflowId
       );
       );
-      executeWorkflow(workflowData, {
+      BackgroundWorkflowUtils.instance.executeWorkflow(workflowData, {
         data: {
         data: {
           variables: message,
           variables: message,
         },
         },
@@ -202,7 +188,7 @@ class BackgroundWorkflowTriggers {
 
 
       if (triggerBlock) {
       if (triggerBlock) {
         if (isStartup && triggerBlock.type === 'on-startup') {
         if (isStartup && triggerBlock.type === 'on-startup') {
-          executeWorkflow(currWorkflow);
+          BackgroundWorkflowUtils.instance.executeWorkflow(currWorkflow);
         } else {
         } else {
           if (isStartup && triggerBlock.triggers) {
           if (isStartup && triggerBlock.triggers) {
             for (const trigger of triggerBlock.triggers) {
             for (const trigger of triggerBlock.triggers) {

+ 94 - 9
src/background/BackgroundWorkflowUtils.js

@@ -1,7 +1,28 @@
+import { IS_FIREFOX } from '@/common/utils/constant';
 import browser from 'webextension-polyfill';
 import browser from 'webextension-polyfill';
-import { startWorkflowExec } from '@/workflowEngine';
+import BackgroundOffscreen from './BackgroundOffscreen';
 
 
 class BackgroundWorkflowUtils {
 class BackgroundWorkflowUtils {
+  /** @type {BackgroundWorkflowUtils} */
+  static #_instance;
+
+  /**
+   * BackgroundWorkflowUtils singleton
+   * @type {BackgroundWorkflowUtils}
+   */
+  static get instance() {
+    if (!this.#_instance) this.#_instance = new BackgroundWorkflowUtils();
+
+    return this.#_instance;
+  }
+
+  /** @type {import('@/workflowEngine/WorkflowManager').default} */
+  #workflowManager;
+
+  constructor() {
+    this.#workflowManager = null;
+  }
+
   static flattenTeamWorkflows(workflows) {
   static flattenTeamWorkflows(workflows) {
     return Object.values(Object.values(workflows || {})[0] || {});
     return Object.values(Object.values(workflows || {})[0] || {});
   }
   }
@@ -39,16 +60,80 @@ class BackgroundWorkflowUtils {
     return findWorkflow;
     return findWorkflow;
   }
   }
 
 
-  static async executeWorkflow(workflowData, options) {
+  async #ensureWorkflowManager() {
+    if (!IS_FIREFOX) return;
+
+    this.#workflowManager = (
+      await import('@/workflowEngine/WorkflowManager')
+    ).default.instance;
+  }
+
+  /**
+   * Stop workflow execution
+   * @param {string} stateId
+   * @returns {Promise<void>}
+   */
+  async stopExecution(stateId) {
+    if (IS_FIREFOX) {
+      await this.#ensureWorkflowManager();
+      this.#workflowManager.stopExecution(stateId);
+      return;
+    }
+
+    await BackgroundOffscreen.instance.sendMessage('workflow:stop', stateId);
+  }
+
+  /**
+   * Resume workflow execution
+   * @param {string} stateId
+   * @param {object} nextBlock
+   * @returns {Promise<void>}
+   */
+  async resumeExecution(stateId, nextBlock) {
+    if (IS_FIREFOX) {
+      await this.#ensureWorkflowManager();
+      this.#workflowManager.resumeExecution(stateId, nextBlock);
+      return;
+    }
+
+    await BackgroundOffscreen.instance.sendMessage('workflow:resume', {
+      id: stateId,
+      nextBlock,
+    });
+  }
+
+  /**
+   * Update workflow execution state
+   * @param {string} stateId
+   * @param {object} data
+   * @returns {Promise<void>}
+   */
+  async updateExecutionState(stateId, data) {
+    if (IS_FIREFOX) {
+      await this.#ensureWorkflowManager();
+      this.#workflowManager.updateExecution(stateId, data);
+      return;
+    }
+
+    await BackgroundOffscreen.instance.sendMessage('workflow:update', {
+      data,
+      id: stateId,
+    });
+  }
+
+  async executeWorkflow(workflowData, options) {
     if (workflowData.isDisabled) return;
     if (workflowData.isDisabled) return;
 
 
-    /**
-     * Under v2, the background runtime environment is a real browser window. It has DOM, URL...
-      But these don't exist under v3. v3 uses service_worker (https://developer.mozilla.org/zh-CN/docs/Web/API/Service_Worker_API), so a dashboard page is created to run the workflow
-      So v2 and isPopup are actually the same
-     */
-    const isMV2 = browser.runtime.getManifest().manifest_version === 2;
-    startWorkflowExec(workflowData, options, isMV2);
+    if (IS_FIREFOX) {
+      await this.#ensureWorkflowManager();
+      this.#workflowManager.execute(workflowData, options);
+      return;
+    }
+
+    await BackgroundOffscreen.instance.sendMessage(
+      'workflow:execute',
+      workflowData
+    );
   }
   }
 }
 }
 
 

+ 52 - 60
src/background/index.js

@@ -3,7 +3,6 @@ import { MessageListener } from '@/utils/message';
 import { sleep } from '@/utils/helper';
 import { sleep } from '@/utils/helper';
 import getFile, { readFileAsBase64 } from '@/utils/getFile';
 import getFile, { readFileAsBase64 } from '@/utils/getFile';
 import automa from '@business';
 import automa from '@business';
-import { workflowState } from '@/workflowEngine';
 import { registerWorkflowTrigger } from '../utils/workflowTrigger';
 import { registerWorkflowTrigger } from '../utils/workflowTrigger';
 import BackgroundUtils from './BackgroundUtils';
 import BackgroundUtils from './BackgroundUtils';
 import BackgroundWorkflowUtils from './BackgroundWorkflowUtils';
 import BackgroundWorkflowUtils from './BackgroundWorkflowUtils';
@@ -123,26 +122,17 @@ message.on('dashboard:refresh-packages', async () => {
   });
   });
 });
 });
 
 
-message.on('workflow:stop', (stateId) => workflowState.stop(stateId));
+message.on('workflow:stop', (stateId) =>
+  BackgroundWorkflowUtils.instance.stopExecution(stateId)
+);
 message.on('workflow:execute', async (workflowData, sender) => {
 message.on('workflow:execute', async (workflowData, sender) => {
-  const context = workflowData.settings.execContext;
-  const isMV2 = browser.runtime.getManifest().manifest_version === 2;
-  if (!isMV2 && (!context || context === 'popup')) {
-    await BackgroundUtils.openDashboard('?fromBackground=true', false);
-    await BackgroundUtils.sendMessageToDashboard('workflow:execute', {
-      data: workflowData,
-      options: workflowData.option,
-    });
-    return;
-  }
-
   if (workflowData.includeTabId) {
   if (workflowData.includeTabId) {
     if (!workflowData.options) workflowData.options = {};
     if (!workflowData.options) workflowData.options = {};
 
 
     workflowData.options.tabId = sender.tab.id;
     workflowData.options.tabId = sender.tab.id;
   }
   }
 
 
-  BackgroundWorkflowUtils.executeWorkflow(
+  BackgroundWorkflowUtils.instance.executeWorkflow(
     workflowData,
     workflowData,
     workflowData?.options || {}
     workflowData?.options || {}
   );
   );
@@ -193,56 +183,58 @@ message.on('recording:stop', async () => {
 });
 });
 message.on('workflow:resume', ({ id, nextBlock }) => {
 message.on('workflow:resume', ({ id, nextBlock }) => {
   if (!id) return;
   if (!id) return;
-  workflowState.resume(id, nextBlock);
+  BackgroundWorkflowUtils.instance.resumeExecution(id, nextBlock);
 });
 });
 message.on('workflow:breakpoint', (id) => {
 message.on('workflow:breakpoint', (id) => {
   if (!id) return;
   if (!id) return;
-  workflowState.update(id, { status: 'breakpoint' });
+  BackgroundWorkflowUtils.instance.updateExecutionState(id, {
+    status: 'breakpoint',
+  });
 });
 });
 
 
 automa('background', message);
 automa('background', message);
 
 
-browser.runtime.onMessage.addListener(message.listener());
-
-if (!isFirefox) {
-  const sandboxIframe = document.createElement('iframe');
-  sandboxIframe.src = '/sandbox.html';
-  sandboxIframe.id = 'sandbox';
-
-  document.body.appendChild(sandboxIframe);
-
-  window.addEventListener('message', async ({ data }) => {
-    if (data?.type !== 'automa-fetch') return;
-
-    const sendResponse = (result) => {
-      sandboxIframe.contentWindow.postMessage(
-        {
-          type: 'fetchResponse',
-          data: result,
-          id: data.data.id,
-        },
-        '*'
-      );
-    };
-
-    const { type, resource } = data.data;
-    try {
-      const response = await fetch(resource.url, resource);
-      if (!response.ok) throw new Error(response.statusText);
-
-      let result = null;
-
-      if (type === 'base64') {
-        const blob = await response.blob();
-        const base64 = await readFileAsBase64(blob);
-
-        result = base64;
-      } else {
-        result = await response[type]();
-      }
-      sendResponse({ isError: false, result });
-    } catch (error) {
-      sendResponse({ isError: true, result: error.message });
-    }
-  });
-}
+browser.runtime.onMessage.addListener(message.listener);
+
+// if (!isFirefox) {
+//   const sandboxIframe = document.createElement('iframe');
+//   sandboxIframe.src = '/sandbox.html';
+//   sandboxIframe.id = 'sandbox';
+
+//   document.body.appendChild(sandboxIframe);
+
+//   window.addEventListener('message', async ({ data }) => {
+//     if (data?.type !== 'automa-fetch') return;
+
+//     const sendResponse = (result) => {
+//       sandboxIframe.contentWindow.postMessage(
+//         {
+//           type: 'fetchResponse',
+//           data: result,
+//           id: data.data.id,
+//         },
+//         '*'
+//       );
+//     };
+
+//     const { type, resource } = data.data;
+//     try {
+//       const response = await fetch(resource.url, resource);
+//       if (!response.ok) throw new Error(response.statusText);
+
+//       let result = null;
+
+//       if (type === 'base64') {
+//         const blob = await response.blob();
+//         const base64 = await readFileAsBase64(blob);
+
+//         result = base64;
+//       } else {
+//         result = await response[type]();
+//       }
+//       sendResponse({ isError: false, result });
+//     } catch (error) {
+//       sendResponse({ isError: true, result: error.message });
+//     }
+//   });
+// }

+ 1 - 0
src/common/utils/constant.js

@@ -0,0 +1 @@
+export const IS_FIREFOX = BROWSER_TYPE === 'firefox';

+ 2 - 2
src/components/newtab/app/AppLogsItemRunning.vue

@@ -76,10 +76,10 @@ import { useRouter } from 'vue-router';
 import { useI18n } from 'vue-i18n';
 import { useI18n } from 'vue-i18n';
 import { countDuration } from '@/utils/helper';
 import { countDuration } from '@/utils/helper';
 import { useWorkflowStore } from '@/stores/workflow';
 import { useWorkflowStore } from '@/stores/workflow';
-import { stopWorkflowExec } from '@/workflowEngine';
 import dbLogs from '@/db/logs';
 import dbLogs from '@/db/logs';
 import dayjs from '@/lib/dayjs';
 import dayjs from '@/lib/dayjs';
 import LogsHistory from '@/components/newtab/logs/LogsHistory.vue';
 import LogsHistory from '@/components/newtab/logs/LogsHistory.vue';
+import RendererWorkflowService from '@/service/renderer/RendererWorkflowService';
 
 
 const props = defineProps({
 const props = defineProps({
   logId: {
   logId: {
@@ -103,7 +103,7 @@ const running = computed(() =>
 );
 );
 
 
 function stopWorkflow() {
 function stopWorkflow() {
-  stopWorkflowExec(running.value.id);
+  RendererWorkflowService.stopWorkflowExecution(running.value.id);
   emit('close');
   emit('close');
 }
 }
 
 

+ 2 - 2
src/components/newtab/shared/SharedLogsTable.vue

@@ -128,8 +128,8 @@
 import { reactive } from 'vue';
 import { reactive } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { useI18n } from 'vue-i18n';
 import { countDuration } from '@/utils/helper';
 import { countDuration } from '@/utils/helper';
-import { stopWorkflowExec } from '@/workflowEngine';
 import dayjs from '@/lib/dayjs';
 import dayjs from '@/lib/dayjs';
+import RendererWorkflowService from '@/service/renderer/RendererWorkflowService';
 
 
 defineProps({
 defineProps({
   logs: {
   logs: {
@@ -160,7 +160,7 @@ function getTranslation(key, defText = '') {
   return te(key) ? t(key) : defText;
   return te(key) ? t(key) : defText;
 }
 }
 function stopWorkflow(stateId) {
 function stopWorkflow(stateId) {
-  stopWorkflowExec(stateId);
+  RendererWorkflowService.stopWorkflowExecution(stateId);
 }
 }
 function toggleSelectedLog(selected, id) {
 function toggleSelectedLog(selected, id) {
   if (selected) {
   if (selected) {

+ 2 - 2
src/components/newtab/shared/SharedWorkflowState.vue

@@ -59,7 +59,7 @@
 import browser from 'webextension-polyfill';
 import browser from 'webextension-polyfill';
 import { useI18n } from 'vue-i18n';
 import { useI18n } from 'vue-i18n';
 import { getBlocks } from '@/utils/getSharedData';
 import { getBlocks } from '@/utils/getSharedData';
-import { stopWorkflowExec } from '@/workflowEngine';
+import RendererWorkflowService from '@/service/renderer/RendererWorkflowService';
 import dayjs from '@/lib/dayjs';
 import dayjs from '@/lib/dayjs';
 
 
 const props = defineProps({
 const props = defineProps({
@@ -81,6 +81,6 @@ function openTab() {
   browser.tabs.update(props.data.state.tabId, { active: true });
   browser.tabs.update(props.data.state.tabId, { active: true });
 }
 }
 function stopWorkflow() {
 function stopWorkflow() {
-  stopWorkflowExec(props.data.id);
+  RendererWorkflowService.stopWorkflowExecution(props.data.id);
 }
 }
 </script>
 </script>

+ 2 - 2
src/components/newtab/workflow/WorkflowRunning.vue

@@ -43,8 +43,8 @@
 import browser from 'webextension-polyfill';
 import browser from 'webextension-polyfill';
 import { useI18n } from 'vue-i18n';
 import { useI18n } from 'vue-i18n';
 import { getBlocks } from '@/utils/getSharedData';
 import { getBlocks } from '@/utils/getSharedData';
-import { stopWorkflowExec } from '@/workflowEngine';
 import dayjs from '@/lib/dayjs';
 import dayjs from '@/lib/dayjs';
+import RendererWorkflowService from '@/service/renderer/RendererWorkflowService';
 
 
 defineProps({
 defineProps({
   data: {
   data: {
@@ -70,6 +70,6 @@ function openTab(tabId) {
   browser.tabs.update(tabId, { active: true });
   browser.tabs.update(tabId, { active: true });
 }
 }
 function stopWorkflow(item) {
 function stopWorkflow(item) {
-  stopWorkflowExec(item);
+  RendererWorkflowService.stopWorkflowExecution(item);
 }
 }
 </script>
 </script>

+ 2 - 2
src/components/newtab/workflow/editor/EditorLocalActions.vue

@@ -345,7 +345,7 @@ import { tagColors } from '@/utils/shared';
 import { parseJSON, findTriggerBlock } from '@/utils/helper';
 import { parseJSON, findTriggerBlock } from '@/utils/helper';
 import { exportWorkflow, convertWorkflow } from '@/utils/workflowData';
 import { exportWorkflow, convertWorkflow } from '@/utils/workflowData';
 import { registerWorkflowTrigger } from '@/utils/workflowTrigger';
 import { registerWorkflowTrigger } from '@/utils/workflowTrigger';
-import { executeWorkflow } from '@/workflowEngine';
+import RendererWorkflowService from '@/service/renderer/RendererWorkflowService';
 import getTriggerText from '@/utils/triggerText';
 import getTriggerText from '@/utils/triggerText';
 import convertWorkflowData from '@/utils/convertWorkflowData';
 import convertWorkflowData from '@/utils/convertWorkflowData';
 import WorkflowShareTeam from '@/components/newtab/workflow/WorkflowShareTeam.vue';
 import WorkflowShareTeam from '@/components/newtab/workflow/WorkflowShareTeam.vue';
@@ -475,7 +475,7 @@ function executeCurrWorkflow() {
     saveWorkflow();
     saveWorkflow();
   }
   }
 
 
-  executeWorkflow({
+  RendererWorkflowService.executeWorkflow({
     ...props.workflow,
     ...props.workflow,
     isTesting: props.isDataChanged,
     isTesting: props.isDataChanged,
   });
   });

+ 0 - 1
src/components/newtab/workflows/WorkflowsHosted.vue

@@ -15,7 +15,6 @@ import { useI18n } from 'vue-i18n';
 import { useDialog } from '@/composable/dialog';
 import { useDialog } from '@/composable/dialog';
 import { arraySorter } from '@/utils/helper';
 import { arraySorter } from '@/utils/helper';
 import { useHostedWorkflowStore } from '@/stores/hostedWorkflow';
 import { useHostedWorkflowStore } from '@/stores/hostedWorkflow';
-import { executeWorkflow } from '@/workflowEngine';
 import SharedCard from '@/components/newtab/shared/SharedCard.vue';
 import SharedCard from '@/components/newtab/shared/SharedCard.vue';
 
 
 const props = defineProps({
 const props = defineProps({

+ 3 - 3
src/components/newtab/workflows/WorkflowsLocal.vue

@@ -26,7 +26,7 @@
           :is-pinned="true"
           :is-pinned="true"
           :menu="menu"
           :menu="menu"
           @dragstart="onDragStart"
           @dragstart="onDragStart"
-          @execute="executeWorkflow(workflow)"
+          @execute="RendererWorkflowService.executeWorkflow(workflow)"
           @toggle-pin="togglePinWorkflow(workflow)"
           @toggle-pin="togglePinWorkflow(workflow)"
           @toggle-disable="toggleDisableWorkflow(workflow)"
           @toggle-disable="toggleDisableWorkflow(workflow)"
         />
         />
@@ -42,7 +42,7 @@
         :is-pinned="state.pinnedWorkflows.includes(workflow.id)"
         :is-pinned="state.pinnedWorkflows.includes(workflow.id)"
         :menu="menu"
         :menu="menu"
         @dragstart="onDragStart"
         @dragstart="onDragStart"
-        @execute="executeWorkflow(workflow)"
+        @execute="RendererWorkflowService.executeWorkflow(workflow)"
         @toggle-pin="togglePinWorkflow(workflow)"
         @toggle-pin="togglePinWorkflow(workflow)"
         @toggle-disable="toggleDisableWorkflow(workflow)"
         @toggle-disable="toggleDisableWorkflow(workflow)"
       />
       />
@@ -122,7 +122,7 @@ import { useDialog } from '@/composable/dialog';
 import { useWorkflowStore } from '@/stores/workflow';
 import { useWorkflowStore } from '@/stores/workflow';
 import { exportWorkflow } from '@/utils/workflowData';
 import { exportWorkflow } from '@/utils/workflowData';
 import { useSharedWorkflowStore } from '@/stores/sharedWorkflow';
 import { useSharedWorkflowStore } from '@/stores/sharedWorkflow';
-import { executeWorkflow } from '@/workflowEngine';
+import RendererWorkflowService from '@/service/renderer/RendererWorkflowService';
 import WorkflowsLocalCard from './WorkflowsLocalCard.vue';
 import WorkflowsLocalCard from './WorkflowsLocalCard.vue';
 
 
 const props = defineProps({
 const props = defineProps({

+ 2 - 2
src/components/newtab/workflows/WorkflowsShared.vue

@@ -4,7 +4,7 @@
     :key="workflow.id"
     :key="workflow.id"
     :data="workflow"
     :data="workflow"
     :show-details="false"
     :show-details="false"
-    @execute="executeWorkflow(workflow)"
+    @execute="RendererWorkflowService.executeWorkflow(workflow)"
     @click="$router.push(`/workflows/${$event.id}/shared`)"
     @click="$router.push(`/workflows/${$event.id}/shared`)"
   />
   />
 </template>
 </template>
@@ -12,8 +12,8 @@
 import { computed } from 'vue';
 import { computed } from 'vue';
 import { useSharedWorkflowStore } from '@/stores/sharedWorkflow';
 import { useSharedWorkflowStore } from '@/stores/sharedWorkflow';
 import { arraySorter } from '@/utils/helper';
 import { arraySorter } from '@/utils/helper';
-import { executeWorkflow } from '@/workflowEngine';
 import SharedCard from '@/components/newtab/shared/SharedCard.vue';
 import SharedCard from '@/components/newtab/shared/SharedCard.vue';
+import RendererWorkflowService from '@/service/renderer/RendererWorkflowService';
 
 
 const props = defineProps({
 const props = defineProps({
   search: {
   search: {

+ 3 - 3
src/components/newtab/workflows/WorkflowsUserTeam.vue

@@ -35,9 +35,9 @@
       :data="workflow"
       :data="workflow"
       :menu="workflowMenus"
       :menu="workflowMenus"
       :disabled="isUnknownTeam"
       :disabled="isUnknownTeam"
-      @menuSelected="onMenuSelected"
-      @execute="executeWorkflow(workflow)"
       @click="openWorkflowPage"
       @click="openWorkflowPage"
+      @menuSelected="onMenuSelected"
+      @execute="RendererWorkflowService.executeWorkflow(workflow)"
     >
     >
       <template #footer-content>
       <template #footer-content>
         <span
         <span
@@ -61,7 +61,7 @@ import { useTeamWorkflowStore } from '@/stores/teamWorkflow';
 import { arraySorter } from '@/utils/helper';
 import { arraySorter } from '@/utils/helper';
 import { useDialog } from '@/composable/dialog';
 import { useDialog } from '@/composable/dialog';
 import { tagColors } from '@/utils/shared';
 import { tagColors } from '@/utils/shared';
-import { executeWorkflow } from '@/workflowEngine';
+import RendererWorkflowService from '@/service/renderer/RendererWorkflowService';
 import SharedCard from '@/components/newtab/shared/SharedCard.vue';
 import SharedCard from '@/components/newtab/shared/SharedCard.vue';
 
 
 const props = defineProps({
 const props = defineProps({

+ 1 - 0
src/manifest.chrome.json

@@ -1,6 +1,7 @@
 {
 {
   "manifest_version": 3,
   "manifest_version": 3,
   "name": "Automa",
   "name": "Automa",
+  "minimum_chrome_version": "116",
   "action": {
   "action": {
     "default_popup": "popup.html",
     "default_popup": "popup.html",
     "default_icon": "icon-128.png"
     "default_icon": "icon-128.png"

+ 2 - 15
src/newtab/App.vue

@@ -89,8 +89,7 @@ import { useSharedWorkflowStore } from '@/stores/sharedWorkflow';
 import { loadLocaleMessages, setI18nLanguage } from '@/lib/vueI18n';
 import { loadLocaleMessages, setI18nLanguage } from '@/lib/vueI18n';
 import { getUserWorkflows } from '@/utils/api';
 import { getUserWorkflows } from '@/utils/api';
 import { getWorkflowPermissions } from '@/utils/workflowData';
 import { getWorkflowPermissions } from '@/utils/workflowData';
-import { sendMessage } from '@/utils/message';
-import { workflowState, startWorkflowExec } from '@/workflowEngine';
+import { MessageListener } from '@/utils/message';
 import emitter from '@/lib/mitt';
 import emitter from '@/lib/mitt';
 import automa from '@business';
 import automa from '@business';
 import dbLogs from '@/db/logs';
 import dbLogs from '@/db/logs';
@@ -249,9 +248,6 @@ const messageEvents = {
         });
         });
     }
     }
   },
   },
-  'workflow:execute': function ({ data, options }) {
-    startWorkflowExec(data, options ?? data?.options ?? {});
-  },
   'recording:stop': stopRecording,
   'recording:stop': stopRecording,
   'background--recording:stop': stopRecording,
   'background--recording:stop': stopRecording,
 };
 };
@@ -302,7 +298,7 @@ window.addEventListener('message', ({ data }) => {
     );
     );
   };
   };
 
 
-  sendMessage('fetch', data.data, 'background')
+  MessageListener.sendMessage('fetch', data.data, 'background')
     .then((result) => {
     .then((result) => {
       sendResponse({ isError: false, result });
       sendResponse({ isError: false, result });
     })
     })
@@ -327,15 +323,6 @@ watch(
 
 
 (async () => {
 (async () => {
   try {
   try {
-    workflowState.storage = {
-      get() {
-        return workflowStore.popupStates;
-      },
-      set(key, value) {
-        workflowStore.popupStates = Object.values(value);
-      },
-    };
-
     const { workflowStates } = await browser.storage.local.get(
     const { workflowStates } = await browser.storage.local.get(
       'workflowStates'
       'workflowStates'
     );
     );

+ 2 - 2
src/newtab/pages/workflows/Host.vue

@@ -115,7 +115,7 @@ import { useGroupTooltip } from '@/composable/groupTooltip';
 import { findTriggerBlock } from '@/utils/helper';
 import { findTriggerBlock } from '@/utils/helper';
 import convertWorkflowData from '@/utils/convertWorkflowData';
 import convertWorkflowData from '@/utils/convertWorkflowData';
 import { useWorkflowStore } from '@/stores/workflow';
 import { useWorkflowStore } from '@/stores/workflow';
-import { executeWorkflow } from '@/workflowEngine';
+import RendererWorkflowService from '@/service/renderer/RendererWorkflowService';
 import { useHostedWorkflowStore } from '@/stores/hostedWorkflow';
 import { useHostedWorkflowStore } from '@/stores/hostedWorkflow';
 import getTriggerText from '@/utils/triggerText';
 import getTriggerText from '@/utils/triggerText';
 import WorkflowEditor from '@/components/newtab/workflow/WorkflowEditor.vue';
 import WorkflowEditor from '@/components/newtab/workflow/WorkflowEditor.vue';
@@ -213,7 +213,7 @@ function executeCurrWorkflow() {
     id: workflowId,
     id: workflowId,
   };
   };
 
 
-  executeWorkflow(payload);
+  RendererWorkflowService.executeWorkflow(payload);
 }
 }
 async function retrieveTriggerText() {
 async function retrieveTriggerText() {
   const triggerBlock = findTriggerBlock(workflow.value.drawflow);
   const triggerBlock = findTriggerBlock(workflow.value.drawflow);

+ 2 - 2
src/newtab/pages/workflows/[id].vue

@@ -323,7 +323,7 @@ import { excludeGroupBlocks } from '@/utils/shared';
 import { useGroupTooltip } from '@/composable/groupTooltip';
 import { useGroupTooltip } from '@/composable/groupTooltip';
 import { useCommandManager } from '@/composable/commandManager';
 import { useCommandManager } from '@/composable/commandManager';
 import { debounce, parseJSON, throttle, getActiveTab } from '@/utils/helper';
 import { debounce, parseJSON, throttle, getActiveTab } from '@/utils/helper';
-import { executeWorkflow } from '@/workflowEngine';
+import RendererWorkflowService from '@/service/renderer/RendererWorkflowService';
 import { registerWorkflowTrigger } from '@/utils/workflowTrigger';
 import { registerWorkflowTrigger } from '@/utils/workflowTrigger';
 import emitter from '@/lib/mitt';
 import emitter from '@/lib/mitt';
 import functions from '@/workflowEngine/templating/templatingFunctions';
 import functions from '@/workflowEngine/templating/templatingFunctions';
@@ -705,7 +705,7 @@ async function executeFromBlock(blockId) {
       workflowOptions.tabId = tab.id;
       workflowOptions.tabId = tab.id;
     }
     }
 
 
-    executeWorkflow(workflow.value, workflowOptions);
+    RendererWorkflowService.executeWorkflow(workflow.value, workflowOptions);
   } catch (error) {
   } catch (error) {
     console.error(error);
     console.error(error);
   }
   }

+ 1 - 0
src/offscreen/index.js

@@ -0,0 +1 @@
+import './message-listener';

+ 22 - 0
src/offscreen/message-listener.js

@@ -0,0 +1,22 @@
+import { MessageListener } from '@/utils/message';
+import WorkflowManager from '@/workflowEngine/WorkflowManager';
+import { runtime } from 'webextension-polyfill';
+
+const messageListener = new MessageListener('offscreen');
+runtime.onMessage.addListener(messageListener.listener);
+
+messageListener.on('workflow:execute', (data) => {
+  WorkflowManager.instance.execute(data);
+});
+
+messageListener.on('workflow:stop', (stateId) => {
+  WorkflowManager.instance.stopExecution(stateId);
+});
+
+messageListener.on('workflow:resume', ({ id, nextBlock }) => {
+  WorkflowManager.instance.resumeExecution(id, nextBlock);
+});
+
+messageListener.on('workflow:update', ({ id, data }) => {
+  WorkflowManager.instance.updateExecution(id, data);
+});

+ 31 - 0
src/service/renderer/RendererWorkflowService.js

@@ -0,0 +1,31 @@
+import { MessageListener } from '@/utils/message';
+import { toRaw } from 'vue';
+
+class RendererWorkflowService {
+  static executeWorkflow(workflowData, options) {
+    /**
+     * Convert Vue-created proxy into plain object.
+     * It will throw error if there a proxy inside the object.
+     */
+    const clonedWorkflowData = {};
+    Object.keys(workflowData).forEach((key) => {
+      clonedWorkflowData[key] = toRaw(workflowData[key]);
+    });
+
+    return MessageListener.sendMessage(
+      'workflow:execute',
+      { ...workflowData, options },
+      'background'
+    );
+  }
+
+  static stopWorkflowExecution(executionId) {
+    return MessageListener.sendMessage(
+      'workflow:stop',
+      executionId,
+      'background'
+    );
+  }
+}
+
+export default RendererWorkflowService;

+ 36 - 16
src/utils/message.js

@@ -4,10 +4,35 @@ import { objectHasKey } from './helper';
 const nameBuilder = (prefix, name) => (prefix ? `${prefix}--${name}` : name);
 const nameBuilder = (prefix, name) => (prefix ? `${prefix}--${name}` : name);
 const isFirefox = BROWSER_TYPE === 'firefox';
 const isFirefox = BROWSER_TYPE === 'firefox';
 
 
+/**
+ *
+ * @param {string=} name
+ * @param {*=} data
+ * @param {string=} prefix
+ *
+ * @returns {Promise<*>}
+ */
+export function sendMessage(name = '', data = {}, prefix = '') {
+  let payload = {
+    name: nameBuilder(prefix, name),
+    data,
+  };
+
+  if (isFirefox) {
+    payload = JSON.stringify(payload);
+  }
+
+  return browser.runtime.sendMessage(payload);
+}
+
 export class MessageListener {
 export class MessageListener {
+  static sendMessage = sendMessage;
+
   constructor(prefix = '') {
   constructor(prefix = '') {
     this.listeners = {};
     this.listeners = {};
     this.prefix = prefix;
     this.prefix = prefix;
+
+    this.listener = this.listener.bind(this);
   }
   }
 
 
   on(name, listener) {
   on(name, listener) {
@@ -21,11 +46,7 @@ export class MessageListener {
     return this.on;
     return this.on;
   }
   }
 
 
-  listener() {
-    return this.listen.bind(this);
-  }
-
-  listen(message, sender) {
+  listener(message, sender) {
     try {
     try {
       if (isFirefox) message = JSON.parse(message);
       if (isFirefox) message = JSON.parse(message);
 
 
@@ -39,6 +60,7 @@ export class MessageListener {
       if (!(response instanceof Promise)) {
       if (!(response instanceof Promise)) {
         return Promise.resolve(response);
         return Promise.resolve(response);
       }
       }
+
       return response;
       return response;
     } catch (err) {
     } catch (err) {
       return Promise.reject(
       return Promise.reject(
@@ -46,17 +68,15 @@ export class MessageListener {
       );
       );
     }
     }
   }
   }
-}
-
-export function sendMessage(name = '', data = {}, prefix = '') {
-  let payload = {
-    name: nameBuilder(prefix, name),
-    data,
-  };
 
 
-  if (isFirefox) {
-    payload = JSON.stringify(payload);
+  /**
+   *
+   * @param {string} name
+   * @param {*} data
+   *
+   * @returns {Promise<*>}
+   */
+  sendMessage(name, data) {
+    return sendMessage(name, data, this.prefix);
   }
   }
-
-  return browser.runtime.sendMessage(payload);
 }
 }

+ 166 - 0
src/workflowEngine/WorkflowManager.js

@@ -0,0 +1,166 @@
+import browser from 'webextension-polyfill';
+import { fetchApi } from '@/utils/api';
+import getBlockMessage from '@/utils/getBlockMessage';
+import convertWorkflowData from '@/utils/convertWorkflowData';
+import dayjs from '@/lib/dayjs';
+import WorkflowEvent from './WorkflowEvent';
+import WorkflowState from './WorkflowState';
+import WorkflowLogger from './WorkflowLogger';
+import WorkflowEngine from './WorkflowEngine';
+import blocksHandler from './blocksHandler';
+
+const workflowStateStorage = {
+  get() {
+    return browser.storage.local
+      .get('workflowStates')
+      .then(({ workflowStates }) => workflowStates || []);
+  },
+  set(key, value) {
+    const states = Object.values(value);
+
+    return browser.storage.local.set({ workflowStates: states });
+  },
+};
+
+class WorkflowManager {
+  /** @type {WorkflowManager} */
+  static #_instance;
+
+  /**
+   * WorkflowManager singleton
+   * @type {WorkflowManager}
+   */
+  static get instance() {
+    if (!this.#_instance) this.#_instance = new WorkflowManager();
+
+    return this.#_instance;
+  }
+
+  /** @type {WorkflowState} */
+  #state;
+
+  /** @type {WorkflowLogger} */
+  #logger;
+
+  constructor() {
+    this.#logger = new WorkflowLogger();
+    this.#state = new WorkflowState({ storage: workflowStateStorage });
+  }
+
+  execute(workflowData, options) {
+    if (workflowData.testingMode) {
+      for (const value of this.#state.states.values()) {
+        if (value.workflowId === workflowData.id) return null;
+      }
+    }
+
+    const convertedWorkflow = convertWorkflowData(workflowData);
+    const engine = new WorkflowEngine(convertedWorkflow, {
+      options,
+      states: this.#state,
+      logger: this.#logger,
+      blocksHandler: blocksHandler(),
+    });
+
+    engine.init();
+    engine.on('destroyed', ({ id, status, history, blockDetail, ...rest }) => {
+      if (status !== 'stopped') {
+        browser.permissions
+          .contains({ permissions: ['notifications'] })
+          .then((hasPermission) => {
+            if (!hasPermission || !workflowData.settings.notification) return;
+
+            const name = workflowData.name.slice(0, 32);
+
+            browser.notifications.create(`logs:${id}`, {
+              type: 'basic',
+              iconUrl: browser.runtime.getURL('icon-128.png'),
+              title: status === 'success' ? 'Success' : 'Error',
+              message: `${
+                status === 'success' ? 'Successfully' : 'Failed'
+              } to run the "${name}" workflow`,
+            });
+          });
+      }
+
+      if (convertedWorkflow.settings?.events) {
+        const workflowHistory = history.map((item) => {
+          delete item.logId;
+          delete item.prevBlockData;
+          delete item.workerId;
+
+          item.description = item.description || '';
+
+          return item;
+        });
+        const workflowRefData = {
+          status,
+          startedAt: rest.startedTimestamp,
+          endedAt: rest.endedTimestamp
+            ? rest.endedTimestamp - rest.startedTimestamp
+            : null,
+          logs: workflowHistory,
+          errorMessage:
+            status === 'error' ? getBlockMessage(blockDetail) : null,
+        };
+
+        convertedWorkflow.settings.events.forEach((event) => {
+          if (status === 'success' && !event.events.includes('finish:success'))
+            return;
+          if (status === 'error' && !event.events.includes('finish:failed'))
+            return;
+
+          WorkflowEvent.handle(event.action, {
+            workflow: workflowRefData,
+            variables: { ...engine.referenceData.variables },
+            globalData: { ...engine.referenceData.globalData },
+          });
+        });
+      }
+    });
+
+    browser.storage.local.get('checkStatus').then(({ checkStatus }) => {
+      const isSameDay = dayjs().isSame(checkStatus, 'day');
+      if (!isSameDay || !checkStatus) {
+        fetchApi('/status')
+          .then((response) => response.json())
+          .then(() => {
+            browser.storage.local.set({ checkStatus: new Date().toString() });
+          });
+      }
+    });
+
+    return engine;
+  }
+
+  /**
+   * Stop workflow execution
+   * @param {string} stateId
+   * @returns {Promise<void>}
+   */
+  stopExecution(stateId) {
+    return this.#state.stop(stateId);
+  }
+
+  /**
+   * Resume workflow execution
+   * @param {string} id
+   * @param {object} nextBlock
+   * @returns {Promise<void>}
+   */
+  resumeExecution(id, nextBlock) {
+    return this.#state.resume(id, nextBlock);
+  }
+
+  /**
+   * Resume workflow execution
+   * @param {string} id
+   * @param {object} stateData
+   * @returns {Promise<void>}
+   */
+  updateExecution(id, stateData) {
+    return this.#state.update(id, stateData);
+  }
+}
+
+export default WorkflowManager;

+ 2 - 2
src/workflowEngine/blocksHandler/handlerLogData.js

@@ -1,5 +1,4 @@
 import getTranslateLog from '@/utils/getTranslateLog';
 import getTranslateLog from '@/utils/getTranslateLog';
-import { workflowState } from '../index';
 
 
 export async function logData({ id, data }) {
 export async function logData({ id, data }) {
   if (!data.workflowId) {
   if (!data.workflowId) {
@@ -7,7 +6,8 @@ export async function logData({ id, data }) {
   }
   }
 
 
   // 工作流状态数组
   // 工作流状态数组
-  const { states } = workflowState;
+  // block handler is inside WorkflowWorker scope. See WorkflowWorker.js:343
+  const { states } = this.engine.states;
   let logs = [];
   let logs = [];
   if (states) {
   if (states) {
     // 转换为数组
     // 转换为数组

+ 0 - 161
src/workflowEngine/index.js

@@ -1,161 +0,0 @@
-/* eslint-disable no-restricted-globals */
-import { toRaw } from 'vue';
-import browser from 'webextension-polyfill';
-import dayjs from '@/lib/dayjs';
-import { parseJSON } from '@/utils/helper';
-import { fetchApi } from '@/utils/api';
-import { sendMessage } from '@/utils/message';
-import convertWorkflowData from '@/utils/convertWorkflowData';
-import getBlockMessage from '@/utils/getBlockMessage';
-import WorkflowState from './WorkflowState';
-import WorkflowLogger from './WorkflowLogger';
-import WorkflowEngine from './WorkflowEngine';
-import blocksHandler from './blocksHandler';
-import { workflowEventHandler } from './workflowEvent';
-
-const workflowStateStorage = {
-  get() {
-    return browser.storage.local
-      .get('workflowStates')
-      .then(({ workflowStates }) => workflowStates || []);
-  },
-  set(key, value) {
-    const states = Object.values(value);
-
-    return browser.storage.local.set({ workflowStates: states });
-  },
-};
-
-export const workflowLogger = new WorkflowLogger();
-export const workflowState = new WorkflowState({
-  storage: workflowStateStorage,
-});
-
-export function stopWorkflowExec(executionId) {
-  workflowState.stop(executionId);
-  sendMessage('workflow:stop', executionId, 'background');
-}
-
-export function startWorkflowExec(workflowData, options, isPopup = true) {
-  if (self.localStorage) {
-    const runCounts =
-      parseJSON(self.localStorage.getItem('runCounts'), {}) || {};
-    runCounts[workflowData.id] = (runCounts[workflowData.id] || 0) + 1;
-
-    self.localStorage.setItem('runCounts', JSON.stringify(runCounts));
-  }
-
-  if (workflowData.testingMode) {
-    for (const value of workflowState.states.values()) {
-      if (value.workflowId === workflowData.id) return null;
-    }
-  }
-
-  const clonedWorkflowData = {};
-  Object.keys(workflowData).forEach((key) => {
-    clonedWorkflowData[key] = toRaw(workflowData[key]);
-  });
-
-  const convertedWorkflow = convertWorkflowData(clonedWorkflowData);
-  const engine = new WorkflowEngine(convertedWorkflow, {
-    options,
-    isPopup,
-    states: workflowState,
-    logger: workflowLogger,
-    blocksHandler: blocksHandler(),
-  });
-
-  engine.init();
-  engine.on('destroyed', ({ id, status, history, blockDetail, ...rest }) => {
-    if (status !== 'stopped') {
-      browser.permissions
-        .contains({ permissions: ['notifications'] })
-        .then((hasPermission) => {
-          if (!hasPermission || !clonedWorkflowData.settings.notification)
-            return;
-
-          const name = clonedWorkflowData.name.slice(0, 32);
-
-          browser.notifications.create(`logs:${id}`, {
-            type: 'basic',
-            iconUrl: browser.runtime.getURL('icon-128.png'),
-            title: status === 'success' ? 'Success' : 'Error',
-            message: `${
-              status === 'success' ? 'Successfully' : 'Failed'
-            } to run the "${name}" workflow`,
-          });
-        });
-    }
-
-    if (convertedWorkflow.settings?.events) {
-      const workflowHistory = history.map((item) => {
-        delete item.logId;
-        delete item.prevBlockData;
-        delete item.workerId;
-
-        item.description = item.description || '';
-
-        return item;
-      });
-      const workflowRefData = {
-        status,
-        startedAt: rest.startedTimestamp,
-        endedAt: rest.endedTimestamp
-          ? rest.endedTimestamp - rest.startedTimestamp
-          : null,
-        logs: workflowHistory,
-        errorMessage: status === 'error' ? getBlockMessage(blockDetail) : null,
-      };
-
-      convertedWorkflow.settings.events.forEach((event) => {
-        if (status === 'success' && !event.events.includes('finish:success'))
-          return;
-        if (status === 'error' && !event.events.includes('finish:failed'))
-          return;
-
-        workflowEventHandler(event.action, {
-          workflow: workflowRefData,
-          variables: { ...engine.referenceData.variables },
-          globalData: { ...engine.referenceData.globalData },
-        });
-      });
-    }
-  });
-
-  browser.storage.local.get('checkStatus').then(({ checkStatus }) => {
-    const isSameDay = dayjs().isSame(checkStatus, 'day');
-    if (!isSameDay || !checkStatus) {
-      fetchApi('/status')
-        .then((response) => response.json())
-        .then(() => {
-          browser.storage.local.set({ checkStatus: new Date().toString() });
-        });
-    }
-  });
-
-  return engine;
-}
-
-export function executeWorkflow(workflowData, options) {
-  if (!workflowData || workflowData.isDisabled) return;
-
-  const isMV2 = browser.runtime.getManifest().manifest_version === 2;
-  const context = workflowData?.settings?.execContext;
-
-  if (isMV2 || context === 'background') {
-    sendMessage('workflow:execute', { ...workflowData, options }, 'background');
-    return;
-  }
-
-  if (window) window.fromBackground = false;
-
-  browser.tabs
-    .query({ active: true, currentWindow: true })
-    .then(async ([tab]) => {
-      if (tab && tab.url.includes(browser.runtime.getURL(''))) {
-        await browser.windows.update(tab.windowId, { focused: false });
-      }
-
-      startWorkflowExec(workflowData, options);
-    });
-}

+ 2 - 4
src/workflowEngine/templating/renderString.js

@@ -1,11 +1,9 @@
-import browser from 'webextension-polyfill';
 import { messageSandbox } from '../helper';
 import { messageSandbox } from '../helper';
 import mustacheReplacer from './mustacheReplacer';
 import mustacheReplacer from './mustacheReplacer';
 
 
 const isFirefox = BROWSER_TYPE === 'firefox';
 const isFirefox = BROWSER_TYPE === 'firefox';
-const isMV2 = browser.runtime.getManifest().manifest_version === 2;
 
 
-export default async function (str, data, isPopup = true, options = {}) {
+export default async function (str, data, options = {}) {
   if (!str || typeof str !== 'string') return '';
   if (!str || typeof str !== 'string') return '';
 
 
   const hasMustacheTag = /\{\{(.*?)\}\}/.test(str);
   const hasMustacheTag = /\{\{(.*?)\}\}/.test(str);
@@ -19,7 +17,7 @@ export default async function (str, data, isPopup = true, options = {}) {
   let renderedValue = {};
   let renderedValue = {};
   const evaluateJS = str.startsWith('!!');
   const evaluateJS = str.startsWith('!!');
 
 
-  if (evaluateJS && !isFirefox && (isMV2 || isPopup)) {
+  if (evaluateJS && !isFirefox) {
     const refKeysRegex =
     const refKeysRegex =
       /(variables|table|secrets|loopData|workflow|googleSheets|globalData)@/g;
       /(variables|table|secrets|loopData|workflow|googleSheets|globalData)@/g;
     const strToRender = str.replace(refKeysRegex, '$1.');
     const strToRender = str.replace(refKeysRegex, '$1.');

+ 44 - 41
src/workflowEngine/workflowEvent.js

@@ -2,48 +2,51 @@ import { nanoid } from 'nanoid';
 import { messageSandbox } from './helper';
 import { messageSandbox } from './helper';
 import renderString from './templating/renderString';
 import renderString from './templating/renderString';
 
 
-async function javascriptCode(event, refData) {
-  const instanceId = `automa${nanoid()}`;
-
-  await messageSandbox('javascriptBlock', {
-    refData,
-    instanceId,
-    preloadScripts: [],
-    blockData: {
-      code: event.code,
-    },
-  });
-}
-
-async function httpRequest({ url, method, headers, body }, refData) {
-  if (!url.trim()) return;
-
-  const reqHeaders = {
-    'Content-Type': 'application/json',
-  };
-  headers.forEach((header) => {
-    reqHeaders[header.name] = header.value;
-  });
-
-  const renderedBody =
-    method !== 'GET' ? (await renderString(body, refData)).value : undefined;
-
-  await fetch(url, {
-    method,
-    body: renderedBody,
-    headers: reqHeaders,
-  });
-}
+class WorkflowEvent {
+  static async #httpRequest({ url, method, headers, body }, refData) {
+    if (!url.trim()) return;
+
+    const reqHeaders = {
+      'Content-Type': 'application/json',
+    };
+    headers.forEach((header) => {
+      reqHeaders[header.name] = header.value;
+    });
+
+    const renderedBody =
+      method !== 'GET' ? (await renderString(body, refData)).value : undefined;
+
+    await fetch(url, {
+      method,
+      body: renderedBody,
+      headers: reqHeaders,
+    });
+  }
 
 
-const eventHandlerMap = {
-  'js-code': javascriptCode,
-  'http-request': httpRequest,
-};
+  static async #javascriptCode(event, refData) {
+    const instanceId = `automa${nanoid()}`;
+
+    await messageSandbox('javascriptBlock', {
+      refData,
+      instanceId,
+      preloadScripts: [],
+      blockData: {
+        code: event.code,
+      },
+    });
+  }
 
 
-export async function workflowEventHandler(event, refData) {
-  try {
-    await eventHandlerMap[event.type](event, refData);
-  } catch (error) {
-    console.error(error);
+  static async handle(event, refData) {
+    switch (event.type) {
+      case 'http-request':
+        await this.#httpRequest(event, refData);
+        break;
+      case 'js-code':
+        await this.#javascriptCode(event, refData);
+        break;
+      default:
+    }
   }
   }
 }
 }
+
+export default WorkflowEvent;

+ 7 - 0
webpack.config.js

@@ -47,6 +47,7 @@ const options = {
     params: path.join(__dirname, 'src', 'params', 'index.js'),
     params: path.join(__dirname, 'src', 'params', 'index.js'),
     background: path.join(__dirname, 'src', 'background', 'index.js'),
     background: path.join(__dirname, 'src', 'background', 'index.js'),
     contentScript: path.join(__dirname, 'src', 'content', 'index.js'),
     contentScript: path.join(__dirname, 'src', 'content', 'index.js'),
+    offscreen: path.join(__dirname, 'src', 'offscreen', 'index.js'),
     recordWorkflow: path.join(
     recordWorkflow: path.join(
       __dirname,
       __dirname,
       'src',
       'src',
@@ -219,6 +220,12 @@ const options = {
       chunks: ['params'],
       chunks: ['params'],
       cache: false,
       cache: false,
     }),
     }),
+    new HtmlWebpackPlugin({
+      template: path.join(__dirname, 'src', 'offscreen', 'index.html'),
+      filename: 'offscreen.html',
+      chunks: ['offscreen'],
+      cache: false,
+    }),
     new webpack.DefinePlugin({
     new webpack.DefinePlugin({
       __VUE_OPTIONS_API__: true,
       __VUE_OPTIONS_API__: true,
       __VUE_PROD_DEVTOOLS__: false,
       __VUE_PROD_DEVTOOLS__: false,

+ 25 - 0
yarn.lock

@@ -1576,6 +1576,14 @@
   dependencies:
   dependencies:
     "@types/node" "*"
     "@types/node" "*"
 
 
+"@types/chrome@^0.0.267":
+  version "0.0.267"
+  resolved "https://registry.yarnpkg.com/@types/chrome/-/chrome-0.0.267.tgz#eefb7d2085993437d8109eb495431dfc0e3dbda1"
+  integrity sha512-vnCWPpYjazSPRMNmybRH+0q4f738F+Pbbls4ZPFsPr9/4TTNJyK1OLZDpSnghnEWb4stfmIUtq/GegnlfD4sPA==
+  dependencies:
+    "@types/filesystem" "*"
+    "@types/har-format" "*"
+
 "@types/connect-history-api-fallback@^1.3.5":
 "@types/connect-history-api-fallback@^1.3.5":
   version "1.5.0"
   version "1.5.0"
   resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz#9fd20b3974bdc2bcd4ac6567e2e0f6885cb2cf41"
   resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz#9fd20b3974bdc2bcd4ac6567e2e0f6885cb2cf41"
@@ -1637,6 +1645,18 @@
     "@types/qs" "*"
     "@types/qs" "*"
     "@types/serve-static" "*"
     "@types/serve-static" "*"
 
 
+"@types/filesystem@*":
+  version "0.0.36"
+  resolved "https://registry.yarnpkg.com/@types/filesystem/-/filesystem-0.0.36.tgz#7227c2d76bfed1b21819db310816c7821d303857"
+  integrity sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==
+  dependencies:
+    "@types/filewriter" "*"
+
+"@types/filewriter@*":
+  version "0.0.33"
+  resolved "https://registry.yarnpkg.com/@types/filewriter/-/filewriter-0.0.33.tgz#d9d611db9d9cd99ae4e458de420eeb64ad604ea8"
+  integrity sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==
+
 "@types/glob@^7.1.1":
 "@types/glob@^7.1.1":
   version "7.2.0"
   version "7.2.0"
   resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb"
   resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb"
@@ -1645,6 +1665,11 @@
     "@types/minimatch" "*"
     "@types/minimatch" "*"
     "@types/node" "*"
     "@types/node" "*"
 
 
+"@types/har-format@*":
+  version "1.2.15"
+  resolved "https://registry.yarnpkg.com/@types/har-format/-/har-format-1.2.15.tgz#f352493638c2f89d706438a19a9eb300b493b506"
+  integrity sha512-RpQH4rXLuvTXKR0zqHq3go0RVXYv/YVqv4TnPH95VbwUxZdQlK1EtcMvQvMpDngHbt13Csh9Z4qT9AbkiQH5BA==
+
 "@types/html-minifier-terser@^6.0.0":
 "@types/html-minifier-terser@^6.0.0":
   version "6.1.0"
   version "6.1.0"
   resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#4fc33a00c1d0c16987b1a20cf92d20614c55ac35"
   resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#4fc33a00c1d0c16987b1a20cf92d20614c55ac35"