1
0
Эх сурвалжийг харах

refactor: workflow engine

Ahmad Kholid 3 жил өмнө
parent
commit
12f9b28862

+ 1 - 1
src/background/collection-engine/flow-handler.js

@@ -1,4 +1,4 @@
-import workflowEngine from '../workflow-engine';
+import workflowEngine from '../workflow-engine/engine';
 import dataExporter from '@/utils/data-exporter';
 
 export function workflow(flow) {

+ 1 - 1
src/background/collection-engine/index.js

@@ -3,7 +3,7 @@ import browser from 'webextension-polyfill';
 import { toCamelCase } from '@/utils/helper';
 import * as flowHandler from './flow-handler';
 import workflowState from '../workflow-state';
-import workflowEngine from '../workflow-engine';
+import workflowEngine from '../workflow-engine/engine';
 
 class CollectionEngine {
   constructor(collection) {

+ 55 - 38
src/background/index.js

@@ -1,12 +1,9 @@
-/* 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 WorkflowState from './workflow-state2';
-import CollectionEngine from './collection-engine';
-import WorkflowEngine from './workflow-engine2';
+import WorkflowState from './workflow-state';
+// import CollectionEngine from './collection-engine';
+import WorkflowEngine from './workflow-engine/engine';
 import blocksHandler from './workflow-engine/blocks-handler';
 import WorkflowLogger from './workflow-logger';
 
@@ -22,39 +19,64 @@ const storage = {
     }
   },
   async set(key, value) {
+    if (key === 'workflowState') {
+      sessionStorage.setItem(key, JSON.stringify(value));
+    }
+
     await browser.storage.local.set({ [key]: value });
-  }
+  },
 };
 const workflow = {
+  states: new WorkflowState({ storage }),
+  logger: new WorkflowLogger({ storage }),
   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 });
+    const findWorkflow = workflows.find(({ id }) => id === workflowId);
 
-    return states;
+    return findWorkflow;
   },
-  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();
+  execute(workflowData, options) {
+    const engine = new WorkflowEngine(workflowData, {
+      ...options,
+      blocksHandler,
+      logger: this.logger,
+      states: this.states,
+    });
 
-    console.log(engine);
+    if (options?.resume) {
+      engine.resume(options.state);
+    } else {
+      engine.init();
+    }
 
     return engine;
-  }
+  },
 };
 
+async function checkWorkflowStates() {
+  const states = await workflow.states.get();
+  // const sessionStates = parseJSON(sessionStorage.getItem('workflowState'), {});
+
+  Object.values(states || {}).forEach((state) => {
+    /* Enable when using manifest 3 */
+    // const resumeWorkflow =
+    //   !state.isDestroyed && objectHasKey(sessionStates, state.id);
+
+    if (false) {
+      workflow.get(state.workflowId).then((workflowData) => {
+        workflow.execute(workflowData, {
+          state,
+          resume: true,
+        });
+      });
+    } else {
+      delete states[state.id];
+    }
+  });
+
+  await storage.set('workflowState', states);
+}
+checkWorkflowStates();
 async function checkVisitWebTriggers(states, tab) {
   const visitWebTriggers = await storage.get('visitWebTriggers');
   const triggeredWorkflow = visitWebTriggers.find(({ url, isRegex }) => {
@@ -69,26 +91,23 @@ async function checkVisitWebTriggers(states, tab) {
     if (workflowData) workflow.execute(workflowData);
   }
 }
-async function checkWorkflowStates() {
-  /* check if tab is reloaded */
-}
 browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
   if (changeInfo.status === 'complete') {
     await checkVisitWebTriggers(null, tab);
   }
 });
 browser.alarms.onAlarm.addListener(({ name }) => {
-  workflow.get(name).then((workflow) => {
-    if (!workflow) return;
+  workflow.get(name).then((currentWorkflow) => {
+    if (!currentWorkflow) return;
 
-    workflow.execute(workflow);
+    workflow.execute(currentWorkflow);
 
     const triggerBlock = Object.values(
-      JSON.parse(workflow.drawflow).drawflow.Home.data
+      JSON.parse(currentWorkflow.drawflow).drawflow.Home.data
     ).find((block) => block.name === 'trigger');
 
     if (triggerBlock?.data.type === 'specific-day') {
-      registerSpecificDay(workflow.id, triggerBlock.data);
+      registerSpecificDay(currentWorkflow.id, triggerBlock.data);
     }
   });
 });
@@ -161,13 +180,11 @@ message.on('collection:stop', (id) => {
   collection.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 });
+  await workflow.states.stop(id);
 });
 
 browser.runtime.onMessage.addListener(message.listener());

