Просмотр исходного кода

feat: refactor workflow engine

Ahmad Kholid 3 лет назад
Родитель
Сommit
16e288988d
29 измененных файлов с 637 добавлено и 174 удалено
  1. 1 0
      package.json
  2. 70 81
      src/background/index.js
  3. 57 0
      src/background/message-listener.js
  4. 10 6
      src/background/workflow-engine/blocks-handler/handler-active-tab.js
  5. 5 7
      src/background/workflow-engine/blocks-handler/handler-close-tab.js
  6. 1 1
      src/background/workflow-engine/blocks-handler/handler-export-data.js
  7. 2 2
      src/background/workflow-engine/blocks-handler/handler-forward-page.js
  8. 2 2
      src/background/workflow-engine/blocks-handler/handler-go-back.js
  9. 1 1
      src/background/workflow-engine/blocks-handler/handler-google-sheets.js
  10. 6 6
      src/background/workflow-engine/blocks-handler/handler-interaction-block.js
  11. 1 1
      src/background/workflow-engine/blocks-handler/handler-loop-breakpoint.js
  12. 6 7
      src/background/workflow-engine/blocks-handler/handler-loop-data.js
  13. 15 29
      src/background/workflow-engine/blocks-handler/handler-new-tab.js
  14. 5 5
      src/background/workflow-engine/blocks-handler/handler-switch-to.js
  15. 4 4
      src/background/workflow-engine/blocks-handler/handler-take-screenshot.js
  16. 2 2
      src/background/workflow-engine/blocks-handler/handler-trigger.js
  17. 1 0
      src/background/workflow-engine/execute-content-script.js
  18. 2 2
      src/background/workflow-engine/index.js
  19. 311 0
      src/background/workflow-engine2.js
  20. 16 0
      src/background/workflow-logger.js
  21. 5 1
      src/background/workflow-state.js
  22. 79 0
      src/background/workflow-state2.js
  23. 1 0
      src/content/index.js
  24. 1 1
      src/newtab/App.vue
  25. 1 1
      src/store/index.js
  26. 6 0
      src/utils/helper.js
  27. 7 6
      src/utils/reference-data/index.js
  28. 14 9
      src/utils/reference-data/mustache-replacer.js
  29. 5 0
      yarn.lock

+ 1 - 0
package.json

@@ -29,6 +29,7 @@
     "@vuex-orm/core": "^0.36.4",
     "compare-versions": "^4.1.2",
     "dayjs": "^1.10.7",
+    "defu": "^5.0.0",
     "drawflow": "^0.0.51",
     "idb": "^7.0.0",
     "mousetrap": "^1.6.5",

+ 70 - 81
src/background/index.js

@@ -1,94 +1,87 @@
+/* eslint-disable */
 import browser from 'webextension-polyfill';
 import { objectHasKey } from '@/utils/helper';
 import { MessageListener } from '@/utils/message';
 import { registerSpecificDay } from '../utils/workflow-trigger';
 import workflowState from './workflow-state';
-import workflowEngine from './workflow-engine';
+import WorkflowState from './workflow-state2';
 import CollectionEngine from './collection-engine';
