123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514 |
- import browser from 'webextension-polyfill';
- import cloneDeep from 'lodash.clonedeep';
- import {
- toCamelCase,
- sleep,
- objectHasKey,
- parseJSON,
- isObject,
- } from '@/utils/helper';
- import templating from './templating';
- import renderString from './templating/renderString';
- import { convertData, waitTabLoaded } from './helper';
- import injectContentScript from './injectContentScript';
- function blockExecutionWrapper(blockHandler, blockData) {
- return new Promise((resolve, reject) => {
- let timeout = null;
- const timeoutMs = blockData?.settings?.blockTimeout;
- if (timeoutMs && timeoutMs > 0) {
- timeout = setTimeout(() => {
- reject(new Error('Timeout'));
- }, timeoutMs);
- }
- blockHandler()
- .then((result) => {
- resolve(result);
- })
- .catch((error) => {
- reject(error);
- })
- .finally(() => {
- if (timeout) clearTimeout(timeout);
- });
- });
- }
- class WorkflowWorker {
- constructor(id, engine, options = {}) {
- this.id = id;
- this.engine = engine;
- this.settings = engine.workflow.settings;
- this.blocksDetail = options.blocksDetail || {};
- this.loopEls = [];
- this.loopList = {};
- this.repeatedTasks = {};
- this.preloadScripts = [];
- this.breakpointState = null;
- this.windowId = null;
- this.currentBlock = null;
- this.childWorkflowId = null;
- this.debugAttached = false;
- this.activeTab = {
- url: '',
- frameId: 0,
- frames: {},
- groupId: null,
- id: engine.options?.tabId,
- };
- }
- init({ blockId, execParam, state }) {
- if (state) {
- Object.keys(state).forEach((key) => {
- this[key] = state[key];
- });
- }
- const block = this.engine.blocks[blockId];
- this.executeBlock(block, execParam);
- }
- 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 insertDefault = this.settings.insertDefaultColumn ?? true;
- const columnId =
- (this.engine.columns[key] ? key : this.engine.columnsId[key]) || 'column';
- if (columnId === 'column' && !insertDefault) return;
- const currentColumn = this.engine.columns[columnId];
- const columnName = currentColumn.name || 'column';
- const convertedValue = convertData(value, currentColumn.type);
- if (objectHasKey(this.engine.referenceData.table, currentColumn.index)) {
- this.engine.referenceData.table[currentColumn.index][columnName] =
- convertedValue;
- } else {
- this.engine.referenceData.table.push({
- [columnName]: convertedValue,
- });
- }
- currentColumn.index += 1;
- }
- setVariable(name, value) {
- const vars = this.engine.referenceData.variables;
- if (name.startsWith('$push:')) {
- const { 1: varName } = name.split('$push:');
- if (!objectHasKey(vars, varName)) vars[varName] = [];
- else if (!Array.isArray(vars[varName])) vars[varName] = [vars[varName]];
- vars[varName].push(value);
- }
- vars[name] = value;
- this.engine.addRefDataSnapshot('variables');
- }
- getBlockConnections(blockId, outputIndex = 1) {
- if (this.engine.isDestroyed) return null;
- const outputId = `${blockId}-output-${outputIndex}`;
- const connections = this.engine.connectionsMap[outputId];
- if (!connections) return null;
- return [...connections.values()];
- }
- executeNextBlocks(
- connections,
- prevBlockData,
- nextBlockBreakpointCount = null
- ) {
- connections.forEach((connection, index) => {
- const { id, targetHandle, sourceHandle } =
- typeof connection === 'string'
- ? { id: connection, targetHandle: '', sourceHandle: '' }
- : connection;
- const execParam = {
- prevBlockData,
- targetHandle,
- sourceHandle,
- nextBlockBreakpointCount,
- };
- if (index === 0) {
- this.executeBlock(this.engine.blocks[id], {
- prevBlockData,
- ...execParam,
- });
- } else {
- const state = cloneDeep({
- windowId: this.windowId,
- loopList: this.loopList,
- activeTab: this.activeTab,
- currentBlock: this.currentBlock,
- repeatedTasks: this.repeatedTasks,
- preloadScripts: this.preloadScripts,
- debugAttached: this.debugAttached,
- });
- this.engine.addWorker({
- state,
- execParam,
- blockId: id,
- });
- }
- });
- }
- resume(nextBlock) {
- if (!this.breakpointState) return;
- const { block, execParam, isRetry } = this.breakpointState;
- const payload = { ...execParam, resume: true };
- payload.nextBlockBreakpointCount = nextBlock ? 1 : null;
- this.executeBlock(block, payload, isRetry);
- this.breakpointState = null;
- }
- async executeBlock(block, execParam = {}, isRetry = false) {
- const currentState = await this.engine.states.get(this.engine.id);
- if (!currentState || currentState.isDestroyed) {
- if (this.engine.isDestroyed) return;
- await this.engine.destroy('stopped');
- return;
- }
- const startExecuteTime = Date.now();
- const prevBlock = this.currentBlock;
- this.currentBlock = { ...block, startedAt: startExecuteTime };
- const isInBreakpoint =
- this.engine.isTestingMode &&
- ((block.data?.$breakpoint && !execParam.resume) ||
- execParam.nextBlockBreakpointCount === 0);
- if (!isRetry) {
- const payload = {
- activeTabUrl: this.activeTab.url,
- childWorkflowId: this.childWorkflowId,
- nextBlockBreakpoint: Boolean(execParam.nextBlockBreakpointCount),
- };
- if (isInBreakpoint && currentState.status !== 'breakpoint')
- payload.status = 'breakpoint';
- await this.engine.updateState(payload);
- }
- if (execParam.nextBlockBreakpointCount) {
- execParam.nextBlockBreakpointCount -= 1;
- }
- if (isInBreakpoint || currentState.status === 'breakpoint') {
- this.engine.isInBreakpoint = true;
- this.breakpointState = { block, execParam, isRetry };
- return;
- }
- const blockHandler = this.engine.blocksHandler[toCamelCase(block.label)];
- const handler =
- !blockHandler && this.blocksDetail[block.label].category === 'interaction'
- ? this.engine.blocksHandler.interactionBlock
- : blockHandler;
- if (!handler) {
- console.error(`${block.label} doesn't have handler`);
- this.engine.destroy('stopped');
- return;
- }
- const { prevBlockData } = execParam;
- const refData = {
- prevBlockData,
- ...this.engine.referenceData,
- activeTabUrl: this.activeTab.url,
- };
- const replacedBlock = await templating({
- block,
- data: refData,
- isPopup: this.engine.isPopup,
- refKeys:
- isRetry || block.data.disableBlock
- ? null
- : this.blocksDetail[block.label].refDataKeys,
- });
- const blockDelay = this.settings?.blockDelay || 0;
- const addBlockLog = (status, obj = {}) => {
- let { description } = block.data;
- if (block.label === 'loop-breakpoint') description = block.data.loopId;
- else if (block.label === 'block-package') description = block.data.name;
- this.engine.addLogHistory({
- description,
- prevBlockData,
- type: status,
- name: block.label,
- blockId: block.id,
- workerId: this.id,
- timestamp: startExecuteTime,
- activeTabUrl: this.activeTab?.url,
- replacedValue: replacedBlock.replacedValue,
- duration: Math.round(Date.now() - startExecuteTime),
- ...obj,
- });
- };
- const executeBlocks = (blocks, data) => {
- return this.executeNextBlocks(
- blocks,
- data,
- execParam.nextBlockBreakpointCount
- );
- };
- try {
- let result;
- if (block.data.disableBlock) {
- result = {
- data: '',
- nextBlockId: this.getBlockConnections(block.id),
- };
- } else {
- const bindedHandler = handler.bind(this, replacedBlock, {
- refData,
- prevBlock,
- ...(execParam || {}),
- });
- result = await blockExecutionWrapper(bindedHandler, block.data);
- if (this.engine.isDestroyed) return;
- if (result.replacedValue) {
- replacedBlock.replacedValue = result.replacedValue;
- }
- addBlockLog(result.status || 'success', {
- logId: result.logId,
- ctxData: result?.ctxData,
- });
- }
- if (result.nextBlockId && !result.destroyWorker) {
- if (blockDelay > 0) {
- setTimeout(() => {
- executeBlocks(result.nextBlockId, result.data);
- }, blockDelay);
- } else {
- executeBlocks(result.nextBlockId, result.data);
- }
- } else {
- this.engine.destroyWorker(this.id);
- }
- } catch (error) {
- console.error(error);
- const errorLogData = {
- message: error.message,
- ...(error.data || {}),
- ...(error.ctxData || {}),
- };
- 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, execParam, true);
- return;
- }
- if (blockOnError.insertData) {
- for (const item of blockOnError.dataToInsert) {
- let value = (
- await renderString(item.value, refData, this.engine.isPopup)
- )?.value;
- value = parseJSON(value, value);
- if (item.type === 'variable') {
- this.setVariable(item.name, value);
- } else {
- this.addDataToColumn(item.name, value);
- }
- }
- }
- const nextBlocks = this.getBlockConnections(
- block.id,
- blockOnError.toDo === 'continue' ? 1 : 'fallback'
- );
- if (blockOnError.toDo !== 'error' && nextBlocks) {
- addBlockLog('error', errorLogData);
- executeBlocks(nextBlocks, prevBlockData);
- return;
- }
- }
- const errorLogItem = errorLogData;
- addBlockLog('error', errorLogItem);
- errorLogItem.blockId = block.id;
- const { onError } = this.settings;
- const nodeConnections = this.getBlockConnections(block.id);
- if (onError === 'keep-running' && nodeConnections) {
- setTimeout(() => {
- executeBlocks(nodeConnections, error.data || '');
- }, blockDelay);
- } else if (onError === 'restart-workflow' && !this.parentWorkflow) {
- const restartCount = this.engine.restartWorkersCount[this.id] || 0;
- const maxRestart = this.settings.restartTimes ?? 3;
- if (restartCount >= maxRestart) {
- delete this.engine.restartWorkersCount[this.id];
- this.engine.destroy('error', error.message, errorLogItem);
- return;
- }
- this.reset();
- const triggerBlock = this.engine.blocks[this.engine.triggerBlockId];
- if (triggerBlock) this.executeBlock(triggerBlock, execParam);
- this.engine.restartWorkersCount[this.id] = restartCount + 1;
- } else {
- this.engine.destroy('error', error.message, errorLogItem);
- }
- }
- }
- reset() {
- this.loopList = {};
- this.repeatedTasks = {};
- this.windowId = null;
- this.currentBlock = null;
- this.childWorkflowId = null;
- this.engine.history = [];
- this.engine.preloadScripts = [];
- this.engine.columns = {
- column: {
- index: 0,
- type: 'any',
- name: this.settings?.defaultColumnName || 'column',
- },
- };
- this.activeTab = {
- url: '',
- frameId: 0,
- frames: {},
- groupId: null,
- id: this.options?.tabId,
- };
- this.engine.referenceData = {
- table: [],
- loopData: {},
- workflow: {},
- googleSheets: {},
- variables: this.engine.options?.variables || {},
- globalData: this.engine.referenceData.globalData,
- };
- }
- async _sendMessageToTab(payload, options = {}, runBeforeLoad = false) {
- try {
- if (!this.activeTab.id) {
- const error = new Error('no-tab');
- error.workflowId = this.id;
- throw error;
- }
- if (!runBeforeLoad) {
- await waitTabLoaded({
- tabId: this.activeTab.id,
- ms: this.settings?.tabLoadTimeout ?? 30000,
- });
- }
- const { executedBlockOnWeb, debugMode } = this.settings;
- const messagePayload = {
- isBlock: true,
- debugMode,
- executedBlockOnWeb,
- loopEls: this.loopEls,
- 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) {
- console.error(error);
- const noConnection = error.message?.includes(
- 'Could not establish connection'
- );
- const channelClosed = error.message?.includes('message channel closed');
- if (noConnection || channelClosed) {
- const isScriptInjected = await injectContentScript(
- this.activeTab.id,
- this.activeTab.frameId
- );
- if (isScriptInjected) {
- const result = await this._sendMessageToTab(
- payload,
- options,
- runBeforeLoad
- );
- return result;
- }
- 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 WorkflowWorker;
|