+ 17 - 13
src/background/workflow-engine/blocks-handler/handler-execute-workflow.js

@@ -1,15 +1,16 @@
 import browser from 'webextension-polyfill';
-import WorkflowEngine from '../index';
+import WorkflowEngine from '../engine';
 import { getBlockConnection } from '../helper';
+import { isWhitespace } from '@/utils/helper';
 
 function workflowListener(workflow, options) {
   return new Promise((resolve, reject) => {
     const engine = new WorkflowEngine(workflow, options);
     engine.init();
-    engine.on('destroyed', ({ id, status, message, currentBlock }) => {
+    engine.on('destroyed', ({ id, status, message }) => {
       if (status === 'error') {
         const error = new Error(message);
-        error.data = { logId: id, name: currentBlock.name };
+        error.data = { logId: id };
 
         reject(error);
         return;
@@ -38,17 +39,20 @@ async function executeWorkflow(block) {
 
       throw errorInstance;
     }
-
-    const onInit = (engine) => {
-      this.childWorkflow = engine;
-    };
     const options = {
-      events: { onInit },
-      isChildWorkflow: true,
-      collectionLogId: this.id,
-      collectionId: this.workflow.id,
-      parentWorkflow: { name: this.workflow.name },
-      globalData: !/\S/g.test(data.globalData) ? null : data.globalData,
+      parentWorkflow: {
+        id: this.id,
+        name: this.workflow.name,
+      },
+      events: {
+        onInit: (engine) => {
+          this.childWorkflowId = engine.id;
+        },
+      },
+      states: this.states,
+      logger: this.logger,
+      blocksHandler: this.blocksHandler,
+      globalData: isWhitespace(data.globalData) ? null : data.globalData,
     };
 
     if (workflow.drawflow.includes(this.workflow.id)) {

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

@@ -18,7 +18,11 @@ async function interactionHandler(block, { refData }) {
       await new Promise((resolve) => setTimeout(resolve, 5000));
 
     if (objectHasKey(block.data, 'dataColumn')) {
-      if (!block.data.saveData)
+      const dontSaveData =
+        (block.name === 'forms' && !block.data.getValue) ||
+        !block.data.saveData;
+
+      if (dontSaveData)
         return {
           data,
           nextBlockId,

+ 2 - 0
src/background/workflow-engine/blocks-handler/handler-new-tab.js

@@ -50,6 +50,8 @@ async function newTab(block) {
     this.activeTab.frameId = 0;
     this.activeTab.frames = await executeContentScript(this.activeTab.id);
 
+    console.log(this.activeTab);
+
     return {
       data: url,
       nextBlockId,

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

@@ -1,6 +1,6 @@
 import { objectHasKey } from '@/utils/helper';
 import { getBlockConnection } from '../helper';
-import executeContentScript, { getFrames } from '../execute-content-script';
+import executeContentScript from '../execute-content-script';
 
 async function switchTo(block) {
   const nextBlockId = getBlockConnection(block);
@@ -17,7 +17,6 @@ async function switchTo(block) {
       };
     }
 
-    const frames = await getFrames(this.activeTab.id);
     const { url, isSameOrigin } = await this._sendMessageToTab(block, {
       frameId: 0,
     });
@@ -31,7 +30,7 @@ async function switchTo(block) {
       };
     }
 
-    if (objectHasKey(frames, url)) {
+    if (objectHasKey(this.activeTab.frames, url)) {
       this.activeTab.frameId = this.activeTab.frames[url];
 
       await executeContentScript(this.activeTab.id, this.activeTab.frameId);

+ 244 - 302
src/background/workflow-engine/engine.js

@@ -1,172 +1,143 @@
-/* eslint-disable no-underscore-dangle */
 import browser from 'webextension-polyfill';
 import { nanoid } from 'nanoid';
 import { tasks } from '@/utils/shared';
 import { convertData } from './helper';
-import { generateJSON } from '@/utils/data-exporter';
 import { toCamelCase, parseJSON, isObject, objectHasKey } from '@/utils/helper';
-import errorMessage from './error-message';
 import referenceData from '@/utils/reference-data';
-import workflowState from '../workflow-state';
 import executeContentScript from './execute-content-script';
 
-let reloadTimeout;
-
-function tabRemovedHandler(tabId) {
-  if (tabId !== this.tabId) return;
-
-  delete this.tabId;
-
-  if (
-    this.currentBlock.name === 'new-tab' ||
-    tasks[this.currentBlock.name].category === 'interaction'
-  ) {
-    this.destroy('error', 'active-tab-removed');
-  }
-
-  workflowState.update(this.id, this.state);
-}
-function tabUpdatedHandler(tabId, changeInfo, tab) {
-  const listener = this.tabUpdatedListeners[tabId];
-
-  if (listener) {
-    listener.callback(tabId, changeInfo, () => {
-      delete this.tabUpdatedListeners[tabId];
-    });
-  } else if (this.tabId === tabId) {
-    if (!reloadTimeout) {
-      reloadTimeout = setTimeout(() => {
-        this.isPaused = false;
-      }, 15000);
-    }
-
-    this.isPaused = true;
-
-    if (changeInfo.status === 'complete') {
-      clearTimeout(reloadTimeout);
-      reloadTimeout = null;
-
-      executeContentScript(tabId)
-        .then((frames) => {
-          this.tabId = tabId;
-          this.frames = frames;
-          this.isPaused = false;
-          this.activeTabUrl = tab?.url || '';
-        })
-        .catch((error) => {
-          console.error(error);
-          this.isPaused = false;
-        });
-    }
-  }
-}
-
 class WorkflowEngine {
   constructor(
     workflow,
-    {
-      globalData,
-      tabId = null,
-      isInCollection,
-      collectionLogId,
-      blocksHandler,
-      parentWorkflow,
-    }
+    { states, logger, blocksHandler, tabId, parentWorkflow, globalData }
   ) {
-    const globalDataVal = globalData || workflow.globalData;
-
     this.id = nanoid();
-    this.tabId = tabId;
+    this.states = states;
+    this.logger = logger;
     this.workflow = workflow;
     this.blocksHandler = blocksHandler;
-    this.isInCollection = isInCollection;
     this.parentWorkflow = parentWorkflow;
-    this.collectionLogId = collectionLogId;
-    this.globalData = parseJSON(globalDataVal, globalDataVal);
-    this.activeTabUrl = '';
-    this.columns = { column: { index: 0, type: 'any' } };
-    this.data = [];
-    this.logs = [];
-    this.blocks = {};
-    this.frames = {};
+    this.saveLog = workflow.settings?.saveLog ?? true;
+
     this.loopList = {};
-    this.loopData = {};
     this.repeatedTasks = {};
-    this.eventListeners = {};
-    this.isPaused = false;
-    this.isDestroyed = false;
-    this.isUsingProxy = false;
-    this.frameId = 0;
+
     this.windowId = null;
-    this.tabGroupId = null;
     this.currentBlock = null;
-    this.childWorkflow = null;
-    this.workflowTimeout = null;
+    this.childWorkflowId = null;
 
-    this.saveLog = workflow.settings?.saveLog ?? true;
+    this.isDestroyed = false;
+    this.isUsingProxy = false;
+
+    this.blocks = {};
+    this.history = [];
+    this.eventListeners = {};
+    this.columns = { column: { index: 0, type: 'any' } };
 
-    this.googleSheets = {};
+    const globalDataValue = globalData || workflow.globalData;
 
-    this.tabUpdatedListeners = {};
-    this.tabUpdatedHandler = tabUpdatedHandler.bind(this);
-    this.tabRemovedHandler = tabRemovedHandler.bind(this);
+    this.activeTab = {
+      url: '',
+      id: tabId,
+      frameId: 0,
+      frames: {},
+      groupId: null,
+    };
+    this.referenceData = {
+      loopData: {},
+      dataColumns: [],
+      googleSheets: {},
+      globalData: parseJSON(globalDataValue, globalDataValue),
+    };
+
+    this.onWorkflowStopped = (id) => {
+      if (this.id !== id || this.isDestroyed) return;
+
+      this.stop();
+    };
   }
 
-  init() {
+  init(currentBlock) {
     if (this.workflow.isDisabled) return;
 
-    const drawflowData =
-      typeof this.workflow.drawflow === 'string'
-        ? JSON.parse(this.workflow.drawflow || '{}')
-        : this.workflow.drawflow;
-    const blocks = drawflowData?.drawflow?.Home.data;
+    if (!this.states) {
+      console.error(`"${this.workflow.name}" workflow doesn't have states`);
+      this.destroy('error');
+      return;
+    }
+
+    const { drawflow } = this.workflow;
+    const flow =
+      typeof drawflow === 'string' ? parseJSON(drawflow, {}) : drawflow;
+    const blocks = flow?.drawflow?.Home.data;
 
     if (!blocks) {
-      console.error(errorMessage('no-block', this.workflow));
+      console.error(`${this.workflow.name} doesn't have blocks`);
       return;
     }
 
-    const blocksArr = Object.values(blocks);
-    const triggerBlock = blocksArr.find(({ name }) => name === 'trigger');
-
+    const triggerBlock = Object.values(blocks).find(
+      ({ name }) => name === 'trigger'
+    );
     if (!triggerBlock) {
-      console.error(errorMessage('no-trigger-block', this.workflow));
+      console.error(`${this.workflow.name} doesn't have a trigger block`);
       return;
     }
 
-    browser.tabs.onUpdated.addListener(this.tabUpdatedHandler);
-    browser.tabs.onRemoved.addListener(this.tabRemovedHandler);
-
     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.startedTimestamp = Date.now();
     this.workflow.dataColumns = dataColumns;
+    this.currentBlock = currentBlock || triggerBlock;
 
-    dataColumns.forEach(({ name, type }) => {
-      this.columns[name] = { index: 0, type };
-    });
+    this.states.on('stop', this.onWorkflowStopped);
 
-    workflowState
+    this.states
       .add(this.id, {
         state: this.state,
         workflowId: this.workflow.id,
-        isInCollection: this.isInCollection,
+        parentState: this.parentWorkflow,
       })
       .then(() => {
-        this._blockHandler(triggerBlock);
+        this.executeBlock(this.currentBlock);
       });
   }
 
-  addData(key, value) {
+  resume({ id, state }) {
+    this.id = id;
+
+    Object.keys(state).forEach((key) => {
+      this[key] = state[key];
+    });
+
+    this.init(state.currentBlock);
+  }
+
+  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.addData(itemKey, itemValue);
+          this.addDataToColumn(itemKey, itemValue);
         });
       });
 
@@ -177,50 +148,22 @@ class WorkflowEngine {
     const currentColumn = this.columns[columnName];
     const convertedValue = convertData(value, currentColumn.type);
 
-    if (objectHasKey(this.data, currentColumn.index)) {
-      this.data[currentColumn.index][columnName] = convertedValue;
+    if (objectHasKey(this.referenceData.dataColumns, currentColumn.index)) {
+      this.referenceData.dataColumns[currentColumn.index][columnName] =
+        convertedValue;
     } else {
-      this.data.push({ [columnName]: convertedValue });
+      this.referenceData.dataColumns.push({ [columnName]: convertedValue });
     }
 
     currentColumn.index += 1;
   }
 
-  addLog(detail) {
-    if (
-      !this.saveLog &&
-      (this.logs.length >= 1001 || detail.name === 'blocks-group') &&
-      detail.type !== 'error'
-    )
-      return;
-
-    this.logs.push(detail);
-  }
-
-  on(name, listener) {
-    (this.eventListeners[name] = this.eventListeners[name] || []).push(
-      listener
-    );
-  }
-
-  pause(pause = true) {
-    this.isPaused = pause;
-
-    workflowState.update(this.id, this.state);
-  }
-
-  async stop(message) {
+  async stop() {
     try {
-      if (this.childWorkflow) {
-        await this.childWorkflow.stop();
+      if (this.childWorkflowId) {
+        await this.states.stop(this.childWorkflowId);
       }
 
-      this.addLog({
-        message,
-        type: 'stop',
-        name: 'stop',
-      });
-
       await this.destroy('stopped');
     } catch (error) {
       console.error(error);
@@ -229,55 +172,123 @@ class WorkflowEngine {
 
   async destroy(status, message) {
     try {
+      if (this.isDestroyed) return;
       if (this.isUsingProxy) chrome.proxy.settings.clear({});
 
-      await browser.tabs.onRemoved.removeListener(this.tabRemovedHandler);
-      await browser.tabs.onUpdated.removeListener(this.tabUpdatedHandler);
-
-      await workflowState.delete(this.id);
-
-      clearTimeout(this.workflowTimeout);
-      this.isDestroyed = true;
-      this.endedTimestamp = Date.now();
+      const endedTimestamp = Date.now();
 
       if (!this.workflow.isTesting && this.saveLog) {
-        const { logs } = await browser.storage.local.get('logs');
-        const { name, icon, id } = this.workflow;
-        const jsonData = generateJSON(Object.keys(this.data), this.data);
+        const { name, id } = this.workflow;
 
-        logs.push({
+        await this.logger.add({
           name,
-          icon,
           status,
           message,
           id: this.id,
           workflowId: id,
-          data: jsonData,
-          history: this.logs,
-          endedAt: this.endedTimestamp,
+          history: this.history,
+          endedAt: endedTimestamp,
+          parentLog: this.parentWorkflow,
           startedAt: this.startedTimestamp,
-          isChildLog: !!this.parentWorkflow,
-          isInCollection: this.isInCollection,
-          collectionLogId: this.collectionLogId,
+          data: this.referenceData.dataColumns,
         });
-
-        await browser.storage.local.set({ logs });
       }
 
       this.dispatchEvent('destroyed', {
-        id: this.id,
         status,
         message,
+        id: this.id,
         currentBlock: this.currentBlock,
       });
 
+      this.states.off('stop', this.onWorkflowStopped);
+      await this.states.delete(this.id);
+
+      this.isDestroyed = true;
       this.eventListeners = {};
-      this.tabUpdatedListeners = {};
     } catch (error) {
       console.error(error);
     }
   }
 
+  async executeBlock(block, prevBlockData) {
+    const currentState = await this.states.get(this.id);
+
+    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 });
+
+    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];
 
@@ -288,157 +299,88 @@ class WorkflowEngine {
     });
   }
 
+  on(name, listener) {
+    (this.eventListeners[name] = this.eventListeners[name] || []).push(
+      listener
+    );
+  }
+
   get state() {
     const keys = [
-      'tabId',
-      'isPaused',
-      'isDestroyed',
+      'history',
+      'activeTab',
+      'isUsingProxy',
       'currentBlock',
-      'isInCollection',
+      'referenceData',
+      'childWorkflowId',
       'startedTimestamp',
     ];
-    const state = keys.reduce((acc, key) => {
-      acc[key] = this[key];
-
-      return acc;
-    }, {});
-
-    state.name = this.workflow.name;
-    state.icon = this.workflow.icon;
+    const state = {
+      name: this.workflow.name,
+      icon: this.workflow.icon,
+    };
 
-    if (this.parentWorkflow) state.parentState = this.parentWorkflow;
+    keys.forEach((key) => {
+      state[key] = this[key];
+    });
 
     return state;
   }
 
-  _blockHandler(block, prevBlockData) {
-    if (this.isDestroyed) return;
-    if (this.isPaused) {
-      browser.tabs.get(this.tabId).then(({ status }) => {
-        this.isPaused = status !== 'complete';
-
-        setTimeout(() => {
-          this._blockHandler(block, prevBlockData);
-        }, 1000);
+  async _sendMessageToTab(payload, options = {}) {
+    const checkActiveTab = () => {
+      return new Promise((resolve, reject) => {
+        const activeTabStatus = () => {
+          browser.tabs
+            .get(this.activeTab.id)
+            .then((tab) => {
+              console.log('Tab status:\t', tab.status);
+              if (tab.status === 'loading') {
+                setTimeout(() => {
+                  activeTabStatus();
+                }, 1000);
+                return;
+              }
+
+              resolve();
+            })
+            .catch(reject);
+        };
+
+        activeTabStatus();
       });
+    };
 
-      return;
-    }
-
-    const disableTimeoutKeys = ['delay', 'javascript-code', 'webhook'];
-
-    if (!disableTimeoutKeys.includes(block.name)) {
-      this.workflowTimeout = setTimeout(() => {
-        if (!this.isDestroyed) this.stop('stop-timeout');
-      }, this.workflow.settings.timeout || 120000);
-    }
-
-    this.currentBlock = block;
-
-    workflowState.update(this.id, this.state);
-    this.dispatchEvent('update', this.state);
-
-    const started = Date.now();
-    const blockHandler = this.blocksHandler[toCamelCase(block?.name)];
-    const handler =
-      !blockHandler && tasks[block.name].category === 'interaction'
-        ? this.blocksHandler.interactionBlock
-        : blockHandler;
-
-    if (handler) {
-      const refData = {
-        prevBlockData,
-        dataColumns: this.data,
-        loopData: this.loopData,
-        globalData: this.globalData,
-        googleSheets: this.googleSheets,
-        activeTabUrl: this.activeTabUrl,
-      };
-      const replacedBlock = referenceData({ block, data: refData });
-      const blockDelay =
-        block.name === 'trigger' ? 0 : this.workflow.settings?.blockDelay || 0;
-
-      handler
-        .call(this, replacedBlock, { prevBlockData, refData })
-        .then((result) => {
-          clearTimeout(this.workflowTimeout);
-          this.workflowTimeout = null;
-
-          this.addLog({
-            type: 'success',
-            name: block.name,
-            logId: result.logId,
-            duration: Math.round(Date.now() - started),
-          });
-
-          if (result.nextBlockId) {
-            setTimeout(() => {
-              this._blockHandler(this.blocks[result.nextBlockId], result.data);
-            }, blockDelay);
-          } else {
-            this.addLog({
-              type: 'finish',
-              name: 'finish',
-            });
-            this.dispatchEvent('finish');
-            this.destroy('success');
-          }
-        })
-        .catch((error) => {
-          this.addLog({
-            type: 'error',
-            message: error.message,
-            name: block.name,
-            ...(error.data || {}),
-          });
-
-          if (
-            this.workflow.settings.onError === 'keep-running' &&
-            error.nextBlockId
-          ) {
-            setTimeout(() => {
-              this._blockHandler(
-                this.blocks[error.nextBlockId],
-                error.data || ''
-              );
-            }, blockDelay);
-          } else {
-            this.destroy('error', error.message);
-          }
-
-          clearTimeout(this.workflowTimeout);
-          this.workflowTimeout = null;
-
-          console.error(error);
-        });
-    } else {
-      console.error(`"${block.name}" block doesn't have a handler`);
-    }
-  }
-
-  _sendMessageToTab(payload, options = {}) {
-    return new Promise((resolve, reject) => {
-      if (!this.tabId) {
+    try {
+      if (!this.activeTab.id) {
         const error = new Error('no-tab');
         error.workflowId = this.id;
 
-        reject(error);
-        return;
+        throw error;
       }
 
-      browser.tabs
-        .sendMessage(this.tabId, { isBlock: true, ...payload }, options)
-        .then(resolve)
-        .catch(reject);
-    });
-  }
+      await checkActiveTab();
 
-  _listener({ id, name, callback, once = true, ...options }) {
-    const listenerNames = {
-      event: 'eventListener',
-      'tab-updated': 'tabUpdatedListeners',
-    };
-    this[listenerNames[name]][id] = { callback, once, ...options };
+      this.activeTab.frames = await executeContentScript(
+        this.activeTab.id,
+        options.frameId || 0
+      );
+      const data = await browser.tabs.sendMessage(
+        this.activeTab.id,
+        { isBlock: true, ...payload },
+        options
+      );
+
+      return data;
+    } catch (error) {
+      if (error.message?.startsWith('Could not establish connection')) {
+        error.message = 'Could not establish connection to the active tab';
+      } else if (error.message?.startsWith('No tab')) {
+        error.message = 'active-tab-removed';
+      }
+
+      throw error;
+    }
   }
 }
 

+ 0 - 26
src/background/workflow-engine/error-message.js

@@ -1,26 +0,0 @@
-import { get } from 'object-path-immutable';
-import { replaceMustache } from '@/utils/helper';
-
-const messages = {
-  'no-trigger-block': '"{{name}}"" workflow doesn\'t have a trigger block.',
-  'no-block': '"{{name}}" workflow doesn\'t have any blocks.',
-  'no-iframe-id':
-    'Can\'t find Frame ID for the frame element with "{{selector}}" selector',
-  'no-tab':
-    'Can\'t connect to a tab, use "New tab" or "Active tab" block before using the "{{name}}" block.',
-};
-
-export default function (errorId, data) {
-  const message = messages[errorId];
-
-  if (!message) return `Can't find message for this error (${errorId})`;
-
-  const resultMessage = replaceMustache(message, (match) => {
-    const key = match.slice(2, -2);
-    const result = get(data, key);
-
-    return result ?? key;
-  });
-
-  return resultMessage;
-}

+ 0 - 8
src/background/workflow-engine/index.js

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

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

@@ -1,311 +0,0 @@
-/* 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;

+ 99 - 49
src/background/workflow-state.js

@@ -1,75 +1,125 @@
 /* eslint-disable  no-param-reassign */
-import browser from 'webextension-polyfill';
 
-async function updater(callback, id) {
-  try {
-    const state = await this.get();
-    const index = id ? state.findIndex((item) => item.id === id) : -1;
-    const items = callback(state, index);
+class WorkflowState {
+  constructor({ storage, key = 'workflowState' }) {
+    this.key = key;
+    this.storage = storage;
+    this.eventListeners = {};
+  }
+
+  async _updater(callback, event) {
+    try {
+      const storageStates = await this.get();
+      const states = callback(storageStates);
+
+      await this.storage.set(this.key, states);
 
-    await browser.storage.local.set({ workflowState: items });
+      if (event) {
+        this.dispatchEvent(event.name, event.params);
+      }
 
-    return items;
-  } catch (error) {
-    console.error(error);
+      return states;
+    } catch (error) {
+      console.error(error);
 
-    return [];
+      return [];
+    }
   }
-}
 
-class WorkflowState {
-  static async get(filter) {
+  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
+    );
+  }
+
+  off(name, listener) {
+    const listeners = this.eventListeners[name];
+    if (!listeners) return;
+
+    const index = listeners.indexOf(listener);
+    if (index !== -1) listeners.splice(index, 1);
+  }
+
+  async get(stateId) {
     try {
-      let { workflowState } = await browser.storage.local.get('workflowState');
-
-      if (workflowState && filter) {
-        workflowState = (
-          Array.isArray(workflowState)
-            ? workflowState
-            : Object.values(workflowState)
-        ).filter(filter);
+      let states = (await this.storage.get(this.key)) || {};
+
+      if (Array.isArray(states)) {
+        states = {};
+        await this.storage.set(this.key, {});
       }
 
-      return workflowState || [];
+      if (typeof stateId === 'function') {
+        states = Object.values(states).find(stateId);
+      } else if (stateId) {
+        states = states[stateId];
+      }
+
+      return states;
     } catch (error) {
       console.error(error);
 
-      return [];
+      return null;
     }
   }
 
-  static add(id, data) {
-    return updater.call(this, (items) => {
-      items.unshift({ id, ...data });
+  add(id, data = {}) {
+    return this._updater((states) => {
+      states[id] = {
+        id,
+        isPaused: false,
+        isDestroyed: false,
+        ...data,
+      };
 
-      return items;
+      return states;
     });
   }
 
-  static update(id, data = {}) {
-    return updater.call(
-      this,
-      (items, index) => {
-        if (typeof index === 'number' && index !== -1) {
-          items[index].state = { ...items[index].state, ...data };
-        }
-
-        return items;
-      },
-      id
-    );
+  async stop(id) {
+    await this.update(id, { isDestroyed: true });
+
+    this.dispatchEvent('stop', id);
+
+    return id;
   }
 
-  static delete(id) {
-    return updater.call(
-      this,
-      (items, index) => {
-        if (index !== -1) items.splice(index, 1);
+  update(id, data = {}) {
+    const event = {
+      name: 'update',
+      params: { id, data },
+    };
 
-        return items;
-      },
-      id
-    );
+    return this._updater((states) => {
+      if (states[id]) {
+        states[id] = { ...states[id], ...data };
+      }
+
+      return states;
+    }, event);
+  }
+
+  delete(id) {
+    const event = {
+      name: 'delete',
+      params: id,
+    };
+
+    return this._updater((states) => {
+      delete states[id];
+
+      return states;
+    }, event);
   }
 }
 

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

@@ -1,79 +0,0 @@
-/* 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;

+ 8 - 3
src/components/newtab/shared/SharedWorkflowState.vue

@@ -43,10 +43,15 @@
       </div>
     </div>
     <div
-      v-if="data.state.parentState"
+      v-if="data.parentState"
       class="py-2 px-4 bg-yellow-200 rounded-lg mt-2 text-sm"
     >
-      {{ t('workflow.state.executeBy', { name: data.state.parentState.name }) }}
+      {{ t('workflow.state.executeBy', { name: data.parentState.name }) }}
+      <span class="lowercase">
+        {{
+          data.isInCollection ? t('common.collection') : t('common.workflow')
+        }}
+      </span>
     </div>
   </ui-card>
 </template>
@@ -65,7 +70,7 @@ const props = defineProps({
 });
 
 const { t } = useI18n();
-
+console.log(props.data);
 function getBlock() {
   if (!props.data.state.currentBlock) return [];
 

+ 1 - 1
src/content/index.js

@@ -4,7 +4,6 @@ import elementSelector from './element-selector';
 import blocksHandler from './blocks-handler';
 
 (() => {
-  alert('ha');
   if (window.isAutomaInjected) return;
 
   window.isAutomaInjected = true;
@@ -23,6 +22,7 @@ import blocksHandler from './blocks-handler';
 
     return new Promise((resolve) => {
       if (data.type === 'content-script-exists') {
+        console.log('content-script-exists');
         resolve(true);
       } else if (data.type === 'select-element') {
         elementSelector();

+ 1 - 0
src/models/log.js

@@ -13,6 +13,7 @@ class Log extends Model {
       endedAt: this.number(0),
       message: this.string(''),
       startedAt: this.number(0),
+      parentLog: this.attr(null),
       workflowId: this.attr(null),
       collectionId: this.attr(null),
       status: this.string('success'),

+ 3 - 1
src/newtab/App.vue

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

+ 4 - 1
src/newtab/pages/Home.vue

@@ -66,7 +66,10 @@ const workflows = computed(() =>
 );
 const logs = computed(() =>
   Log.query()
-    .where(({ isInCollection, isChildLog }) => !isInCollection && !isChildLog)
+    .where(
+      ({ isInCollection, isChildLog, parentLog }) =>
+        !isInCollection && !isChildLog && !parentLog
+    )
     .orderBy('startedAt', 'desc')
     .limit(10)
     .get()

+ 18 - 16
src/newtab/pages/Logs.vue

@@ -121,27 +121,29 @@ const exportDataModal = shallowReactive({
 
 const filteredLogs = computed(() =>
   Log.query()
-    .where(({ name, status, startedAt, isInCollection, isChildLog }) => {
-      if (isInCollection || isChildLog) return false;
+    .where(
+      ({ name, status, startedAt, isInCollection, isChildLog, parentLog }) => {
+        if (isInCollection || isChildLog || parentLog) return false;
 
-      let statusFilter = true;
-      let dateFilter = true;
-      const searchFilter = name
-        .toLocaleLowerCase()
-        .includes(filtersBuilder.query.toLocaleLowerCase());
+        let statusFilter = true;
+        let dateFilter = true;
+        const searchFilter = name
+          .toLocaleLowerCase()
+          .includes(filtersBuilder.query.toLocaleLowerCase());
 
-      if (filtersBuilder.byStatus !== 'all') {
-        statusFilter = status === filtersBuilder.byStatus;
-      }
+        if (filtersBuilder.byStatus !== 'all') {
+          statusFilter = status === filtersBuilder.byStatus;
+        }
 
-      if (filtersBuilder.byDate > 0) {
-        const date = Date.now() - filtersBuilder.byDate * 24 * 60 * 60 * 1000;
+        if (filtersBuilder.byDate > 0) {
+          const date = Date.now() - filtersBuilder.byDate * 24 * 60 * 60 * 1000;
 
-        dateFilter = date <= startedAt;
-      }
+          dateFilter = date <= startedAt;
+        }
 
-      return searchFilter && statusFilter && dateFilter;
-    })
+        return searchFilter && statusFilter && dateFilter;
+      }
+    )
     .orderBy(sortsBuilder.by, sortsBuilder.order)
     .get()
 );

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

@@ -25,7 +25,7 @@
         <ui-list>
           <router-link
             v-if="collectionLog"
-            :to="activeLog.collectionLogId"
+            :to="activeLog.parentLog?.id || activeLog.collectionLogId"
             replace
             class="mb-4 flex"
           >
@@ -176,7 +176,14 @@ const history = computed(() =>
     )
     .map(translateLog)
 );
-const collectionLog = computed(() => Log.find(activeLog.value.collectionLogId));
+const collectionLog = computed(() => {
+  if (activeLog.value.parentLog) {
+    return Log.find(activeLog.value.parentLog.id);
+  }
+
+  return Log.find(activeLog.value.collectionLogId);
+});
+console.log(history.value);
 
 function deleteLog() {
   Log.delete(route.params.id).then(() => {

+ 3 - 1
src/store/index.js

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