-
-function getWorkflow(workflowId) {
-  return new Promise((resolve) => {
-    browser.storage.local.get('workflows').then(({ workflows }) => {
-      const workflow = workflows.find(({ id }) => id === workflowId);
-
-      resolve(workflow);
-    });
-  });
-}
-
-const runningWorkflows = {};
-const runningCollections = {};
-
-async function executeWorkflow(workflow, tabId) {
-  try {
-    const engine = workflowEngine(workflow, { tabId });
-
-    runningWorkflows[engine.id] = engine;
-
+import WorkflowEngine from './workflow-engine2';
+import blocksHandler from './workflow-engine/blocks-handler';
+import WorkflowLogger from './workflow-logger';
+
+const storage = {
+  async get(key) {
+    try {
+      const result = await browser.storage.local.get(key);
+
+      return result[key];
+    } catch (error) {
+      console.error(error);
+      return [];
+    }
+  },
+  async set(key, value) {
+    await browser.storage.local.set({ [key]: value });
+  }
+};
+const workflow = {
+  async get(workflowId) {
+    const { workflows } = await browser.storage.local.get('workflows');
+    const workflow = workflows.find(({ id }) => id === workflowId);
+
+    return workflow;
+  },
+  async states() {
+    const states = new WorkflowState({ storage });
+
+    return states;
+  },
+  async logger() {
+    const logger = new WorkflowLogger({ storage });
+
+    return logger;
+  },
+  async execute(workflow, options) {
+    const states = await this.states();
+    const logger = await this.logger();
+
+    const engine = new WorkflowEngine(workflow, { ...options, states, blocksHandler, logger });
     engine.init();
-    engine.on('destroyed', ({ id }) => {
-      delete runningWorkflows[id];
-    });
 
-    return true;
-  } catch (error) {
-    console.error(error);
-    return error;
+    console.log(engine);
+
+    return engine;
   }
-}
-function executeCollection(collection) {
-  const engine = new CollectionEngine(collection);
+};
 
-  runningCollections[engine.id] = engine;
+async function checkVisitWebTriggers(states, tab) {
+  const visitWebTriggers = await storage.get('visitWebTriggers');
+  const triggeredWorkflow = visitWebTriggers.find(({ url, isRegex }) => {
+    if (url.trim() === '') return false;
 
-  engine.init();
-  engine.on('destroyed', (id) => {
-    delete runningWorkflows[id];
+    return tab.url.match(isRegex ? new RegExp(url, 'g') : url);
   });
 
-  return true;
-}
-async function checkRunnigWorkflows() {
-  try {
-    const result = await browser.storage.local.get('workflowState');
+  if (triggeredWorkflow) {
+    const workflowData = await workflow.get(triggeredWorkflow.id);
 
-    result.workflowState.forEach(({ id }, index) => {
-      if (objectHasKey(runningWorkflows, id)) return;
-
-      result.workflowState.splice(index, 1);
-    });
-
-    browser.storage.local.set({ workflowState: result.workflowState });
-  } catch (error) {
-    console.error(error);
+    if (workflowData) workflow.execute(workflowData);
   }
 }
-checkRunnigWorkflows();
-
+async function checkWorkflowStates() {
+  /* check if tab is reloaded */
+}
 browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
   if (changeInfo.status === 'complete') {
-    const { visitWebTriggers } = await browser.storage.local.get(
-      'visitWebTriggers'
-    );
-    const trigger = visitWebTriggers.find(({ url, isRegex }) => {
-      if (url.trim() === '') return false;
-
-      return tab.url.match(isRegex ? new RegExp(url, 'g') : url);
-    });
-    const state = await workflowState.get((item) => item.state.tabId === tabId);
-
-    if (trigger && state.length === 0) {
-      const workflow = await getWorkflow(trigger.id);
-
-      if (workflow) executeWorkflow(workflow, tabId);
-    }
+    await checkVisitWebTriggers(null, tab);
   }
 });
 browser.alarms.onAlarm.addListener(({ name }) => {
-  getWorkflow(name).then((workflow) => {
+  workflow.get(name).then((workflow) => {
     if (!workflow) return;
 
-    executeWorkflow(workflow);
+    workflow.execute(workflow);
 
     const triggerBlock = Object.values(
       JSON.parse(workflow.drawflow).drawflow.Home.data
@@ -108,7 +101,7 @@ chrome.runtime.onInstalled.addListener((details) => {
         shortcuts: {},
         workflows: [],
         collections: [],
-        workflowState: [],
+        workflowState: {},
         isFirstTime: true,
         visitWebTriggers: [],
       })
@@ -157,7 +150,7 @@ message.on('get:sender', (_, sender) => {
   return sender;
 });
 
-message.on('collection:execute', executeCollection);
+// message.on('collection:execute', executeCollection);
 message.on('collection:stop', (id) => {
   const collection = runningCollections[id];
   if (!collection) {
@@ -168,17 +161,13 @@ message.on('collection:stop', (id) => {
   collection.stop();
 });
 
-message.on('workflow:check-state', checkRunnigWorkflows);
-message.on('workflow:execute', (workflow) => executeWorkflow(workflow));
-message.on('workflow:stop', (id) => {
-  const workflow = runningWorkflows[id];
-
-  if (!workflow) {
-    workflowState.delete(id);
-    return;
-  }
-
-  workflow.stop();
+// message.on('workflow:check-state', checkRunnigWorkflows);
+message.on('workflow:execute', (param) => {
+  workflow.execute(param);
+});
+message.on('workflow:stop', async (id) => {
+  const states = await workflow.states();
+  await states.update(id, { isDestroyed: true });
 });
 
 browser.runtime.onMessage.addListener(message.listener());

+ 57 - 0
src/background/message-listener.js

@@ -0,0 +1,57 @@
+export default function () {
+  const message = new MessageListener('background');
+
+  message.on('fetch:text', (url) => {
+    return fetch(url).then((response) => response.text());
+  });
+  message.on('open:dashboard', async (url) => {
+    const tabOptions = {
+      active: true,
+      url: browser.runtime.getURL(
+        `/newtab.html#${typeof url === 'string' ? url : ''}`
+      ),
+    };
+
+    try {
+      const [tab] = await browser.tabs.query({
+        url: browser.runtime.getURL('/newtab.html'),
+      });
+
+      if (tab) {
+        await browser.tabs.update(tab.id, tabOptions);
+        await browser.tabs.reload(tab.id);
+      } else {
+        browser.tabs.create(tabOptions);
+      }
+    } catch (error) {
+      console.error(error);
+    }
+  });
+  message.on('get:sender', (_, sender) => {
+    return sender;
+  });
+
+  message.on('collection:execute', executeCollection);
+  message.on('collection:stop', (id) => {
+    const collection = runningCollections[id];
+    if (!collection) {
+      workflowState.delete(id);
+      return;
+    }
+
+    collection.stop();
+  });
+
+  message.on('workflow:check-state', checkRunnigWorkflows);
+  message.on('workflow:execute', (workflow) => executeWorkflow(workflow));
+  message.on('workflow:stop', (id) => {
+    const workflow = runningWorkflows[id];
+
+    if (!workflow) {
+      workflowState.delete(id);
+      return;
+    }
+
+    workflow.stop();
+  });
+}

+ 10 - 6
src/background/workflow-engine/blocks-handler/handler-active-tab.js

@@ -11,8 +11,8 @@ async function activeTab(block) {
       data: '',
     };
 
-    if (this.tabId) {
-      await browser.tabs.update(this.tabId, { active: true });
+    if (this.activeTab.id) {
+      await browser.tabs.update(this.activeTab.id, { active: true });
 
       return data;
     }
@@ -29,11 +29,15 @@ async function activeTab(block) {
       throw error;
     }
 
-    this.frames = await executeContentScript(tab.id);
+    const frames = await executeContentScript(tab.id);
 
-    this.frameId = 0;
-    this.tabId = tab.id;
-    this.activeTabUrl = tab.url;
+    this.activeTab = {
+      ...this.activeTab,
+      frames,
+      frameId: 0,
+      id: tab.id,
+      url: tab.url,
+    };
     this.windowId = tab.windowId;
 
     return data;

+ 5 - 7
src/background/workflow-engine/blocks-handler/handler-close-tab.js

@@ -1,15 +1,14 @@
 import browser from 'webextension-polyfill';
 import { getBlockConnection } from '../helper';
 
-async function closeTab(block) {
-  const nextBlockId = getBlockConnection(block);
+async function closeTab({ data, outputs }) {
+  const nextBlockId = getBlockConnection({ outputs });
 
   try {
-    const { data } = block;
     let tabIds;
 
-    if (data.activeTab && this.tabId) {
-      tabIds = this.tabId;
+    if (data.activeTab && this.activeTab.id) {
+      tabIds = this.activeTab.id;
     } else if (data.url) {
       tabIds = (await browser.tabs.query({ url: data.url })).map(
         (tab) => tab.id
@@ -23,8 +22,7 @@ async function closeTab(block) {
       data: '',
     };
   } catch (error) {
-    const errorInstance = typeof error === 'string' ? new Error(error) : error;
-    errorInstance.nextBlockId = nextBlockId;
+    error.nextBlockId = nextBlockId;
 
     throw error;
   }

+ 1 - 1
src/background/workflow-engine/blocks-handler/handler-export-data.js

@@ -3,7 +3,7 @@ import dataExporter from '@/utils/data-exporter';
 
 function exportData(block) {
   return new Promise((resolve) => {
-    dataExporter(this.data, block.data);
+    dataExporter(this.referenceData.dataColumns, block.data);
 
     resolve({
       data: '',

+ 2 - 2
src/background/workflow-engine/blocks-handler/handler-forward-page.js

@@ -5,7 +5,7 @@ function forwardPage(block) {
   return new Promise((resolve, reject) => {
     const nextBlockId = getBlockConnection(block);
 
-    if (!this.tabId) {
+    if (!this.activeTab.id) {
       const error = new Error('no-tab');
       error.nextBlockId = nextBlockId;
 
@@ -15,7 +15,7 @@ function forwardPage(block) {
     }
 
     browser.tabs
-      .goForward(this.tabId)
+      .goForward(this.activeTab.id)
       .then(() => {
         resolve({
           nextBlockId,

+ 2 - 2
src/background/workflow-engine/blocks-handler/handler-go-back.js

@@ -5,7 +5,7 @@ export function goBack(block) {
   return new Promise((resolve, reject) => {
     const nextBlockId = getBlockConnection(block);
 
-    if (!this.tabId) {
+    if (!this.activeTab.id) {
       const error = new Error('no-tab');
       error.nextBlockId = nextBlockId;
 
@@ -15,7 +15,7 @@ export function goBack(block) {
     }
 
     browser.tabs
-      .goBack(this.tabId)
+      .goBack(this.activeTab.id)
       .then(() => {
         resolve({
           nextBlockId,

+ 1 - 1
src/background/workflow-engine/blocks-handler/handler-google-sheets.js

@@ -33,7 +33,7 @@ export default async function ({ data, outputs }) {
       result = spreadsheetValues;
 
       if (data.refKey && !isWhitespace(data.refKey)) {
-        this.googleSheets[data.refKey] = spreadsheetValues;
+        this.referenceData.googleSheets[data.refKey] = spreadsheetValues;
       }
     }
 

+ 6 - 6
src/background/workflow-engine/blocks-handler/handler-interaction-block.js

@@ -11,7 +11,7 @@ async function interactionHandler(block, { refData }) {
 
   try {
     const data = await this._sendMessageToTab(messagePayload, {
-      frameId: this.frameId || 0,
+      frameId: this.activeTab.frameId || 0,
     });
 
     if (block.name === 'link')
@@ -29,20 +29,20 @@ async function interactionHandler(block, { refData }) {
 
       if (Array.isArray(data) && currentColumnType !== 'array') {
         data.forEach((item) => {
-          this.addData(block.data.dataColumn, item);
+          this.addDataToColumn(block.data.dataColumn, item);
           if (objectHasKey(block.data, 'extraRowDataColumn')) {
             if (block.data.addExtraRow)
-              this.addData(
+              this.addDataToColumn(
                 block.data.extraRowDataColumn,
                 block.data.extraRowValue
               );
           }
         });
       } else {
-        this.addData(block.data.dataColumn, data);
+        this.addDataToColumn(block.data.dataColumn, data);
         if (objectHasKey(block.data, 'extraRowDataColumn')) {
           if (block.data.addExtraRow)
-            this.addData(
+            this.addDataToColumn(
               block.data.extraRowDataColumn,
               block.data.extraRowValue
             );
@@ -51,7 +51,7 @@ async function interactionHandler(block, { refData }) {
     } else if (block.name === 'javascript-code') {
       const arrData = Array.isArray(data) ? data : [data];
 
-      this.addData(arrData);
+      this.addDataToColumn(arrData);
     }
 
     return {

+ 1 - 1
src/background/workflow-engine/blocks-handler/handler-loop-breakpoint.js

@@ -20,7 +20,7 @@ function loopBreakpoint(block, { prevBlockData }) {
       });
     } else {
       delete this.loopList[block.data.loopId];
-      delete this.loopData[block.data.loopId];
+      delete this.referenceData.loopData[block.data.loopId];
 
       resolve({
         data: prevBlockData,

+ 6 - 7
src/background/workflow-engine/blocks-handler/handler-loop-data.js

@@ -1,4 +1,3 @@
-import { generateJSON } from '@/utils/data-exporter';
 import { getBlockConnection } from '../helper';
 
 function loopData(block) {
@@ -12,13 +11,13 @@ function loopData(block) {
       let currentLoopData;
 
       if (data.loopThrough === 'numbers') {
-        currentLoopData = this.loopData[data.loopId] + 1;
+        currentLoopData = this.referenceData.loopData[data.loopId] + 1;
       } else {
         currentLoopData =
           this.loopList[data.loopId].data[this.loopList[data.loopId].index];
       }
 
-      this.loopData[data.loopId] = currentLoopData;
+      this.referenceData.loopData[data.loopId] = currentLoopData;
     } else {
       let currLoopData;
 
@@ -27,10 +26,10 @@ function loopData(block) {
           currLoopData = data.fromNumber;
           break;
         case 'data-columns':
-          currLoopData = generateJSON(Object.keys(this.data), this.data);
+          currLoopData = this.referenceData.dataColumns;
           break;
         case 'google-sheets':
-          currLoopData = this.googleSheets[data.referenceKey];
+          currLoopData = this.referenceData.googleSheets[data.referenceKey];
           break;
         case 'custom-data':
           currLoopData = JSON.parse(data.loopData);
@@ -58,12 +57,12 @@ function loopData(block) {
             : data.maxLoop || currLoopData.length,
       };
       /* eslint-disable-next-line */
-      this.loopData[data.loopId] = data.loopThrough === 'numbers' ? data.fromNumber : currLoopData[0];
+      this.referenceData.loopData[data.loopId] = data.loopThrough === 'numbers' ? data.fromNumber : currLoopData[0];
     }
 
     resolve({
       nextBlockId,
-      data: this.loopData[data.loopId],
+      data: this.referenceData.loopData[data.loopId],
     });
   });
 }

+ 15 - 29
src/background/workflow-engine/blocks-handler/handler-new-tab.js

@@ -2,24 +2,6 @@ import browser from 'webextension-polyfill';
 import { getBlockConnection } from '../helper';
 import executeContentScript from '../execute-content-script';
 
-function tabUpdatedListener(tab) {
-  return new Promise((resolve) => {
-    this._listener({
-      name: 'tab-updated',
-      id: tab.id,
-      callback: async (tabId, changeInfo, deleteListener) => {
-        if (changeInfo.status !== 'complete') return;
-
-        const frames = await executeContentScript(tabId);
-
-        deleteListener();
-
-        resolve(frames);
-      },
-    });
-  });
-}
-
 async function newTab(block) {
   if (this.windowId) {
     try {
@@ -29,11 +11,13 @@ async function newTab(block) {
     }
   }
 
+  const nextBlockId = getBlockConnection(block);
+
   try {
     const { updatePrevTab, url, active, inGroup } = block.data;
 
-    if (updatePrevTab && this.tabId) {
-      await browser.tabs.update(this.tabId, { url, active });
+    if (updatePrevTab && this.activeTab.id) {
+      await browser.tabs.update(this.activeTab.id, { url, active });
     } else {
       const tab = await browser.tabs.create({
         url,
@@ -41,37 +25,39 @@ async function newTab(block) {
         windowId: this.windowId,
       });
 
-      this.tabId = tab.id;
-      this.activeTabUrl = url;
+      this.activeTab.id = tab.id;
+      this.activeTab.url = url;
       this.windowId = tab.windowId;
     }
 
     if (inGroup && !updatePrevTab) {
       const options = {
-        groupId: this.tabGroupId,
-        tabIds: this.tabId,
+        groupId: this.activeTab.groupId,
+        tabIds: this.activeTab.id,
       };
 
-      if (!this.tabGroupId) {
+      if (!this.activeTab.groupId) {
         options.createProperties = {
           windowId: this.windowId,
         };
       }
 
       chrome.tabs.group(options, (tabGroupId) => {
-        this.tabGroupId = tabGroupId;
+        this.activeTab.groupId = tabGroupId;
       });
     }
 
-    this.frameId = 0;
-    this.frames = await tabUpdatedListener.call(this, { id: this.tabId });
+    this.activeTab.frameId = 0;
+    this.activeTab.frames = await executeContentScript(this.activeTab.id);
 
     return {
       data: url,
-      nextBlockId: getBlockConnection(block),
+      nextBlockId,
     };
   } catch (error) {
     console.error(error);
+    error.nextBlockId = nextBlockId;
+
     throw error;
   }
 }

+ 5 - 5
src/background/workflow-engine/blocks-handler/handler-switch-to.js

@@ -7,7 +7,7 @@ async function switchTo(block) {
 
   try {
     if (block.data.windowType === 'main-window') {
-      this.frameId = 0;
+      this.activeTab.frameId = 0;
 
       delete this.frameSelector;
 
@@ -17,7 +17,7 @@ async function switchTo(block) {
       };
     }
 
-    const frames = await getFrames(this.tabId);
+    const frames = await getFrames(this.activeTab.id);
     const { url, isSameOrigin } = await this._sendMessageToTab(block, {
       frameId: 0,
     });
@@ -32,13 +32,13 @@ async function switchTo(block) {
     }
 
     if (objectHasKey(frames, url)) {
-      this.frameId = this.frames[url];
+      this.activeTab.frameId = this.activeTab.frames[url];
 
-      await executeContentScript(this.tabId, this.frameId);
+      await executeContentScript(this.activeTab.id, this.activeTab.frameId);
       await new Promise((resolve) => setTimeout(resolve, 1000));
 
       return {
-        data: this.frameId,
+        data: this.activeTab.frameId,
         nextBlockId,
       };
     }

+ 4 - 4
src/background/workflow-engine/blocks-handler/handler-take-screenshot.js

@@ -37,7 +37,7 @@ async function takeScreenshot(block) {
     };
 
     if (captureActiveTab) {
-      if (!this.tabId) {
+      if (!this.activeTab.id) {
         throw new Error('no-tab');
       }
 
@@ -47,7 +47,7 @@ async function takeScreenshot(block) {
       });
 
       await browser.windows.update(this.windowId, { focused: true });
-      await browser.tabs.update(this.tabId, { active: true });
+      await browser.tabs.update(this.activeTab.id, { active: true });
 
       await new Promise((resolve) => setTimeout(resolve, 500));
 
@@ -58,12 +58,12 @@ async function takeScreenshot(block) {
         await browser.tabs.update(tab.id, { active: true });
       }
 
-      if (saveToColumn) this.addData(dataColumn, uri);
+      if (saveToColumn) this.addDataToColumn(dataColumn, uri);
       if (saveToComputer) saveImage({ fileName, uri, ext });
     } else {
       const uri = await browser.tabs.captureVisibleTab(options);
 
-      if (saveToColumn) this.addData(dataColumn, uri);
+      if (saveToColumn) this.addDataToColumn(dataColumn, uri);
       if (saveToComputer) saveImage({ fileName, uri, ext });
     }
 

+ 2 - 2
src/background/workflow-engine/blocks-handler/handler-trigger.js

@@ -5,8 +5,8 @@ async function trigger(block) {
   const nextBlockId = getBlockConnection(block);
 
   try {
-    if (block.data.type === 'visit-web' && this.tabId) {
-      this.frames = await executeContentScript(this.tabId);
+    if (block.data.type === 'visit-web' && this.activeTab.id) {
+      this.activeTab.frames = await executeContentScript(this.activeTab.id);
     }
 
     return { nextBlockId, data: '' };

+ 1 - 0
src/background/workflow-engine/execute-content-script.js

@@ -39,6 +39,7 @@ export default async function (tabId, frameId = 0) {
 
     if (!isScriptExists) {
       await browser.tabs.executeScript(tabId, {
+        runAt: 'document_end',
         frameId: currentFrameId,
         file: './contentScript.bundle.js',
       });

+ 2 - 2
src/background/workflow-engine/index.js

@@ -1,8 +1,8 @@
 import Engine from './engine';
 import blocksHandler from './blocks-handler';
 
-export default function (workflow, options = {}) {
-  const engine = new Engine(workflow, { ...options, blocksHandler });
+export default function (workflow, { options = {}, states } = {}) {
+  const engine = new Engine(workflow, { ...options, states, blocksHandler });
 
   return engine;
 }

+ 311 - 0
src/background/workflow-engine2.js

@@ -0,0 +1,311 @@
+/* eslint-disable */
+import browser from 'webextension-polyfill';
+import { nanoid } from 'nanoid';
+import { tasks } from '@/utils/shared';
+import { convertData } from './workflow-engine/helper';
+import { toCamelCase, parseJSON, isObject, objectHasKey } from '@/utils/helper';
+import referenceData from '@/utils/reference-data';
+
+/*
+parentWorkflow {
+  logId: string,
+  state: object,
+}
+*/
+export function getBlockConnection({ outputs }, index = 1) {
+  const blockId = outputs[`output_${index}`]?.connections[0]?.node;
+
+  return blockId;
+}
+
+class WorkflowEngine {
+  constructor(workflow, { states, logger, blocksHandler, tabId, parentWorkflow, globalData }) {
+    this.id = nanoid();
+    this.states = states;
+    this.logger = logger;
+    this.workflow = workflow;
+    this.blocksHandler = blocksHandler;
+    this.parentWorkflow = parentWorkflow;
+    this.saveLog = workflow.settings?.saveLog ?? true;
+
+    this.loopList = {};
+    this.repeatedTasks = {};
+
+    this.windowId = null;
+    this.currentBlock = null;
+
+    this.isDestroyed = false;
+    this.isUsingProxy = false;
+
+    this.blocks = {};
+    this.history = [];
+    this.eventListeners = {};
+    this.columns = { column: { index: 0, type: 'any' } };
+
+    const globalDataValue = globalData || workflow.globalData;
+
+    this.activeTab = {
+      url: '',
+      id: tabId,
+      frameId: 0,
+      frames: {},
+      groupId: null,
+    }
+    this.referenceData = {
+      loopData: {},
+      dataColumns: [],
+      googleSheets: {},
+      globalData: parseJSON(globalDataValue, globalDataValue),
+    };
+  }
+
+  init() {
+    if (this.workflow.isDisabled) return;
+
+    const { drawflow } = this.workflow;
+    const flow = typeof drawflow === 'string' ? parseJSON(drawflow, {}) : drawflow;
+    const blocks = flow?.drawflow?.Home.data;
+
+    if (!blocks) {
+      console.error(`${this.workflow.name} doesn't have blocks`);
+      return;
+    }
+
+    const triggerBlock = Object.values(blocks).find(({ name }) => name === 'trigger');
+    if (!triggerBlock) {
+      console.error(`${this.workflow.name} doesn't have a trigger block`);
+      return;
+    }
+
+    const dataColumns = Array.isArray(this.workflow.dataColumns)
+      ? this.workflow.dataColumns
+      : Object.values(this.workflow.dataColumns);
+
+    dataColumns.forEach(({ name, type }) => {
+      this.columns[name] = { index: 0, type };
+    });
+
+    this.blocks = blocks;
+    this.currentBlock = triggerBlock;
+    this.startedTimestamp = Date.now();
+    this.workflow.dataColumns = dataColumns;
+
+    this.states.add(this.id, {
+      state: this.state,
+      workflowId: this.workflow.id,
+      parentWorkflow: this.parentWorkflow,
+    }).then(() => {
+      this.executeBlock(triggerBlock);
+    });
+  }
+
+  addLogHistory(detail) {
+    if (
+      !this.saveLog &&
+      (this.history.length >= 1001 || detail.name === 'blocks-group') &&
+      detail.type !== 'error'
+    )
+      return;
+
+    this.history.push(detail);
+  }
+
+  addDataToColumn(key, value) {
+    if (Array.isArray(key)) {
+      key.forEach((item) => {
+        if (!isObject(item)) return;
+
+        Object.entries(item).forEach(([itemKey, itemValue]) => {
+          this.addDataToColumn(itemKey, itemValue);
+        });
+      });
+
+      return;
+    }
+
+    const columnName = objectHasKey(this.columns, key) ? key : 'column';
+    const currentColumn = this.columns[columnName];
+    const convertedValue = convertData(value, currentColumn.type);
+
+    if (objectHasKey(this.referenceData.dataColumns, currentColumn.index)) {
+      this.referenceData.dataColumns[currentColumn.index][columnName] = convertedValue;
+    } else {
+      this.referenceData.dataColumns.push({ [columnName]: convertedValue });
+    }
+
+    currentColumn.index += 1;
+  }
+
+  async stop() {
+    try {
+      if (this.childWorkflowId) {
+        await this.states.update(this.childWorkflowId, { isDestroyed: true });
+      }
+
+      await this.destroy('stopped');
+    } catch (error) {
+      console.error(error);
+    }
+  }
+
+  async destroy(status, message) {
+    try {
+      if (this.isUsingProxy) chrome.proxy.settings.clear({});
+
+      const endedTimestamp = Date.now();
+
+      if (!this.workflow.isTesting && this.saveLog) {
+        const { name, id } = this.workflow;
+
+        await this.logger.add({
+          name,
+          status,
+          message,
+          id: this.id,
+          workflowId: id,
+          history: this.history,
+          endedAt: endedTimestamp,
+          parentLog: this.parentWorkflow,
+          startedAt: this.startedTimestamp,
+          data: this.referenceData.dataColumns,
+        });
+      }
+
+      await this.states.delete(this.id);
+
+      this.isDestroyed = true;
+    } catch (error) {
+      console.error(error);
+    }
+  }
+
+  async executeBlock(block, prevBlockData) {
+    const currentState = await this.states.get(this.id);
+    console.log(currentState);
+    if (!currentState || currentState.isDestroyed) {
+      if (this.isDestroyed) return;
+
+      await this.destroy('stopped');
+      return;
+    }
+
+    this.currentBlock = block;
+
+    await this.states.update(this.id, { state: this.state });
+    console.log(block, prevBlockData);
+    const startExecutedTime = Date.now();
+    const blockHandler = this.blocksHandler[toCamelCase(block?.name)];
+    const handler = !blockHandler && tasks[block.name].category === 'interaction'
+      ? this.blocksHandler.interactionBlock
+      : blockHandler;
+
+    if (!handler) {
+      console.error(`"${block.name}" block doesn't have a handler`);
+      this.destroy('stopped');
+      return;
+    }
+
+    const replacedBlock = referenceData({ block, data: this.referenceData });
+    const blockDelay = this.workflow.settings?.blockDelay || 0;
+
+    try {
+      const result = await handler.call(this, replacedBlock, { prevBlockData, refData: this.referenceData });
+
+      this.addLogHistory({
+        type: 'success',
+        name: block.name,
+        logId: result.logId,
+        duration: Math.round(Date.now() - startExecutedTime),
+      });
+
+      if (result.nextBlockId) {
+        setTimeout(() => {
+          this.executeBlock(this.blocks[result.nextBlockId], result.data);
+        }, blockDelay);
+      } else {
+        this.addLogHistory({
+          type: 'finish',
+          name: 'finish',
+        });
+        this.dispatchEvent('finish');
+        this.destroy('success');
+      }
+    } catch (error) {
+      this.addLogHistory({
+        type: 'error',
+        message: error.message,
+        name: block.name,
+        ...(error.data || {}),
+      });
+
+      if (
+        this.workflow.settings.onError === 'keep-running' &&
+        error.nextBlockId
+      ) {
+        setTimeout(() => {
+          this.executeBlock(
+            this.blocks[error.nextBlockId],
+            error.data || ''
+          );
+        }, blockDelay);
+      } else {
+        this.destroy('error', error.message);
+      }
+
+      console.error(block.name, error);
+    }
+  }
+
+  dispatchEvent(name, params) {
+    const listeners = this.eventListeners[name];
+
+    if (!listeners) return;
+
+    listeners.forEach((callback) => {
+      callback(params);
+    });
+  }
+
+  on(name, listener) {
+    (this.eventListeners[name] = this.eventListeners[name] || []).push(
+      listener
+    );
+  }
+
+  get state() {
+    const keys = [
+      'history',
+      'activeTab',
+      'isUsingProxy',
+      'currentBlock',
+      'referenceData',
+      'startedTimestamp',
+    ];
+    const state = {};
+
+    keys.forEach((key) => {
+      state[key] = this[key];
+    });
+
+    return state;
+  }
+
+  _sendMessageToTab(payload, options = {}) {
+    return new Promise((resolve, reject) => {
+      if (!this.activeTab.id) {
+        const error = new Error('no-tab');
+        error.workflowId = this.id;
+
+        reject(error);
+        return;
+      }
+
+      browser.tabs
+        .sendMessage(this.activeTab.id, { isBlock: true, ...payload }, options)
+        .then(resolve)
+        .catch(reject);
+    });
+  }
+}
+
+export default WorkflowEngine;

+ 16 - 0
src/background/workflow-logger.js

@@ -0,0 +1,16 @@
+class WorkflowLogger {
+  constructor({ storage, key = 'logs' }) {
+    this.key = key;
+    this.storage = storage;
+  }
+
+  async add(data) {
+    const logs = (await this.storage.get(this.key)) || {};
+
+    logs.unshift(data);
+
+    await this.storage.set(this.key, logs);
+  }
+}
+
+export default WorkflowLogger;

+ 5 - 1
src/background/workflow-state.js

@@ -23,7 +23,11 @@ class WorkflowState {
       let { workflowState } = await browser.storage.local.get('workflowState');
 
       if (workflowState && filter) {
-        workflowState = workflowState.filter(filter);
+        workflowState = (
+          Array.isArray(workflowState)
+            ? workflowState
+            : Object.values(workflowState)
+        ).filter(filter);
       }
 
       return workflowState || [];

+ 79 - 0
src/background/workflow-state2.js

@@ -0,0 +1,79 @@
+/* eslint-disable  no-param-reassign */
+
+class WorkflowState {
+  constructor({ storage, key = 'workflowState' }) {
+    this.key = key;
+    this.storage = storage;
+  }
+
+  async _updater(callback) {
+    try {
+      const storageStates = await this.get();
+      const states = callback(storageStates);
+
+      await this.storage.set(this.key, states);
+
+      return states;
+    } catch (error) {
+      console.error(error);
+
+      return [];
+    }
+  }
+
+  async get(stateId) {
+    try {
+      let states = (await this.storage.get(this.key)) || {};
+
+      if (Array.isArray(states)) {
+        states = {};
+        await this.storage.set(this.key, {});
+      }
+
+      if (typeof stateId === 'function') {
+        states = Object.values(storageStates).find(stateId);
+      } else if (stateId) {
+        states = states[stateId];
+      }
+
+      return states;
+    } catch (error) {
+      console.error(error);
+
+      return null;
+    }
+  }
+
+  add(id, data = {}) {
+    return this._updater((states) => {
+      states[id] = {
+        id,
+        isPaused: false,
+        isDestroyed: false,
+        ...data,
+      };
+
+      return states;
+    });
+  }
+
+  update(id, data = {}) {
+    return this._updater((states) => {
+      if (states[id]) {
+        states[id] = { ...states[id], ...data };
+      }
+
+      return states;
+    });
+  }
+
+  delete(id) {
+    return this._updater((states) => {
+      delete states[id];
+
+      return states;
+    });
+  }
+}
+
+export default WorkflowState;

+ 1 - 0
src/content/index.js

@@ -4,6 +4,7 @@ import elementSelector from './element-selector';
 import blocksHandler from './blocks-handler';
 
 (() => {
+  alert('ha');
   if (window.isAutomaInjected) return;
 
   window.isAutomaInjected = true;

+ 1 - 1
src/newtab/App.vue

@@ -55,7 +55,7 @@ function handleStorageChanged(change) {
   if (change.workflowState) {
     store.commit('updateState', {
       key: 'workflowState',
-      value: change.workflowState.newValue,
+      value: Object.values(change.workflowState.newValue || {}),
     });
   }
 }

+ 1 - 1
src/store/index.js

@@ -84,7 +84,7 @@ const store = createStore({
 
         commit('updateState', {
           key: 'workflowState',
-          value: workflowState || [],
+          value: Object.values(workflowState || {}),
         });
       } catch (error) {
         console.error(error);

+ 6 - 0
src/utils/helper.js

@@ -38,6 +38,12 @@ export function parseJSON(data, def) {
   }
 }
 
+export function parseFlow(flow) {
+  const obj = typeof flow === 'string' ? parseJSON(flow, {}) : flow;
+
+  return obj?.drawflow?.Home.data;
+}
+
 export function replaceMustache(str, replacer) {
   /* eslint-disable-next-line */
   return str.replace(/\{\{(.*?)\}\}/g, replacer);

+ 7 - 6
src/utils/reference-data/index.js

@@ -26,7 +26,7 @@ export const funcs = {
   },
 };
 
-export default function ({ block, data }) {
+export default function ({ block, data: refData }) {
   const replaceKeys = [
     'url',
     'name',
@@ -40,15 +40,16 @@ export default function ({ block, data }) {
     'extraRowValue',
   ];
   let replacedBlock = { ...block };
-  const refData = Object.assign(data, { funcs });
+  const data = Object.assign(refData, { funcs });
 
   replaceKeys.forEach((blockDataKey) => {
     if (!objectHasKey(block.data, blockDataKey)) return;
 
-    const newDataValue = mustacheReplacer(
-      replacedBlock.data[blockDataKey],
-      refData
-    );
+    const newDataValue = mustacheReplacer({
+      data,
+      block,
+      str: replacedBlock.data[blockDataKey],
+    });
 
     replacedBlock = setObjectPath(
       replacedBlock,

+ 14 - 9
src/utils/reference-data/mustache-replacer.js

@@ -1,5 +1,5 @@
 import { get as getObjectPath } from 'object-path-immutable';
-import { replaceMustache, isObject } from '../helper';
+import { replaceMustache } from '../helper';
 import keyParser from './key-parser';
 
 export function extractStrFunction(str) {
@@ -13,24 +13,29 @@ export function extractStrFunction(str) {
   };
 }
 
-export default function (str, data) {
+export default function ({ str, data, block }) {
   const replacedStr = replaceMustache(str, (match) => {
     const key = match.slice(2, -2).replace(/\s/g, '');
 
     if (!key) return '';
 
+    let result = '';
     const funcRef = extractStrFunction(key);
 
     if (funcRef && data.funcs[funcRef.name]) {
-      return data.funcs[funcRef.name]?.apply({ refData: data }, funcRef.params);
+      result = data.funcs[funcRef.name]?.apply(
+        { refData: data },
+        funcRef.params
+      );
+    } else {
+      const { dataKey, path } = keyParser(key);
+      result = getObjectPath(data[dataKey], path) ?? match;
     }
 
-    const { dataKey, path } = keyParser(key);
-    const result = getObjectPath(data[dataKey], path) ?? match;
-
-    return isObject(result) || Array.isArray(result)
-      ? JSON.stringify(result)
-      : result;
+    if (block.name === 'webhook') {
+      return JSON.stringify(result);
+    }
+    return typeof result === 'string' ? result : JSON.stringify(result);
   });
 
   return replacedStr;

+ 5 - 0
yarn.lock

@@ -2748,6 +2748,11 @@ defined@^1.0.0:
   resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
   integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=
 
+defu@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/defu/-/defu-5.0.0.tgz#5768f0d402a555bfc4c267246b20f82ce8b5a10b"
+  integrity sha512-VHg73EDeRXlu7oYWRmmrNp/nl7QkdXUxkQQKig0Zk8daNmm84AbGoC8Be6/VVLJEKxn12hR0UBmz8O+xQiAPKQ==
+
 del@^4.1.1:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/del/-/del-4.1.1.tgz#9e8f117222ea44a31ff3a156c049b99052a9f0b4"