workflow-engine.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. /* eslint-disable no-underscore-dangle */
  2. import browser from 'webextension-polyfill';
  3. import { nanoid } from 'nanoid';
  4. import { toCamelCase } from '@/utils/helper';
  5. import { tasks } from '@/utils/shared';
  6. import errorMessage from './error-message';
  7. import workflowState from './workflow-state';
  8. import * as blocksHandler from './blocks-handler';
  9. let reloadTimeout;
  10. function tabMessageHandler({ type, data }) {
  11. const listener = this.tabMessageListeners[type];
  12. if (listener) {
  13. setTimeout(() => {
  14. listener.callback(data);
  15. }, listener.delay || 0);
  16. if (listener.once) delete this.tabMessageListeners[type];
  17. }
  18. }
  19. function tabRemovedHandler(tabId) {
  20. if (tabId !== this.tabId) return;
  21. this.connectedTab?.onMessage.removeListener(this.tabMessageHandler);
  22. this.connectedTab?.disconnect();
  23. delete this.connectedTab;
  24. delete this.tabId;
  25. if (tasks[this.currentBlock.name].category === 'interaction') {
  26. this.destroy('error');
  27. }
  28. }
  29. function tabUpdatedHandler(tabId, changeInfo) {
  30. const listener = this.tabUpdatedListeners[tabId];
  31. if (listener) {
  32. listener.callback(tabId, changeInfo, () => {
  33. delete this.tabUpdatedListeners[tabId];
  34. });
  35. } else if (this.tabId === tabId) {
  36. if (!reloadTimeout) {
  37. console.log('===Register Timeout===');
  38. reloadTimeout = setTimeout(() => {
  39. this.isPaused = false;
  40. }, 15000);
  41. }
  42. this.isPaused = true;
  43. if (changeInfo.status === 'complete') {
  44. console.log('clearTimeout');
  45. clearTimeout(reloadTimeout);
  46. reloadTimeout = null;
  47. browser.tabs
  48. .executeScript(tabId, {
  49. file: './contentScript.bundle.js',
  50. })
  51. .then(() => {
  52. console.log(this.currentBlock);
  53. if (this.connectedTab) this._connectTab(this.tabId);
  54. this.isPaused = false;
  55. })
  56. .catch((error) => {
  57. console.error(error);
  58. this.isPaused = false;
  59. });
  60. }
  61. }
  62. }
  63. class WorkflowEngine {
  64. constructor(workflow) {
  65. this.id = nanoid();
  66. this.workflow = workflow;
  67. this.data = {};
  68. this.blocks = {};
  69. this.eventListeners = {};
  70. this.repeatedTasks = {};
  71. this.logs = [];
  72. this.isPaused = false;
  73. this.isDestroyed = false;
  74. this.currentBlock = null;
  75. this.workflowTimeout = null;
  76. this.tabMessageListeners = {};
  77. this.tabUpdatedListeners = {};
  78. this.tabMessageHandler = tabMessageHandler.bind(this);
  79. this.tabUpdatedHandler = tabUpdatedHandler.bind(this);
  80. this.tabRemovedHandler = tabRemovedHandler.bind(this);
  81. }
  82. init() {
  83. const drawflowData =
  84. typeof this.workflow.drawflow === 'string'
  85. ? JSON.parse(this.workflow.drawflow || '{}')
  86. : this.workflow.drawflow;
  87. const blocks = drawflowData?.drawflow.Home.data;
  88. if (!blocks) {
  89. console.error(errorMessage('no-block', this.workflow));
  90. return;
  91. }
  92. const blocksArr = Object.values(blocks);
  93. const triggerBlock = blocksArr.find(({ name }) => name === 'trigger');
  94. if (!triggerBlock) {
  95. console.error(errorMessage('no-trigger-block', this.workflow));
  96. return;
  97. }
  98. browser.tabs.onUpdated.addListener(this.tabUpdatedHandler);
  99. browser.tabs.onRemoved.addListener(this.tabRemovedHandler);
  100. this.blocks = blocks;
  101. this.startedTimestamp = Date.now();
  102. workflowState
  103. .add(this.id, {
  104. workflowId: this.workflow.id,
  105. state: this.state,
  106. })
  107. .then(() => {
  108. this._blockHandler(triggerBlock);
  109. });
  110. }
  111. on(name, listener) {
  112. (this.eventListeners[name] = this.eventListeners[name] || []).push(
  113. listener
  114. );
  115. }
  116. pause(pause = true) {
  117. this.isPaused = pause;
  118. workflowState.update(this.id, this.state);
  119. }
  120. stop(message) {
  121. this.logs.push({
  122. message,
  123. type: 'stop',
  124. name: 'Workflow is stopped',
  125. });
  126. this.destroy('stopped');
  127. }
  128. async destroy(status) {
  129. try {
  130. this.eventListeners = {};
  131. this.tabMessageListeners = {};
  132. this.tabUpdatedListeners = {};
  133. await browser.tabs.onRemoved.removeListener(this.tabRemovedHandler);
  134. await browser.tabs.onUpdated.removeListener(this.tabUpdatedHandler);
  135. await workflowState.delete(this.id);
  136. clearTimeout(this.workflowTimeout);
  137. this.isDestroyed = true;
  138. this.endedTimestamp = Date.now();
  139. if (!this.workflow.isTesting) {
  140. const { logs } = await browser.storage.local.get('logs');
  141. const { name, icon, id } = this.workflow;
  142. logs.push({
  143. name,
  144. icon,
  145. status,
  146. id: this.id,
  147. workflowId: id,
  148. data: this.data,
  149. history: this.logs,
  150. endedAt: this.endedTimestamp,
  151. startedAt: this.startedTimestamp,
  152. });
  153. await browser.storage.local.set({ logs });
  154. }
  155. this.dispatchEvent('destroyed', this.workflow.id);
  156. } catch (error) {
  157. console.error(error);
  158. }
  159. }
  160. dispatchEvent(name, params) {
  161. const listeners = this.eventListeners[name];
  162. console.log(name, this.eventListeners);
  163. if (!listeners) return;
  164. listeners.forEach((callback) => {
  165. callback(params);
  166. });
  167. }
  168. get state() {
  169. const keys = ['tabId', 'isPaused', 'isDestroyed', 'currentBlock'];
  170. return keys.reduce((acc, key) => {
  171. acc[key] = this[key];
  172. return acc;
  173. }, {});
  174. }
  175. _blockHandler(block, prevBlockData) {
  176. if (this.isDestroyed) {
  177. console.log(
  178. '%cDestroyed',
  179. 'color: red; font-size: 24px; font-weight: bold'
  180. );
  181. return;
  182. }
  183. if (this.isPaused) {
  184. console.log(this.isPaused, 'pause');
  185. setTimeout(() => {
  186. this._blockHandler(block, prevBlockData);
  187. }, 1000);
  188. return;
  189. }
  190. this.workflowTimeout = setTimeout(() => {
  191. if (!this.isDestroyed) this.stop('Workflow stopped because of timeout');
  192. }, this.workflow.settings.timeout || 120000);
  193. workflowState.update(this.id, this.state);
  194. console.log(this.logs);
  195. console.log(`${block.name}:`, block);
  196. this.currentBlock = block;
  197. const isInteraction = tasks[block.name].category === 'interaction';
  198. const handlerName = isInteraction
  199. ? 'interactionHandler'
  200. : toCamelCase(block?.name);
  201. const handler = blocksHandler[handlerName];
  202. if (handler) {
  203. handler
  204. .call(this, block, prevBlockData)
  205. .then((result) => {
  206. if (result.nextBlockId) {
  207. this.logs.push({
  208. type: 'success',
  209. name: tasks[block.name].name,
  210. data: result.data,
  211. });
  212. this._blockHandler(this.blocks[result.nextBlockId], result.data);
  213. } else {
  214. this.logs.push({
  215. type: 'finish',
  216. message: 'Workflow finished running',
  217. name: 'Finish',
  218. });
  219. this.dispatchEvent('finish');
  220. this.destroy('success');
  221. console.log('Done', this);
  222. }
  223. clearTimeout(this.workflowTimeout);
  224. this.workflowTimeout = null;
  225. })
  226. .catch((error) => {
  227. this.logs.push({
  228. type: 'error',
  229. message: error.message,
  230. name: tasks[block.name].name,
  231. });
  232. if (
  233. this.workflow.settings.onError === 'keep-running' &&
  234. error.nextBlockId
  235. ) {
  236. this._blockHandler(
  237. this.blocks[error.nextBlockId],
  238. error.data || ''
  239. );
  240. } else {
  241. this.destroy('error');
  242. }
  243. clearTimeout(this.workflowTimeout);
  244. this.workflowTimeout = null;
  245. console.error(error);
  246. });
  247. } else {
  248. console.error(`"${block.name}" block doesn't have a handler`);
  249. }
  250. }
  251. _connectTab(tabId) {
  252. const connectedTab = browser.tabs.connect(tabId, {
  253. name: `${this.workflow.id}--${this.workflow.name.slice(0, 10)}`,
  254. });
  255. if (this.connectedTab) {
  256. this.connectedTab.onMessage.removeListener(this.tabMessageHandler);
  257. this.connectedTab.disconnect();
  258. }
  259. connectedTab.onMessage.addListener(this.tabMessageHandler);
  260. this.connectedTab = connectedTab;
  261. this.tabId = tabId;
  262. return connectedTab;
  263. }
  264. _listener({ id, name, callback, once = true, ...options }) {
  265. const listenerNames = {
  266. event: 'eventListener',
  267. 'tab-updated': 'tabUpdatedListeners',
  268. 'tab-message': 'tabMessageListeners',
  269. };
  270. this[listenerNames[name]][id] = { callback, once, ...options };
  271. return () => {
  272. delete this.tabMessageListeners[id];
  273. };
  274. }
  275. }
  276. export default WorkflowEngine;