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

refactor: collection engine

Ahmad Kholid 3 жил өмнө
parent
commit
4e246b355e

+ 17 - 11
src/background/collection-engine/flow-handler.js

@@ -1,4 +1,5 @@
-import workflowEngine from '../workflow-engine/engine';
+import WorkflowEngine from '../workflow-engine/engine';
+import blocksHandler from '../workflow-engine/blocks-handler';
 import dataExporter from '@/utils/data-exporter';
 
 export function workflow(flow) {
@@ -24,22 +25,27 @@ export function workflow(flow) {
     }
 
     const { globalData } = this.collection;
-    this.currentWorkflow = currentWorkflow;
 
-    const engine = workflowEngine(currentWorkflow, {
-      isInCollection: true,
-      collectionLogId: this.id,
-      collectionId: this.collection.id,
+    const engine = new WorkflowEngine(currentWorkflow, {
+      blocksHandler,
+      states: this.states,
+      logger: this.logger,
+      parentWorkflow: {
+        id: this.id,
+        isCollection: true,
+        name: this.collection.name,
+      },
       globalData: globalData.trim() === '' ? null : globalData,
     });
 
-    this.workflowEngine = engine;
+    this.executedWorkflow.data = {
+      id: engine.id,
+      name: currentWorkflow.name,
+      icon: currentWorkflow.icon,
+      workflowId: currentWorkflow.id,
+    };
 
     engine.init();
-    engine.on('update', (state) => {
-      this.workflowState = state;
-      this.updateState();
-    });
     engine.on('destroyed', ({ id, status, message }) => {
       this.data.push({
         id,

+ 141 - 82
src/background/collection-engine/index.js

@@ -2,23 +2,54 @@ import { nanoid } from 'nanoid';
 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/engine';
+import blocksHandler from '../workflow-engine/blocks-handler';
+import WorkflowEngine from '../workflow-engine/engine';
+
+const executedWorkflows = (workflows, options) => {
+  if (workflows.length === 0) return;
+
+  const workflow = workflows.shift();
+  const engine = new WorkflowEngine(workflow, options);
+
+  engine.init();
+
+  setTimeout(() => {
+    executedWorkflows(workflows, options);
+  }, 500);
+};
 
 class CollectionEngine {
-  constructor(collection) {
+  constructor(collection, { states, logger }) {
     this.id = nanoid();
+    this.states = states;
+    this.logger = logger;
     this.collection = collection;
-    this.workflows = [];
+
     this.data = [];
-    this.logs = [];
+    this.history = [];
+    this.workflows = [];
+
     this.isDestroyed = false;
     this.currentFlow = null;
-    this.workflowState = null;
-    this.workflowEngine = null;
-    this.currentWorkflow = null;
     this.currentIndex = 0;
     this.eventListeners = {};
+
+    this.executedWorkflow = {
+      data: null,
+      state: null,
+    };
+
+    this.onStatesUpdated = ({ data, id }) => {
+      if (id === this.executedWorkflow.data?.id) {
+        this.executedWorkflow.state = data.state;
+        this.states.update(this.id, { state: this.state });
+      }
+    };
+    this.onStatesStopped = (id) => {
+      if (id !== this.id) return;
+
+      this.stop();
+    };
   }
 
   async init() {
@@ -31,23 +62,35 @@ class CollectionEngine {
       this.startedTimestamp = Date.now();
 
       if (this.collection?.options.atOnce) {
-        this.collection.flow.forEach(({ itemId, type }) => {
-          if (type !== 'workflow') return;
+        const filteredWorkflows = this.collection.flow.reduce(
+          (acc, { itemId, type }) => {
+            if (type !== 'workflow') return acc;
 
-          const currentWorkflow = workflows.find(({ id }) => id === itemId);
+            const currentWorkflow = workflows.find(({ id }) => id === itemId);
 
-          if (currentWorkflow) {
-            const engine = workflowEngine(currentWorkflow, {});
+            if (currentWorkflow) {
+              acc.push(currentWorkflow);
+            }
 
-            engine.init();
-          }
+            return acc;
+          },
+          []
+        );
+
+        executedWorkflows(filteredWorkflows, {
+          blocksHandler,
+          states: this.states,
+          logger: this.logger,
         });
       } else {
-        await workflowState.add(this.id, {
+        await this.states.add(this.id, {
           state: this.state,
-          isCollection: true,
           collectionId: this.collection.id,
         });
+
+        this.states.on('stop', this.onStatesStopped);
+        this.states.on('update', this.onStatesUpdated);
+
         this._flowHandler(this.collection.flow[0]);
       }
     } catch (error) {
@@ -72,28 +115,35 @@ class CollectionEngine {
   }
 
   async destroy(status) {
-    this.isDestroyed = true;
-    this.dispatchEvent('destroyed', { id: this.id });
+    try {
+      if (this.isDestroyed) return;
 
-    const { logs } = await browser.storage.local.get('logs');
-    const { name, icon } = this.collection;
+      this.isDestroyed = true;
+      this.dispatchEvent('destroyed', { id: this.id });
 
-    logs.push({
-      name,
-      icon,
-      status,
-      id: this.id,
-      data: this.data,
-      history: this.logs,
-      endedAt: Date.now(),
-      collectionId: this.collection.id,
-      startedAt: this.startedTimestamp,
-    });
+      const { name, icon } = this.collection;
+
+      await this.logger.add({
+        name,
+        icon,
+        status,
+        id: this.id,
+        data: this.data,
+        endedAt: Date.now(),
+        history: this.history,
+        collectionId: this.collection.id,
+        startedAt: this.startedTimestamp,
+      });
+
+      await this.states.delete(this.id);
 
-    await browser.storage.local.set({ logs });
-    await workflowState.delete(this.id);
+      this.states.off('stop', this.onStatesStopped);
+      this.states.off('update', this.onStatesUpdated);
 
-    this.listeners = {};
+      this.listeners = {};
+    } catch (error) {
+      console.error(error);
+    }
   }
 
   nextFlow() {
@@ -113,36 +163,45 @@ class CollectionEngine {
       id: this.id,
       currentBlock: [],
       name: this.collection.name,
+      currentIndex: this.currentIndex,
+      executedWorkflow: this.executedWorkflow,
       startedTimestamp: this.startedTimestamp,
     };
 
-    if (this.currentWorkflow) {
-      const { name, icon } = this.currentWorkflow;
-
-      data.currentBlock.push({ name, icon });
+    if (this.executedWorkflow.data) {
+      data.currentBlock.push(this.executedWorkflow.data);
     }
 
-    if (this.workflowState) {
-      const { name } = this.workflowState.currentBlock;
-
-      data.currentBlock.push({ name });
+    if (this.executedWorkflow.state) {
+      data.currentBlock.push(this.executedWorkflow.state.currentBlock);
     }
 
     return data;
   }
 
-  updateState() {
-    workflowState.update(this.id, this.state);
+  async stop() {
+    try {
+      if (this.executedWorkflow.data) {
+        await this.states.stop(this.executedWorkflow.data.id);
+      }
+
+      setTimeout(() => {
+        this.destroy('stopped');
+      }, 1000);
+    } catch (error) {
+      console.error(error);
+    }
   }
 
-  stop() {
-    this.workflowEngine.stop();
+  async _flowHandler(flow) {
+    const currentState = await this.states.get(this.id);
 
-    this.destroy('stopped');
-  }
+    if (!currentState || currentState.isDestroyed) {
+      if (this.isDestroyed) return;
 
-  _flowHandler(flow) {
-    if (this.isDestroyed) return;
+      await this.destroy('stopped');
+      return;
+    }
 
     const handlerName =
       flow.type === 'workflow' ? 'workflow' : toCamelCase(flow.itemId);
@@ -150,41 +209,41 @@ class CollectionEngine {
     const started = Date.now();
 
     this.currentFlow = flow;
-    this.updateState();
+    await this.states.update(this.id, { state: this.state });
+
+    if (!handler) {
+      console.error(`"${flow.type}" flow doesn't have a handler`);
+      return;
+    }
 
-    if (handler) {
+    try {
       if (flow.type !== 'workflow') {
-        this.workflowState = null;
-        this.currentWorkflow = null;
-        this.workflowEngine = null;
+        this.executedWorkflow = {
+          data: null,
+          state: null,
+        };
       }
 
-      handler
-        .call(this, flow)
-        .then((data) => {
-          this.logs.push({
-            type: data.type || 'success',
-            name: data.name,
-            logId: data.id,
-            message: data.message,
-            duration: Math.round(Date.now() - started),
-          });
-
-          this.nextFlow();
-        })
-        .catch((error) => {
-          this.logs.push({
-            type: 'error',
-            name: error.name,
-            logId: error.id,
-            message: error.message,
-            duration: Math.round(Date.now() - started),
-          });
-
-          this.nextFlow();
-        });
-    } else {
-      console.error(`"${flow.type}" flow doesn't have a handler`);
+      const data = await handler.call(this, flow);
+
+      this.history.push({
+        type: data.type || 'success',
+        name: data.name,
+        logId: data.id,
+        message: data.message,
+        duration: Math.round(Date.now() - started),
+      });
+      this.nextFlow();
+    } catch (error) {
+      this.history.push({
+        type: 'error',
+        name: error.name,
+        logId: error.id,
+        message: error.message,
+        duration: Math.round(Date.now() - started),
+      });
+
+      this.nextFlow();
     }
   }
 }

+ 9 - 12
src/background/index.js

@@ -2,7 +2,7 @@ import browser from 'webextension-polyfill';
 import { MessageListener } from '@/utils/message';
 import { registerSpecificDay } from '../utils/workflow-trigger';
 import WorkflowState from './workflow-state';
-// import CollectionEngine from './collection-engine';
+import CollectionEngine from './collection-engine';
 import WorkflowEngine from './workflow-engine/engine';
 import blocksHandler from './workflow-engine/blocks-handler';
 import WorkflowLogger from './workflow-logger';
@@ -19,11 +19,11 @@ const storage = {
     }
   },
   async set(key, value) {
+    await browser.storage.local.set({ [key]: value });
+
     if (key === 'workflowState') {
       sessionStorage.setItem(key, JSON.stringify(value));
     }
-
-    await browser.storage.local.set({ [key]: value });
   },
 };
 const workflow = {
@@ -169,15 +169,12 @@ 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('collection:execute', (collection) => {
+  const engine = new CollectionEngine(collection, {
+    states: workflow.states,
+    logger: workflow.logger,
+  });
+  engine.init();
 });
 
 message.on('workflow:execute', (param) => {

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

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

+ 4 - 5
src/background/workflow-engine/engine.js

@@ -52,7 +52,6 @@ class WorkflowEngine {
 
     this.onWorkflowStopped = (id) => {
       if (this.id !== id || this.isDestroyed) return;
-
       this.stop();
     };
   }
@@ -194,6 +193,9 @@ class WorkflowEngine {
         });
       }
 
+      this.states.off('stop', this.onWorkflowStopped);
+      await this.states.delete(this.id);
+
       this.dispatchEvent('destroyed', {
         status,
         message,
@@ -201,9 +203,6 @@ class WorkflowEngine {
         currentBlock: this.currentBlock,
       });
 
-      this.states.off('stop', this.onWorkflowStopped);
-      await this.states.delete(this.id);
-
       this.isDestroyed = true;
       this.eventListeners = {};
     } catch (error) {
@@ -224,6 +223,7 @@ class WorkflowEngine {
     this.currentBlock = block;
 
     await this.states.update(this.id, { state: this.state });
+    this.dispatchEvent('update', { state: this.state });
 
     const startExecutedTime = Date.now();
     const blockHandler = this.blocksHandler[toCamelCase(block?.name)];
@@ -334,7 +334,6 @@ class WorkflowEngine {
           browser.tabs
             .get(this.activeTab.id)
             .then((tab) => {
-              console.log('Tab status:\t', tab.status);
               if (tab.status === 'loading') {
                 setTimeout(() => {
                   activeTabStatus();

+ 1 - 1
src/background/workflow-logger.js

@@ -5,7 +5,7 @@ class WorkflowLogger {
   }
 
   async add(data) {
-    const logs = (await this.storage.get(this.key)) || {};
+    const logs = (await this.storage.get(this.key)) || [];
 
     logs.unshift(data);
 

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

@@ -4,6 +4,8 @@ class WorkflowState {
   constructor({ storage, key = 'workflowState' }) {
     this.key = key;
     this.storage = storage;
+
+    this.cache = null;
     this.eventListeners = {};
   }
 
@@ -52,7 +54,7 @@ class WorkflowState {
 
   async get(stateId) {
     try {
-      let states = (await this.storage.get(this.key)) || {};
+      let states = this.cache ?? ((await this.storage.get(this.key)) || {});
 
       if (Array.isArray(states)) {
         states = {};
@@ -63,6 +65,8 @@ class WorkflowState {
         states = Object.values(states).find(stateId);
       } else if (stateId) {
         states = states[stateId];
+      } else {
+        this.cache = states;
       }
 
       return states;

+ 9 - 8
src/components/newtab/logs/LogsDataViewer.vue

@@ -27,7 +27,7 @@
     </ui-popover>
   </div>
   <shared-codemirror
-    :model-value="jsonData"
+    :model-value="dataStr"
     :class="editorClass"
     lang="json"
     readonly
@@ -35,7 +35,7 @@
   />
 </template>
 <script setup>
-import { ref } from 'vue';
+import { ref, computed } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { dataExportTypes } from '@/utils/shared';
 import dataExporter, { generateJSON } from '@/utils/data-exporter';
@@ -54,12 +54,13 @@ const props = defineProps({
 
 const { t } = useI18n();
 
-const data = Array.isArray(props.log.data)
-  ? props.log.data
-  : generateJSON(Object.keys(props.log.data), props.log.data);
-const dataStr = JSON.stringify(data, null, 2);
-const jsonData =
-  dataStr.length >= 5e4 ? `${dataStr.slice(0, 5e4)}\n...` : dataStr;
+const dataStr = computed(() => {
+  const data = Array.isArray(props.log.data)
+    ? props.log.data
+    : generateJSON(Object.keys(props.log.data), props.log.data);
+
+  return JSON.stringify(data, null, 2);
+});
 
 const fileName = ref(props.log.name);
 

+ 18 - 20
src/components/newtab/shared/SharedWorkflowState.vue

@@ -22,11 +22,7 @@
       >
         <v-remixicon name="riExternalLinkLine" />
       </ui-button>
-      <ui-button
-        variant="accent"
-        :disabled="!!data.state.parentState"
-        @click="stopWorkflow"
-      >
+      <ui-button variant="accent" @click="stopWorkflow">
         <v-remixicon name="riStopLine" class="mr-2 -ml-1" />
         <span>{{ t('common.stop') }}</span>
       </ui-button>
@@ -34,7 +30,7 @@
     <div class="divide-y bg-box-transparent divide-y px-4 rounded-lg">
       <div
         v-for="block in getBlock()"
-        :key="block.name"
+        :key="block.id || block.name"
         class="flex items-center py-2"
       >
         <v-remixicon :name="block.icon" />
@@ -49,7 +45,9 @@
       {{ t('workflow.state.executeBy', { name: data.parentState.name }) }}
       <span class="lowercase">
         {{
-          data.isInCollection ? t('common.collection') : t('common.workflow')
+          data.parentState.isCollection
+            ? t('common.collection')
+            : t('common.workflow')
         }}
       </span>
     </div>
@@ -70,23 +68,23 @@ const props = defineProps({
 });
 
 const { t } = useI18n();
-console.log(props.data);
+
 function getBlock() {
-  if (!props.data.state.currentBlock) return [];
+  const block = props.data.state.currentBlock;
+
+  if (!block) return [];
 
-  if (Array.isArray(props.data.state.currentBlock)) {
-    return props.data.state.currentBlock.map((item) => {
-      if (tasks[item.name])
-        return {
-          ...tasks[item.name],
-          name: t(`workflow.blocks.${item.name}.name`),
-        };
+  const blockArr = Array.isArray(block) ? block : [block];
 
-      return item;
-    });
-  }
+  return blockArr.map((item) => {
+    if (tasks[item.name] && item.outputs)
+      return {
+        ...tasks[item.name],
+        name: t(`workflow.blocks.${item.name}.name`),
+      };
 
-  return [tasks[props.data.state.currentBlock.name]];
+    return item;
+  });
 }
 function formatDate(date, format) {
   if (format === 'relative') return dayjs(date).fromNow();

+ 0 - 1
src/content/index.js

@@ -22,7 +22,6 @@ 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();

+ 2 - 1
src/newtab/App.vue

@@ -56,7 +56,8 @@ function handleStorageChanged(change) {
     store.commit('updateState', {
       key: 'workflowState',
       value: Object.values(change.workflowState.newValue || {}).filter(
-        ({ isDestroyed }) => !isDestroyed
+        ({ isDestroyed, parentState }) =>
+          !isDestroyed && !parentState?.isCollection
       ),
     });
   }

+ 1 - 1
src/newtab/pages/Collections.vue

@@ -41,7 +41,7 @@
         icon="riFolderLine"
         @click="$router.push(`/collections/${$event.id}`)"
         @execute="executeCollection"
-        @menuSelected="menuHandlers[$event.name]($event.data)"
+        @menuSelected="menuHandlers[$event.id]($event.data)"
       />
     </div>
   </div>

+ 1 - 3
src/newtab/pages/collections/[id].vue

@@ -243,9 +243,7 @@ const collectionOptions = shallowReactive({
 });
 
 const runningCollection = computed(() =>
-  store.state.workflowState.filter(
-    ({ collectionId }) => collectionId === route.params.id
-  )
+  store.state.workflowState.filter(({ id }) => id === route.params.id)
 );
 const logs = computed(() =>
   Log.query()

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

@@ -176,6 +176,7 @@ const history = computed(() =>
     )
     .map(translateLog)
 );
+
 const collectionLog = computed(() => {
   if (activeLog.value.parentLog) {
     return Log.find(activeLog.value.parentLog.id);
@@ -183,7 +184,6 @@ const collectionLog = computed(() => {
 
   return Log.find(activeLog.value.collectionLogId);
 });
-console.log(history.value);
 
 function deleteLog() {
   Log.delete(route.params.id).then(() => {

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

@@ -215,7 +215,7 @@ const logs = computed(() =>
     .where(
       (item) =>
         item.workflowId === workflowId &&
-        (!item.isInCollection || !item.isChildLog)
+        (!item.isInCollection || !item.isChildLog || !item.parentLog)
     )
     .orderBy('startedAt', 'desc')
     .get()

+ 2 - 1
src/store/index.js

@@ -85,7 +85,8 @@ const store = createStore({
         commit('updateState', {
           key: 'workflowState',
           value: Object.values(workflowState || {}).filter(
-            ({ isDestroyed }) => !isDestroyed
+            ({ isDestroyed, parentState }) =>
+              !isDestroyed && !parentState?.isCollection
           ),
         });
       } catch (error) {