workflow-engine.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. /* eslint-disable no-underscore-dangle */
  2. import browser from 'webextension-polyfill';
  3. import { toCamelCase } from '@/utils/helper';
  4. import { tasks } from '@/utils/shared';
  5. import * as blocksHandler from './blocks-handler';
  6. import executingWorkflow from '@/utils/executing-workflow';
  7. function tabMessageHandler({ type, data }) {
  8. const listener = this.tabMessageListeners[type];
  9. if (listener) {
  10. setTimeout(() => {
  11. listener.callback(data);
  12. }, listener.delay || 0);
  13. if (listener.once) delete this.tabMessageListeners[type];
  14. }
  15. }
  16. function tabRemovedHandler(tabId) {
  17. if (tabId !== this.tabId) return;
  18. this.connectedTab?.onMessage.removeListener(this.tabMessageHandler);
  19. this.connectedTab?.disconnect();
  20. delete this.connectedTab;
  21. delete this.tabId;
  22. }
  23. function tabUpdatedHandler(tabId, changeInfo) {
  24. const listener = this.tabUpdatedListeners[tabId];
  25. if (listener) {
  26. listener.callback(tabId, changeInfo, () => {
  27. delete this.tabUpdatedListeners[tabId];
  28. });
  29. } else {
  30. if (this.tabId !== tabId) return;
  31. this.isInsidePaused = true;
  32. if (changeInfo.status === 'complete') {
  33. browser.tabs
  34. .executeScript(tabId, {
  35. file: './contentScript.bundle.js',
  36. })
  37. .then(() => {
  38. console.log(this.currentBlock);
  39. if (this._connectedTab) this._connectTab(this.tabId);
  40. this.isInsidePaused = false;
  41. })
  42. .catch((error) => {
  43. console.error(error);
  44. this.isInsidePaused = false;
  45. });
  46. }
  47. }
  48. }
  49. class WorkflowEngine {
  50. constructor(id, workflow) {
  51. this.id = id;
  52. this.workflow = workflow;
  53. this.data = {};
  54. this.blocks = {};
  55. this.eventListeners = {};
  56. this.repeatedTasks = {};
  57. this.logs = [];
  58. this.blocksArr = [];
  59. this.isPaused = false;
  60. this.isDestroyed = false;
  61. this.isInsidePaused = false;
  62. this.currentBlock = null;
  63. this.tabMessageListeners = {};
  64. this.tabUpdatedListeners = {};
  65. this.tabMessageHandler = tabMessageHandler.bind(this);
  66. this.tabUpdatedHandler = tabUpdatedHandler.bind(this);
  67. this.tabRemovedHandler = tabRemovedHandler.bind(this);
  68. }
  69. init() {
  70. const drawflowData =
  71. typeof this.workflow.drawflow === 'string'
  72. ? JSON.parse(this.workflow.drawflow || '{}')
  73. : this.workflow.drawflow;
  74. const blocks = drawflowData?.drawflow.Home.data;
  75. if (!blocks) {
  76. console.error('No block is found');
  77. return;
  78. }
  79. const blocksArr = Object.values(blocks);
  80. const triggerBlock = blocksArr.find(({ name }) => name === 'trigger');
  81. if (!triggerBlock) {
  82. console.error('A trigger block is required');
  83. return;
  84. }
  85. browser.tabs.onUpdated.addListener(this.tabUpdatedHandler);
  86. browser.tabs.onRemoved.addListener(this.tabRemovedHandler);
  87. this.blocks = blocks;
  88. this.blocksArr = blocksArr;
  89. this.startedTimestamp = Date.now();
  90. this._blockHandler(triggerBlock);
  91. }
  92. on(name, listener) {
  93. (this.eventListeners[name] = this.eventListeners[name] || []).push(
  94. listener
  95. );
  96. }
  97. destroy() {
  98. // save log
  99. this._dispatchEvent('destroyed', this.workflow.id);
  100. this.eventListeners = {};
  101. this.tabMessageListeners = {};
  102. this.tabUpdatedListeners = {};
  103. browser.tabs.onRemoved.removeListener(this.tabRemovedHandler);
  104. browser.tabs.onUpdated.removeListener(this.tabUpdatedHandler);
  105. this.isDestroyed = true;
  106. this.endedTimestamp = Date.now();
  107. }
  108. _dispatchEvent(name, params) {
  109. const listeners = this.eventListeners[name];
  110. console.log(name, this.eventListeners);
  111. if (!listeners) return;
  112. listeners.forEach((callback) => {
  113. callback(params);
  114. });
  115. }
  116. _blockHandler(block, prevBlockData) {
  117. if (this.isDestroyed) {
  118. console.log(
  119. '%cDestroyed',
  120. 'color: red; font-size: 24px; font-weight: bold'
  121. );
  122. return;
  123. }
  124. const executedWorkflowData = [
  125. 'data',
  126. 'isPaused',
  127. 'isDestroyed',
  128. 'currentBlock',
  129. ].reduce((acc, key) => {
  130. acc[key] = this[key];
  131. return acc;
  132. }, {});
  133. console.log(executedWorkflowData);
  134. executingWorkflow.update(this.id, { workflow: executedWorkflowData });
  135. if (this.isPaused || this.isInsidePaused) {
  136. setTimeout(() => {
  137. this._blockHandler(block, prevBlockData);
  138. }, 1000);
  139. return;
  140. }
  141. console.log(`${block.name}:`, block);
  142. this.currentBlock = block;
  143. const isInteraction = tasks[block.name].category === 'interaction';
  144. const handlerName = isInteraction
  145. ? 'interactionHandler'
  146. : toCamelCase(block?.name);
  147. const handler = blocksHandler[handlerName];
  148. if (handler) {
  149. handler
  150. .call(this, block, prevBlockData)
  151. .then((result) => {
  152. if (result.nextBlockId) {
  153. this._blockHandler(this.blocks[result.nextBlockId], result.data);
  154. } else {
  155. this._dispatchEvent('finish');
  156. this.destroy();
  157. console.log('Done', this);
  158. }
  159. })
  160. .catch((error) => {
  161. console.error(error, 'new');
  162. });
  163. } else {
  164. console.error(`"${block.name}" block doesn't have a handler`);
  165. }
  166. }
  167. _connectTab(tabId) {
  168. const connectedTab = browser.tabs.connect(tabId, {
  169. name: `${this.workflow.id}--${this.workflow.name.slice(0, 10)}`,
  170. });
  171. if (this.connectedTab) {
  172. this.connectedTab.onMessage.removeListener(this.tabMessageHandler);
  173. this.connectedTab.disconnect();
  174. }
  175. connectedTab.onMessage.addListener(this.tabMessageHandler);
  176. this.connectedTab = connectedTab;
  177. this.tabId = tabId;
  178. return connectedTab;
  179. }
  180. _listener({ id, name, callback, once = true, ...options }) {
  181. const listenerNames = {
  182. event: 'eventListener',
  183. 'tab-updated': 'tabUpdatedListeners',
  184. 'tab-message': 'tabMessageListeners',
  185. };
  186. this[listenerNames[name]][id] = { callback, once, ...options };
  187. return () => {
  188. delete this.tabMessageListeners[id];
  189. };
  190. }
  191. get _connectedTab() {
  192. if (!this.connectedTab) {
  193. this.destroy();
  194. return null;
  195. }
  196. return this.connectedTab;
  197. }
  198. }
  199. export default WorkflowEngine;