|
@@ -1,17 +1,8 @@
|
|
|
import browser from 'webextension-polyfill';
|
|
|
import { nanoid } from 'nanoid';
|
|
|
import { tasks } from '@/utils/shared';
|
|
|
-import {
|
|
|
- clearCache,
|
|
|
- toCamelCase,
|
|
|
- sleep,
|
|
|
- parseJSON,
|
|
|
- isObject,
|
|
|
- objectHasKey,
|
|
|
-} from '@/utils/helper';
|
|
|
-import referenceData from '@/utils/reference-data';
|
|
|
-import { convertData, waitTabLoaded, getBlockConnection } from './helper';
|
|
|
-import executeContentScript from './execute-content-script';
|
|
|
+import { clearCache, sleep, parseJSON, isObject } from '@/utils/helper';
|
|
|
+import Worker from './worker';
|
|
|
|
|
|
class WorkflowEngine {
|
|
|
constructor(
|
|
@@ -26,13 +17,7 @@ class WorkflowEngine {
|
|
|
this.parentWorkflow = parentWorkflow;
|
|
|
this.saveLog = workflow.settings?.saveLog ?? true;
|
|
|
|
|
|
- this.loopList = {};
|
|
|
- this.repeatedTasks = {};
|
|
|
-
|
|
|
- this.windowId = null;
|
|
|
- this.triggerBlock = null;
|
|
|
- this.currentBlock = null;
|
|
|
- this.childWorkflowId = null;
|
|
|
+ this.workers = new Map();
|
|
|
|
|
|
this.isDestroyed = false;
|
|
|
this.isUsingProxy = false;
|
|
@@ -58,13 +43,6 @@ class WorkflowEngine {
|
|
|
}
|
|
|
this.options = options;
|
|
|
|
|
|
- this.activeTab = {
|
|
|
- url: '',
|
|
|
- frameId: 0,
|
|
|
- frames: {},
|
|
|
- groupId: null,
|
|
|
- id: options?.tabId,
|
|
|
- };
|
|
|
this.referenceData = {
|
|
|
variables,
|
|
|
table: [],
|
|
@@ -87,38 +65,6 @@ class WorkflowEngine {
|
|
|
};
|
|
|
}
|
|
|
|
|
|
- reset() {
|
|
|
- this.loopList = {};
|
|
|
- this.repeatedTasks = {};
|
|
|
-
|
|
|
- this.windowId = null;
|
|
|
- this.currentBlock = null;
|
|
|
- this.childWorkflowId = null;
|
|
|
-
|
|
|
- this.isDestroyed = false;
|
|
|
- this.isUsingProxy = false;
|
|
|
-
|
|
|
- this.history = [];
|
|
|
- this.preloadScripts = [];
|
|
|
- this.columns = { column: { index: 0, name: 'column', type: 'any' } };
|
|
|
-
|
|
|
- this.activeTab = {
|
|
|
- url: '',
|
|
|
- frameId: 0,
|
|
|
- frames: {},
|
|
|
- groupId: null,
|
|
|
- id: this.options?.tabId,
|
|
|
- };
|
|
|
- this.referenceData = {
|
|
|
- table: [],
|
|
|
- loopData: {},
|
|
|
- workflow: {},
|
|
|
- googleSheets: {},
|
|
|
- variables: this.options.variables,
|
|
|
- globalData: this.referenceData.globalData,
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
init() {
|
|
|
if (this.workflow.isDisabled) return;
|
|
|
|
|
@@ -174,7 +120,6 @@ class WorkflowEngine {
|
|
|
this.blocks = blocks;
|
|
|
this.startedTimestamp = Date.now();
|
|
|
this.workflow.table = columns;
|
|
|
- this.currentBlock = triggerBlock;
|
|
|
|
|
|
this.states.on('stop', this.onWorkflowStopped);
|
|
|
|
|
@@ -185,7 +130,7 @@ class WorkflowEngine {
|
|
|
parentState: this.parentWorkflow,
|
|
|
})
|
|
|
.then(() => {
|
|
|
- this.executeBlock(this.currentBlock);
|
|
|
+ this.addWorker({ blockId: triggerBlock.id });
|
|
|
});
|
|
|
}
|
|
|
|
|
@@ -199,6 +144,13 @@ class WorkflowEngine {
|
|
|
this.init(state.currentBlock);
|
|
|
}
|
|
|
|
|
|
+ addWorker(detail) {
|
|
|
+ const worker = new Worker(this);
|
|
|
+ worker.init(detail);
|
|
|
+
|
|
|
+ this.workers.set(worker.id, worker);
|
|
|
+ }
|
|
|
+
|
|
|
addLogHistory(detail) {
|
|
|
if (
|
|
|
!this.saveLog &&
|
|
@@ -214,7 +166,7 @@ class WorkflowEngine {
|
|
|
detail.replacedValue ||
|
|
|
(tasks[detail.name]?.refDataKeys && this.saveLog)
|
|
|
) {
|
|
|
- const { activeTabUrl, variables, loopData, prevBlockData } = JSON.parse(
|
|
|
+ const { activeTabUrl, variables, loopData } = JSON.parse(
|
|
|
JSON.stringify(this.referenceData)
|
|
|
);
|
|
|
|
|
@@ -223,7 +175,7 @@ class WorkflowEngine {
|
|
|
loopData,
|
|
|
variables,
|
|
|
activeTabUrl,
|
|
|
- prevBlockData,
|
|
|
+ prevBlockData: detail.prevBlockData || '',
|
|
|
},
|
|
|
replacedValue: detail.replacedValue,
|
|
|
};
|
|
@@ -234,39 +186,6 @@ class WorkflowEngine {
|
|
|
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 columnId =
|
|
|
- (this.columns[key] ? key : this.columnsId[key]) || 'column';
|
|
|
- const currentColumn = this.columns[columnId];
|
|
|
- const columnName = currentColumn.name || 'column';
|
|
|
- const convertedValue = convertData(value, currentColumn.type);
|
|
|
-
|
|
|
- if (objectHasKey(this.referenceData.table, currentColumn.index)) {
|
|
|
- this.referenceData.table[currentColumn.index][columnName] =
|
|
|
- convertedValue;
|
|
|
- } else {
|
|
|
- this.referenceData.table.push({ [columnName]: convertedValue });
|
|
|
- }
|
|
|
-
|
|
|
- currentColumn.index += 1;
|
|
|
- }
|
|
|
-
|
|
|
- setVariable(name, value) {
|
|
|
- this.referenceData.variables[name] = value;
|
|
|
- }
|
|
|
-
|
|
|
async stop() {
|
|
|
try {
|
|
|
if (this.childWorkflowId) {
|
|
@@ -297,6 +216,19 @@ class WorkflowEngine {
|
|
|
await browser.storage.local.set({ workflowQueue });
|
|
|
}
|
|
|
|
|
|
+ destroyWorker(workerId) {
|
|
|
+ this.workers.delete(workerId);
|
|
|
+
|
|
|
+ if (this.workers.size === 0) {
|
|
|
+ this.addLogHistory({
|
|
|
+ type: 'finish',
|
|
|
+ name: 'finish',
|
|
|
+ });
|
|
|
+ this.dispatchEvent('finish');
|
|
|
+ this.destroy('success');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
async destroy(status, message) {
|
|
|
try {
|
|
|
if (this.isDestroyed) return;
|
|
@@ -311,6 +243,7 @@ class WorkflowEngine {
|
|
|
}
|
|
|
|
|
|
const endedTimestamp = Date.now();
|
|
|
+ this.workers.clear();
|
|
|
this.executeQueue();
|
|
|
|
|
|
if (!this.workflow.isTesting) {
|
|
@@ -371,151 +304,21 @@ class WorkflowEngine {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- async executeBlock(block, prevBlockData, isRetry) {
|
|
|
- const currentState = await this.states.get(this.id);
|
|
|
-
|
|
|
- if (!currentState || currentState.isDestroyed) {
|
|
|
- if (this.isDestroyed) return;
|
|
|
-
|
|
|
- await this.destroy('stopped');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- this.currentBlock = block;
|
|
|
- this.referenceData.prevBlockData = prevBlockData;
|
|
|
- this.referenceData.activeTabUrl = this.activeTab.url || '';
|
|
|
-
|
|
|
- if (!isRetry) {
|
|
|
- await this.states.update(this.id, { state: this.state });
|
|
|
- this.dispatchEvent('update', { state: this.state });
|
|
|
- }
|
|
|
-
|
|
|
- const startExecuteTime = 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,
|
|
|
- refKeys:
|
|
|
- isRetry || block.data.disableBlock
|
|
|
- ? null
|
|
|
- : tasks[block.name].refDataKeys,
|
|
|
- });
|
|
|
- const blockDelay = this.workflow.settings?.blockDelay || 0;
|
|
|
- const addBlockLog = (status, obj = {}) => {
|
|
|
- this.addLogHistory({
|
|
|
- type: status,
|
|
|
- name: block.name,
|
|
|
- description: block.data.description,
|
|
|
- replacedValue: replacedBlock.replacedValue,
|
|
|
- duration: Math.round(Date.now() - startExecuteTime),
|
|
|
- ...obj,
|
|
|
- });
|
|
|
+ async updateState(data) {
|
|
|
+ const state = {
|
|
|
+ ...this.state,
|
|
|
+ ...data,
|
|
|
+ tabIds: [],
|
|
|
+ currentBlock: [],
|
|
|
};
|
|
|
|
|
|
- try {
|
|
|
- let result;
|
|
|
-
|
|
|
- if (block.data.disableBlock) {
|
|
|
- result = {
|
|
|
- data: '',
|
|
|
- nextBlockId: getBlockConnection(block),
|
|
|
- };
|
|
|
- } else {
|
|
|
- result = await handler.call(this, replacedBlock, {
|
|
|
- prevBlockData,
|
|
|
- refData: this.referenceData,
|
|
|
- });
|
|
|
-
|
|
|
- if (result.replacedValue) {
|
|
|
- replacedBlock.replacedValue = result.replacedValue;
|
|
|
- }
|
|
|
-
|
|
|
- addBlockLog(result.status || 'success', {
|
|
|
- logId: result.logId,
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- 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) {
|
|
|
- const { onError: blockOnError } = replacedBlock.data;
|
|
|
- if (blockOnError && blockOnError.enable) {
|
|
|
- if (blockOnError.retry && blockOnError.retryTimes) {
|
|
|
- await sleep(blockOnError.retryInterval * 1000);
|
|
|
- blockOnError.retryTimes -= 1;
|
|
|
- await this.executeBlock(replacedBlock, prevBlockData, true);
|
|
|
-
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- const nextBlockId = getBlockConnection(
|
|
|
- block,
|
|
|
- blockOnError.toDo === 'continue' ? 1 : 2
|
|
|
- );
|
|
|
- if (blockOnError.toDo !== 'error' && nextBlockId) {
|
|
|
- this.executeBlock(this.blocks[nextBlockId], '');
|
|
|
- return;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- addBlockLog('error', {
|
|
|
- message: error.message,
|
|
|
- ...(error.data || {}),
|
|
|
- });
|
|
|
-
|
|
|
- const { onError } = this.workflow.settings;
|
|
|
-
|
|
|
- if (onError === 'keep-running' && error.nextBlockId) {
|
|
|
- setTimeout(() => {
|
|
|
- this.executeBlock(this.blocks[error.nextBlockId], error.data || '');
|
|
|
- }, blockDelay);
|
|
|
- } else if (onError === 'restart-workflow' && !this.parentWorkflow) {
|
|
|
- const restartKey = `restart-count:${this.id}`;
|
|
|
- const restartCount = +localStorage.getItem(restartKey) || 0;
|
|
|
- const maxRestart = this.workflow.settings.restartTimes ?? 3;
|
|
|
-
|
|
|
- if (restartCount >= maxRestart) {
|
|
|
- localStorage.removeItem(restartKey);
|
|
|
- this.destroy();
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- this.reset();
|
|
|
-
|
|
|
- const triggerBlock = Object.values(this.blocks).find(
|
|
|
- ({ name }) => name === 'trigger'
|
|
|
- );
|
|
|
- this.executeBlock(triggerBlock);
|
|
|
-
|
|
|
- localStorage.setItem(restartKey, restartCount + 1);
|
|
|
- } else {
|
|
|
- this.destroy('error', error.message);
|
|
|
- }
|
|
|
+ this.workers.forEach((worker) => {
|
|
|
+ state.tabIds.push(worker.activeTab.id);
|
|
|
+ state.currentBlock.push(worker.currentBlock);
|
|
|
+ });
|
|
|
|
|
|
- console.error(`${block.name}:`, error);
|
|
|
- }
|
|
|
+ await this.states.update(this.id, { state });
|
|
|
+ this.dispatchEvent('update', { state });
|
|
|
}
|
|
|
|
|
|
dispatchEvent(name, params) {
|
|
@@ -535,16 +338,7 @@ class WorkflowEngine {
|
|
|
}
|
|
|
|
|
|
get state() {
|
|
|
- const keys = [
|
|
|
- 'history',
|
|
|
- 'columns',
|
|
|
- 'activeTab',
|
|
|
- 'isUsingProxy',
|
|
|
- 'currentBlock',
|
|
|
- 'referenceData',
|
|
|
- 'childWorkflowId',
|
|
|
- 'startedTimestamp',
|
|
|
- ];
|
|
|
+ const keys = ['columns', 'referenceData', 'startedTimestamp'];
|
|
|
const state = {
|
|
|
name: this.workflow.name,
|
|
|
icon: this.workflow.icon,
|
|
@@ -556,49 +350,6 @@ class WorkflowEngine {
|
|
|
|
|
|
return state;
|
|
|
}
|
|
|
-
|
|
|
- async _sendMessageToTab(payload, options = {}) {
|
|
|
- try {
|
|
|
- if (!this.activeTab.id) {
|
|
|
- const error = new Error('no-tab');
|
|
|
- error.workflowId = this.id;
|
|
|
-
|
|
|
- throw error;
|
|
|
- }
|
|
|
-
|
|
|
- await waitTabLoaded(this.activeTab.id);
|
|
|
- await executeContentScript(
|
|
|
- this.activeTab.id,
|
|
|
- this.activeTab.frameId || 0
|
|
|
- );
|
|
|
-
|
|
|
- const { executedBlockOnWeb, debugMode } = this.workflow.settings;
|
|
|
- const messagePayload = {
|
|
|
- isBlock: true,
|
|
|
- debugMode,
|
|
|
- executedBlockOnWeb,
|
|
|
- activeTabId: this.activeTab.id,
|
|
|
- frameSelector: this.frameSelector,
|
|
|
- ...payload,
|
|
|
- };
|
|
|
-
|
|
|
- const data = await browser.tabs.sendMessage(
|
|
|
- this.activeTab.id,
|
|
|
- messagePayload,
|
|
|
- { frameId: this.activeTab.frameId, ...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;
|
|
|
- }
|
|
|
- }
|
|
|
}
|
|
|
|
|
|
export default WorkflowEngine;
|