workflow-engine.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  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. }
  26. function tabUpdatedHandler(tabId, changeInfo) {
  27. const listener = this.tabUpdatedListeners[tabId];
  28. if (listener) {
  29. listener.callback(tabId, changeInfo, () => {
  30. delete this.tabUpdatedListeners[tabId];
  31. });
  32. } else if (this.tabId === tabId) {
  33. if (!reloadTimeout) {
  34. console.log('===Register Timeout===');
  35. reloadTimeout = setTimeout(() => {
  36. this.isPaused = false;
  37. }, 15000);
  38. }
  39. this.isPaused = true;
  40. if (changeInfo.status === 'complete') {
  41. console.log('clearTimeout');
  42. clearTimeout(reloadTimeout);
  43. reloadTimeout = null;
  44. browser.tabs
  45. .executeScript(tabId, {
  46. file: './contentScript.bundle.js',
  47. })
  48. .then(() => {
  49. console.log(this.currentBlock);
  50. if (this.connectedTab) this._connectTab(this.tabId);
  51. this.isPaused = false;
  52. })
  53. .catch((error) => {
  54. console.error(error);
  55. this.isPaused = false;
  56. });
  57. }
  58. }
  59. }
  60. class WorkflowEngine {
  61. constructor(workflow) {
  62. this.id = nanoid();
  63. this.workflow = workflow;
  64. this.data = {};
  65. this.blocks = {};
  66. this.eventListeners = {};
  67. this.repeatedTasks = {};
  68. this.logs = [];
  69. this.blocksArr = [];
  70. this.isPaused = false;
  71. this.isDestroyed = false;
  72. this.currentBlock = null;
  73. this.workflowTimeout = null;
  74. this.tabMessageListeners = {};
  75. this.tabUpdatedListeners = {};
  76. this.tabMessageHandler = tabMessageHandler.bind(this);
  77. this.tabUpdatedHandler = tabUpdatedHandler.bind(this);
  78. this.tabRemovedHandler = tabRemovedHandler.bind(this);
  79. }
  80. init() {
  81. const drawflowData =
  82. typeof this.workflow.drawflow === 'string'
  83. ? JSON.parse(this.workflow.drawflow || '{}')
  84. : this.workflow.drawflow;
  85. const blocks = drawflowData?.drawflow.Home.data;
  86. if (!blocks) {
  87. console.error(errorMessage('no-block', this.workflow));
  88. return;
  89. }
  90. const blocksArr = Object.values(blocks);
  91. const triggerBlock = blocksArr.find(({ name }) => name === 'trigger');
  92. if (!triggerBlock) {
  93. console.error(errorMessage('no-trigger-block', this.workflow));
  94. return;
  95. }
  96. browser.tabs.onUpdated.addListener(this.tabUpdatedHandler);
  97. browser.tabs.onRemoved.addListener(this.tabRemovedHandler);
  98. this.blocks = blocks;
  99. this.blocksArr = blocksArr;
  100. this.startedTimestamp = Date.now();
  101. workflowState.add(this.id, this.state).then(() => {
  102. this._blockHandler(triggerBlock);
  103. });
  104. }
  105. on(name, listener) {
  106. (this.eventListeners[name] = this.eventListeners[name] || []).push(
  107. listener
  108. );
  109. }
  110. pause(pause = true) {
  111. this.isPaused = pause;
  112. workflowState.update(this.tabId, this.state);
  113. }
  114. stop() {
  115. /* to-do add stop log */
  116. console.log('stoppp');
  117. this.destroy();
  118. }
  119. destroy() {
  120. // save log
  121. this.dispatchEvent('destroyed', this.workflow.id);
  122. this.eventListeners = {};
  123. this.tabMessageListeners = {};
  124. this.tabUpdatedListeners = {};
  125. browser.tabs.onRemoved.removeListener(this.tabRemovedHandler);
  126. browser.tabs.onUpdated.removeListener(this.tabUpdatedHandler);
  127. workflowState.delete(this.id);
  128. this.isDestroyed = true;
  129. this.endedTimestamp = Date.now();
  130. }
  131. dispatchEvent(name, params) {
  132. const listeners = this.eventListeners[name];
  133. console.log(name, this.eventListeners);
  134. if (!listeners) return;
  135. listeners.forEach((callback) => {
  136. callback(params);
  137. });
  138. }
  139. get state() {
  140. const keys = ['tabId', 'isPaused', 'isDestroyed', 'currentBlock'];
  141. return keys.reduce((acc, key) => {
  142. acc[key] = this[key];
  143. return acc;
  144. }, {});
  145. }
  146. _blockHandler(block, prevBlockData) {
  147. if (this.isDestroyed) {
  148. console.log(
  149. '%cDestroyed',
  150. 'color: red; font-size: 24px; font-weight: bold'
  151. );
  152. return;
  153. }
  154. if (this.isPaused) {
  155. console.log(this.isPaused, 'pause');
  156. setTimeout(() => {
  157. this._blockHandler(block, prevBlockData);
  158. }, 1000);
  159. return;
  160. }
  161. console.log(this.workflow);
  162. this.workflowTimeout = setTimeout(
  163. () => this.stop(),
  164. this.workflow.settings.timeout || 120000
  165. );
  166. workflowState.update(this.id, this.state);
  167. console.log(`${block.name}:`, block);
  168. this.currentBlock = block;
  169. const isInteraction = tasks[block.name].category === 'interaction';
  170. const handlerName = isInteraction
  171. ? 'interactionHandler'
  172. : toCamelCase(block?.name);
  173. const handler = blocksHandler[handlerName];
  174. if (handler) {
  175. handler
  176. .call(this, block, prevBlockData)
  177. .then((result) => {
  178. if (result.nextBlockId) {
  179. this._blockHandler(this.blocks[result.nextBlockId], result.data);
  180. } else {
  181. this.dispatchEvent('finish');
  182. this.stop();
  183. console.log('Done', this);
  184. }
  185. clearTimeout(this.workflowTimeout);
  186. this.workflowTimeout = null;
  187. })
  188. .catch((error) => {
  189. if (
  190. this.workflow.settings.onError === 'keep-running' &&
  191. error.nextBlockId
  192. ) {
  193. this._blockHandler(
  194. this.blocks[error.nextBlockId],
  195. error.data || ''
  196. );
  197. } else {
  198. this.stop();
  199. }
  200. clearTimeout(this.workflowTimeout);
  201. this.workflowTimeout = null;
  202. console.dir(error);
  203. console.error(error, 'new');
  204. });
  205. } else {
  206. console.error(`"${block.name}" block doesn't have a handler`);
  207. }
  208. }
  209. _connectTab(tabId) {
  210. const connectedTab = browser.tabs.connect(tabId, {
  211. name: `${this.workflow.id}--${this.workflow.name.slice(0, 10)}`,
  212. });
  213. if (this.connectedTab) {
  214. this.connectedTab.onMessage.removeListener(this.tabMessageHandler);
  215. this.connectedTab.disconnect();
  216. }
  217. connectedTab.onMessage.addListener(this.tabMessageHandler);
  218. this.connectedTab = connectedTab;
  219. this.tabId = tabId;
  220. return connectedTab;
  221. }
  222. _listener({ id, name, callback, once = true, ...options }) {
  223. const listenerNames = {
  224. event: 'eventListener',
  225. 'tab-updated': 'tabUpdatedListeners',
  226. 'tab-message': 'tabMessageListeners',
  227. };
  228. this[listenerNames[name]][id] = { callback, once, ...options };
  229. return () => {
  230. delete this.tabMessageListeners[id];
  231. };
  232. }
  233. }
  234. export default WorkflowEngine;