worker.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. import { nanoid } from 'nanoid';
  2. import browser from 'webextension-polyfill';
  3. import { toCamelCase, sleep, objectHasKey, isObject } from '@/utils/helper';
  4. import { tasks } from '@/utils/shared';
  5. import referenceData from '@/utils/referenceData';
  6. import { convertData, waitTabLoaded, getBlockConnection } from './helper';
  7. class Worker {
  8. constructor(engine) {
  9. this.id = nanoid(5);
  10. this.engine = engine;
  11. this.settings = engine.workflow.settings;
  12. this.loopList = {};
  13. this.repeatedTasks = {};
  14. this.preloadScripts = [];
  15. this.windowId = null;
  16. this.currentBlock = null;
  17. this.childWorkflowId = null;
  18. this.debugAttached = false;
  19. this.activeTab = {
  20. url: '',
  21. frameId: 0,
  22. frames: {},
  23. groupId: null,
  24. id: engine.options?.tabId,
  25. };
  26. }
  27. init({ blockId, prevBlockData, state }) {
  28. if (state) {
  29. Object.keys(state).forEach((key) => {
  30. this[key] = state[key];
  31. });
  32. }
  33. const block = this.engine.blocks[blockId];
  34. this.executeBlock(block, prevBlockData);
  35. }
  36. addDataToColumn(key, value) {
  37. if (Array.isArray(key)) {
  38. key.forEach((item) => {
  39. if (!isObject(item)) return;
  40. Object.entries(item).forEach(([itemKey, itemValue]) => {
  41. this.addDataToColumn(itemKey, itemValue);
  42. });
  43. });
  44. return;
  45. }
  46. const insertDefault = this.settings.insertDefaultColumn ?? true;
  47. const columnId =
  48. (this.engine.columns[key] ? key : this.engine.columnsId[key]) || 'column';
  49. if (columnId === 'column' && !insertDefault) return;
  50. const currentColumn = this.engine.columns[columnId];
  51. const columnName = currentColumn.name || 'column';
  52. const convertedValue = convertData(value, currentColumn.type);
  53. if (objectHasKey(this.engine.referenceData.table, currentColumn.index)) {
  54. this.engine.referenceData.table[currentColumn.index][columnName] =
  55. convertedValue;
  56. } else {
  57. this.engine.referenceData.table.push({ [columnName]: convertedValue });
  58. }
  59. currentColumn.index += 1;
  60. }
  61. setVariable(name, value) {
  62. this.engine.referenceData.variables[name] = value;
  63. }
  64. executeNextBlocks(connections, prevBlockData) {
  65. connections.forEach(({ node }, index) => {
  66. if (index === 0) {
  67. this.executeBlock(this.engine.blocks[node], prevBlockData);
  68. } else {
  69. const state = structuredClone({
  70. windowId: this.windowId,
  71. loopList: this.loopList,
  72. activeTab: this.activeTab,
  73. currentBlock: this.currentBlock,
  74. repeatedTasks: this.repeatedTasks,
  75. preloadScripts: this.preloadScripts,
  76. });
  77. this.engine.addWorker({
  78. state,
  79. prevBlockData,
  80. blockId: node,
  81. });
  82. }
  83. });
  84. }
  85. async executeBlock(block, prevBlockData, isRetry) {
  86. const currentState = await this.engine.states.get(this.engine.id);
  87. if (!currentState || currentState.isDestroyed) {
  88. if (this.engine.isDestroyed) return;
  89. await this.engine.destroy('stopped');
  90. return;
  91. }
  92. const prevBlock = this.currentBlock;
  93. this.currentBlock = block;
  94. if (!isRetry) {
  95. await this.engine.updateState({
  96. activeTabUrl: this.activeTab.url,
  97. childWorkflowId: this.childWorkflowId,
  98. });
  99. }
  100. const startExecuteTime = Date.now();
  101. const blockHandler = this.engine.blocksHandler[toCamelCase(block.name)];
  102. const handler =
  103. !blockHandler && tasks[block.name].category === 'interaction'
  104. ? this.engine.blocksHandler.interactionBlock
  105. : blockHandler;
  106. if (!handler) {
  107. this.engine.destroy('stopped');
  108. return;
  109. }
  110. const refData = {
  111. prevBlockData,
  112. ...this.engine.referenceData,
  113. activeTabUrl: this.activeTab.url,
  114. };
  115. const replacedBlock = referenceData({
  116. block,
  117. data: refData,
  118. refKeys:
  119. isRetry || block.data.disableBlock
  120. ? null
  121. : tasks[block.name].refDataKeys,
  122. });
  123. const blockDelay = this.settings?.blockDelay || 0;
  124. const addBlockLog = (status, obj = {}) => {
  125. this.engine.addLogHistory({
  126. prevBlockData,
  127. type: status,
  128. name: block.name,
  129. workerId: this.id,
  130. description: block.data.description,
  131. replacedValue: replacedBlock.replacedValue,
  132. duration: Math.round(Date.now() - startExecuteTime),
  133. ...obj,
  134. });
  135. };
  136. try {
  137. let result;
  138. if (block.data.disableBlock) {
  139. result = {
  140. data: '',
  141. nextBlockId: getBlockConnection(block),
  142. };
  143. } else {
  144. result = await handler.call(this, replacedBlock, {
  145. refData,
  146. prevBlock,
  147. prevBlockData,
  148. });
  149. if (result.replacedValue) {
  150. replacedBlock.replacedValue = result.replacedValue;
  151. }
  152. addBlockLog(result.status || 'success', {
  153. logId: result.logId,
  154. });
  155. }
  156. let nodeConnections = null;
  157. if (typeof result.nextBlockId === 'string') {
  158. nodeConnections = [{ node: result.nextBlockId }];
  159. } else {
  160. nodeConnections = result.nextBlockId.connections;
  161. }
  162. if (nodeConnections.length > 0 && !result.destroyWorker) {
  163. setTimeout(() => {
  164. this.executeNextBlocks(nodeConnections, result.data);
  165. }, blockDelay);
  166. } else {
  167. this.engine.destroyWorker(this.id);
  168. }
  169. } catch (error) {
  170. console.error(error);
  171. const { onError: blockOnError } = replacedBlock.data;
  172. if (blockOnError && blockOnError.enable) {
  173. if (blockOnError.retry && blockOnError.retryTimes) {
  174. await sleep(blockOnError.retryInterval * 1000);
  175. blockOnError.retryTimes -= 1;
  176. await this.executeBlock(replacedBlock, prevBlockData, true);
  177. return;
  178. }
  179. const nextBlocks = getBlockConnection(
  180. block,
  181. blockOnError.toDo === 'continue' ? 1 : 2
  182. );
  183. if (blockOnError.toDo !== 'error' && nextBlocks?.connections) {
  184. addBlockLog('error', {
  185. message: error.message,
  186. ...(error.data || {}),
  187. });
  188. this.executeNextBlocks(nextBlocks.connections, prevBlockData);
  189. return;
  190. }
  191. }
  192. addBlockLog('error', {
  193. message: error.message,
  194. ...(error.data || {}),
  195. });
  196. const { onError } = this.settings;
  197. const nodeConnections = error.nextBlockId?.connections;
  198. if (onError === 'keep-running' && nodeConnections) {
  199. setTimeout(() => {
  200. this.executeNextBlocks(nodeConnections, error.data || '');
  201. }, blockDelay);
  202. } else if (onError === 'restart-workflow' && !this.parentWorkflow) {
  203. const restartKey = `restart-count:${this.id}`;
  204. const restartCount = +localStorage.getItem(restartKey) || 0;
  205. const maxRestart = this.settings.restartTimes ?? 3;
  206. if (restartCount >= maxRestart) {
  207. localStorage.removeItem(restartKey);
  208. this.engine.destroy();
  209. return;
  210. }
  211. this.reset();
  212. const triggerBlock = Object.values(this.engine.blocks).find(
  213. ({ name }) => name === 'trigger'
  214. );
  215. this.executeBlock(triggerBlock);
  216. localStorage.setItem(restartKey, restartCount + 1);
  217. } else {
  218. this.engine.destroy('error', error.message);
  219. }
  220. }
  221. }
  222. reset() {
  223. this.loopList = {};
  224. this.repeatedTasks = {};
  225. this.windowId = null;
  226. this.currentBlock = null;
  227. this.childWorkflowId = null;
  228. this.engine.history = [];
  229. this.engine.preloadScripts = [];
  230. this.engine.columns = {
  231. column: {
  232. index: 0,
  233. type: 'any',
  234. name: this.settings?.defaultColumnName || 'column',
  235. },
  236. };
  237. this.activeTab = {
  238. url: '',
  239. frameId: 0,
  240. frames: {},
  241. groupId: null,
  242. id: this.options?.tabId,
  243. };
  244. this.engine.referenceData = {
  245. table: [],
  246. loopData: {},
  247. workflow: {},
  248. googleSheets: {},
  249. variables: this.engine.options.variables,
  250. globalData: this.engine.referenceData.globalData,
  251. };
  252. }
  253. async _sendMessageToTab(payload, options = {}) {
  254. try {
  255. if (!this.activeTab.id) {
  256. const error = new Error('no-tab');
  257. error.workflowId = this.id;
  258. throw error;
  259. }
  260. await waitTabLoaded(
  261. this.activeTab.id,
  262. this.settings?.tabLoadTimeout ?? 30000
  263. );
  264. const { executedBlockOnWeb, debugMode } = this.settings;
  265. const messagePayload = {
  266. isBlock: true,
  267. debugMode,
  268. executedBlockOnWeb,
  269. activeTabId: this.activeTab.id,
  270. frameSelector: this.frameSelector,
  271. ...payload,
  272. };
  273. const data = await browser.tabs.sendMessage(
  274. this.activeTab.id,
  275. messagePayload,
  276. { frameId: this.activeTab.frameId, ...options }
  277. );
  278. return data;
  279. } catch (error) {
  280. console.error(error);
  281. if (error.message?.startsWith('Could not establish connection')) {
  282. error.message = 'Could not establish connection to the active tab';
  283. } else if (error.message?.startsWith('No tab')) {
  284. error.message = 'active-tab-removed';
  285. }
  286. throw error;
  287. }
  288. }
  289. }
  290. export default Worker;