InputWorker.mjs 32 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081
  1. /**
  2. * Web worker to handle the inputs.
  3. * Handles storage, modification and retrieval of the inputs.
  4. *
  5. * @author j433866 [j433866@gmail.com]
  6. * @copyright Crown Copyright 2019
  7. * @license Apache-2.0
  8. */
  9. import Utils from "../../core/Utils.mjs";
  10. import {detectFileType} from "../../core/lib/FileType.mjs";
  11. // Default max values
  12. // These will be correctly calculated automatically
  13. self.maxWorkers = 4;
  14. self.maxTabs = 1;
  15. self.pendingFiles = [];
  16. self.inputs = {};
  17. self.loaderWorkers = [];
  18. self.currentInputNum = 1;
  19. self.numInputs = 0;
  20. self.pendingInputs = 0;
  21. self.loadingInputs = 0;
  22. /**
  23. * Respond to message from parent thread.
  24. *
  25. * @param {MessageEvent} e
  26. */
  27. self.addEventListener("message", function(e) {
  28. const r = e.data;
  29. if (!("action" in r)) {
  30. log.error("No action");
  31. return;
  32. }
  33. log.debug(`Receiving ${r.action} from InputWaiter.`);
  34. switch (r.action) {
  35. case "loadUIFiles":
  36. self.loadFiles(r.data);
  37. break;
  38. case "loaderWorkerReady":
  39. self.loaderWorkerReady(r.data);
  40. break;
  41. case "updateMaxWorkers":
  42. self.maxWorkers = r.data;
  43. break;
  44. case "updateMaxTabs":
  45. self.updateMaxTabs(r.data.maxTabs, r.data.activeTab);
  46. break;
  47. case "updateInputValue":
  48. self.updateInputValue(r.data);
  49. break;
  50. case "updateInputObj":
  51. self.updateInputObj(r.data);
  52. break;
  53. case "updateInputProgress":
  54. self.updateInputProgress(r.data);
  55. break;
  56. case "bakeAll":
  57. self.bakeAllInputs();
  58. break;
  59. case "bakeNext":
  60. self.bakeInput(r.data.inputNum, r.data.bakeId);
  61. break;
  62. case "getLoadProgress":
  63. self.getLoadProgress(r.data);
  64. break;
  65. case "setInput":
  66. self.setInput(r.data);
  67. break;
  68. case "setLogLevel":
  69. log.setLevel(r.data, false);
  70. break;
  71. case "addInput":
  72. self.addInput(r.data, "string");
  73. break;
  74. case "refreshTabs":
  75. self.refreshTabs(r.data.inputNum, r.data.direction);
  76. break;
  77. case "removeInput":
  78. self.removeInput(r.data);
  79. break;
  80. case "changeTabRight":
  81. self.changeTabRight(r.data.activeTab);
  82. break;
  83. case "changeTabLeft":
  84. self.changeTabLeft(r.data.activeTab);
  85. break;
  86. case "autobake":
  87. self.autoBake(r.data.activeTab, 0, false);
  88. break;
  89. case "filterTabs":
  90. self.filterTabs(r.data);
  91. break;
  92. case "loaderWorkerMessage":
  93. self.handleLoaderMessage(r.data);
  94. break;
  95. case "inputSwitch":
  96. self.inputSwitch(r.data);
  97. break;
  98. case "updateTabHeader":
  99. self.updateTabHeader(r.data);
  100. break;
  101. case "step":
  102. self.autoBake(r.data.activeTab, r.data.progress, true);
  103. break;
  104. case "getInput":
  105. self.getInput(r.data);
  106. break;
  107. case "getInputNums":
  108. self.getInputNums(r.data);
  109. break;
  110. default:
  111. log.error(`Unknown action '${r.action}'.`);
  112. }
  113. });
  114. /**
  115. * Gets the load progress of the input files, and the
  116. * load progress for the input given in inputNum
  117. *
  118. * @param {number} inputNum - The input to get the file loading progress for
  119. */
  120. self.getLoadProgress = function(inputNum) {
  121. const total = self.numInputs;
  122. const pending = self.pendingFiles.length;
  123. const loading = self.loadingInputs;
  124. const loaded = total - pending - loading;
  125. self.postMessage({
  126. action: "loadingInfo",
  127. data: {
  128. pending: pending,
  129. loading: loading,
  130. loaded: loaded,
  131. total: total,
  132. activeProgress: {
  133. inputNum: inputNum,
  134. progress: self.getInputProgress(inputNum)
  135. }
  136. }
  137. });
  138. };
  139. /**
  140. * Fired when an autobake is initiated.
  141. * Queues the active input and sends a bake command.
  142. *
  143. * @param {number} inputNum - The input to be baked
  144. * @param {number} progress - The current progress of the bake through the recipe
  145. * @param {boolean} [step=false] - Set to true if we should only execute one operation instead of the
  146. * whole recipe
  147. */
  148. self.autoBake = function(inputNum, progress, step=false) {
  149. const input = self.getInputObj(inputNum);
  150. if (input) {
  151. self.postMessage({
  152. action: "bakeAllInputs",
  153. data: {
  154. nums: [parseInt(inputNum, 10)],
  155. step: step,
  156. progress: progress
  157. }
  158. });
  159. }
  160. };
  161. /**
  162. * Fired when we want to bake all inputs (bake button clicked)
  163. * Sends a list of inputNums to the workerwaiter
  164. */
  165. self.bakeAllInputs = function() {
  166. const inputNums = Object.keys(self.inputs),
  167. nums = [];
  168. for (let i = 0; i < inputNums.length; i++) {
  169. if (self.inputs[inputNums[i]].status === "loaded") {
  170. nums.push(parseInt(inputNums[i], 10));
  171. }
  172. }
  173. self.postMessage({
  174. action: "bakeAllInputs",
  175. data: {
  176. nums: nums,
  177. step: false,
  178. progress: 0
  179. }
  180. });
  181. };
  182. /**
  183. * Gets the data for the provided inputNum and sends it to the WorkerWaiter
  184. *
  185. * @param {number} inputNum
  186. * @param {number} bakeId
  187. */
  188. self.bakeInput = function(inputNum, bakeId) {
  189. const inputObj = self.getInputObj(inputNum);
  190. if (inputObj === null ||
  191. inputObj === undefined ||
  192. inputObj.status !== "loaded") {
  193. self.postMessage({
  194. action: "queueInputError",
  195. data: {
  196. inputNum: inputNum,
  197. bakeId: bakeId
  198. }
  199. });
  200. return;
  201. }
  202. let inputData = inputObj.data;
  203. if (typeof inputData !== "string") inputData = inputData.fileBuffer;
  204. self.postMessage({
  205. action: "queueInput",
  206. data: {
  207. input: inputData,
  208. inputNum: inputNum,
  209. bakeId: bakeId
  210. }
  211. });
  212. };
  213. /**
  214. * Gets the stored object for a specific inputNum
  215. *
  216. * @param {number} inputNum - The input we want to get the object for
  217. * @returns {object}
  218. */
  219. self.getInputObj = function(inputNum) {
  220. return self.inputs[inputNum];
  221. };
  222. /**
  223. * Gets the stored value for a specific inputNum.
  224. *
  225. * @param {number} inputNum - The input we want to get the value of
  226. * @returns {string | ArrayBuffer}
  227. */
  228. self.getInputValue = function(inputNum) {
  229. if (self.inputs[inputNum]) {
  230. if (typeof self.inputs[inputNum].data === "string") {
  231. return self.inputs[inputNum].data;
  232. } else {
  233. return self.inputs[inputNum].data.fileBuffer;
  234. }
  235. }
  236. return "";
  237. };
  238. /**
  239. * Gets the stored value or object for a specific inputNum and sends it to the inputWaiter.
  240. *
  241. * @param {object} inputData - Object containing data about the input to retrieve
  242. * @param {number} inputData.inputNum - The inputNum of the input to get
  243. * @param {boolean} inputData.getObj - If true, returns the entire input object instead of just the value
  244. * @param {number} inputData.id - The callback ID for the callback to run when returned to the inputWaiter
  245. */
  246. self.getInput = function(inputData) {
  247. const inputNum = inputData.inputNum,
  248. data = (inputData.getObj) ? self.getInputObj(inputNum) : self.getInputValue(inputNum);
  249. self.postMessage({
  250. action: "getInput",
  251. data: {
  252. data: data,
  253. id: inputData.id
  254. }
  255. });
  256. };
  257. /**
  258. * Gets a list of the stored inputNums, along with the minimum and maximum
  259. *
  260. * @param {number} id - The callback ID to be executed when returned to the inputWaiter
  261. */
  262. self.getInputNums = function(id) {
  263. const inputNums = Object.keys(self.inputs),
  264. min = self.getSmallestInputNum(inputNums),
  265. max = self.getLargestInputNum(inputNums);
  266. self.postMessage({
  267. action: "getInputNums",
  268. data: {
  269. inputNums: inputNums,
  270. min: min,
  271. max: max,
  272. id: id
  273. }
  274. });
  275. };
  276. /**
  277. * Gets the load progress for a specific inputNum
  278. *
  279. * @param {number} inputNum - The input we want to get the progress of
  280. * @returns {number | string} - Returns "error" if there was a load error
  281. */
  282. self.getInputProgress = function(inputNum) {
  283. const inputObj = self.getInputObj(inputNum);
  284. if (inputObj === undefined || inputObj === null) return;
  285. if (inputObj.status === "error") {
  286. return "error";
  287. }
  288. return inputObj.progress;
  289. };
  290. /**
  291. * Gets the largest inputNum of all the inputs
  292. *
  293. * @param {string[]} inputNums - The numbers to find the largest of
  294. * @returns {number}
  295. */
  296. self.getLargestInputNum = function(inputNums) {
  297. return inputNums.reduce((acc, val) => {
  298. val = parseInt(val, 10);
  299. return val > acc ? val : acc;
  300. }, -1);
  301. };
  302. /**
  303. * Gets the smallest inputNum of all the inputs
  304. *
  305. * @param {string[]} inputNums - The numbers to find the smallest of
  306. * @returns {number}
  307. */
  308. self.getSmallestInputNum = function(inputNums) {
  309. const min = inputNums.reduce((acc, val) => {
  310. val = parseInt(val, 10);
  311. return val < acc ? val : acc;
  312. }, Number.MAX_SAFE_INTEGER);
  313. // Assume we don't have this many tabs!
  314. if (min === Number.MAX_SAFE_INTEGER) return -1;
  315. return min;
  316. };
  317. /**
  318. * Gets the next smallest inputNum
  319. *
  320. * @param {number} inputNum - The current input number
  321. * @returns {number}
  322. */
  323. self.getPreviousInputNum = function(inputNum) {
  324. const inputNums = Object.keys(self.inputs);
  325. if (inputNums.length === 0) return -1;
  326. return inputNums.reduce((acc, val) => {
  327. val = parseInt(val, 10);
  328. return (val < inputNum && val > acc) ? val : acc;
  329. }, self.getSmallestInputNum(inputNums));
  330. };
  331. /**
  332. * Gets the next largest inputNum
  333. *
  334. * @param {number} inputNum - The current input number
  335. * @returns {number}
  336. */
  337. self.getNextInputNum = function(inputNum) {
  338. const inputNums = Object.keys(self.inputs);
  339. return inputNums.reduce((acc, val) => {
  340. val = parseInt(val, 10);
  341. return (val > inputNum && val < acc) ? val : acc;
  342. }, self.getLargestInputNum(inputNums));
  343. };
  344. /**
  345. * Gets a list of inputNums starting from the provided inputNum.
  346. * If direction is "left", gets the inputNums higher than the provided number.
  347. * If direction is "right", gets the inputNums lower than the provided number.
  348. * @param {number} inputNum - The inputNum we want to get the neighbours of
  349. * @param {string} direction - Either "left" or "right". Determines which direction we search for nearby numbers in
  350. * @returns {number[]}
  351. */
  352. self.getNearbyNums = function(inputNum, direction) {
  353. const nums = [];
  354. for (let i = 0; i < self.maxTabs; i++) {
  355. let newNum;
  356. if (i === 0 && self.inputs[inputNum] !== undefined) {
  357. newNum = inputNum;
  358. } else {
  359. switch (direction) {
  360. case "left":
  361. newNum = self.getNextInputNum(nums[i - 1]);
  362. if (newNum === nums[i - 1]) {
  363. direction = "right";
  364. newNum = self.getPreviousInputNum(nums[0]);
  365. }
  366. break;
  367. case "right":
  368. newNum = self.getPreviousInputNum(nums[i - 1]);
  369. if (newNum === nums[i - 1]) {
  370. direction = "left";
  371. newNum = self.getNextInputNum(nums[0]);
  372. }
  373. }
  374. }
  375. if (!nums.includes(newNum) && (newNum > 0)) {
  376. nums.push(newNum);
  377. }
  378. }
  379. nums.sort(function(a, b) {
  380. return a - b;
  381. });
  382. return nums;
  383. };
  384. /**
  385. * Gets the data to display in the tab header for an input, and
  386. * posts it back to the inputWaiter
  387. *
  388. * @param {number} inputNum - The inputNum of the tab header
  389. */
  390. self.updateTabHeader = function(inputNum) {
  391. const input = self.getInputObj(inputNum);
  392. if (input === null || input === undefined) return;
  393. let inputData = input.data;
  394. if (typeof inputData !== "string") {
  395. inputData = input.data.name;
  396. }
  397. inputData = inputData.replace(/[\n\r]/g, "");
  398. self.postMessage({
  399. action: "updateTabHeader",
  400. data: {
  401. inputNum: inputNum,
  402. input: inputData.slice(0, 100)
  403. }
  404. });
  405. };
  406. /**
  407. * Gets the input for a specific inputNum, and posts it to the inputWaiter
  408. * so that it can be displayed in the input area
  409. *
  410. * @param {object} inputData
  411. * @param {number} inputData.inputNum - The input to get the data for
  412. * @param {boolean} inputData.silent - If false, the manager statechange event will be fired
  413. */
  414. self.setInput = function(inputData) {
  415. const inputNum = inputData.inputNum;
  416. const silent = inputData.silent;
  417. const input = self.getInputObj(inputNum);
  418. if (input === undefined || input === null) return;
  419. let inputVal = input.data;
  420. const inputObj = {
  421. inputNum: inputNum,
  422. input: inputVal
  423. };
  424. if (typeof inputVal !== "string") {
  425. inputObj.name = inputVal.name;
  426. inputObj.size = inputVal.size;
  427. inputObj.type = inputVal.type;
  428. inputObj.progress = input.progress;
  429. inputObj.status = input.status;
  430. inputVal = inputVal.fileBuffer;
  431. const fileSlice = inputVal.slice(0, 512001);
  432. inputObj.input = fileSlice;
  433. self.postMessage({
  434. action: "setInput",
  435. data: {
  436. inputObj: inputObj,
  437. silent: silent
  438. }
  439. }, [fileSlice]);
  440. } else {
  441. self.postMessage({
  442. action: "setInput",
  443. data: {
  444. inputObj: inputObj,
  445. silent: silent
  446. }
  447. });
  448. }
  449. self.updateTabHeader(inputNum);
  450. };
  451. /**
  452. * Gets the nearby inputNums to the provided number, and posts them
  453. * to the inputWaiter to be displayed on the page.
  454. *
  455. * @param {number} inputNum - The inputNum to find the nearby numbers for
  456. * @param {string} direction - The direction to search for inputNums in. Either "left" or "right"
  457. */
  458. self.refreshTabs = function(inputNum, direction) {
  459. const nums = self.getNearbyNums(inputNum, direction),
  460. inputNums = Object.keys(self.inputs),
  461. tabsLeft = (self.getSmallestInputNum(inputNums) !== nums[0] && nums.length > 0),
  462. tabsRight = (self.getLargestInputNum(inputNums) !== nums[nums.length - 1] && nums.length > 0);
  463. self.postMessage({
  464. action: "refreshTabs",
  465. data: {
  466. nums: nums,
  467. activeTab: (nums.includes(inputNum)) ? inputNum : self.getNextInputNum(inputNum),
  468. tabsLeft: tabsLeft,
  469. tabsRight: tabsRight
  470. }
  471. });
  472. // Update the tab headers for the new tabs
  473. for (let i = 0; i < nums.length; i++) {
  474. self.updateTabHeader(nums[i]);
  475. }
  476. };
  477. /**
  478. * Update the stored status for an input
  479. *
  480. * @param {number} inputNum - The input that's having its status changed
  481. * @param {string} status - The status of the input
  482. */
  483. self.updateInputStatus = function(inputNum, status) {
  484. if (self.inputs[inputNum] !== undefined) {
  485. self.inputs[inputNum].status = status;
  486. }
  487. };
  488. /**
  489. * Update the stored load progress of an input
  490. *
  491. * @param {object} inputData
  492. * @param {number} inputData.inputNum - The input that's having its progress updated
  493. * @param {number} inputData.progress - The load progress of the input
  494. */
  495. self.updateInputProgress = function(inputData) {
  496. const inputNum = inputData.inputNum;
  497. const progress = inputData.progress;
  498. if (self.inputs[inputNum] !== undefined) {
  499. self.inputs[inputNum].progress = progress;
  500. }
  501. };
  502. /**
  503. * Update the stored value of an input.
  504. *
  505. * @param {object} inputData
  506. * @param {number} inputData.inputNum - The input that's having its value updated
  507. * @param {string | ArrayBuffer} inputData.value - The new value of the input
  508. * @param {boolean} inputData.force - If true, still updates the input value if the input type is different to the stored value
  509. */
  510. self.updateInputValue = function(inputData) {
  511. const inputNum = inputData.inputNum;
  512. if (inputNum < 1) return;
  513. if (Object.prototype.hasOwnProperty.call(self.inputs[inputNum].data, "fileBuffer") &&
  514. typeof inputData.value === "string" && !inputData.force) return;
  515. const value = inputData.value;
  516. if (self.inputs[inputNum] !== undefined) {
  517. if (typeof value === "string") {
  518. self.inputs[inputNum].data = value;
  519. } else {
  520. self.inputs[inputNum].data.fileBuffer = value;
  521. }
  522. self.inputs[inputNum].status = "loaded";
  523. self.inputs[inputNum].progress = 100;
  524. return;
  525. }
  526. // If we get to here, an input for inputNum could not be found,
  527. // so create a new one. Only do this if the value is a string, as
  528. // loadFiles will create the input object for files
  529. if (typeof value === "string") {
  530. self.inputs.push({
  531. inputNum: inputNum,
  532. data: value,
  533. status: "loaded",
  534. progress: 100
  535. });
  536. }
  537. };
  538. /**
  539. * Update the stored data object for an input.
  540. * Used if we need to change a string to an ArrayBuffer
  541. *
  542. * @param {object} inputData
  543. * @param {number} inputData.inputNum - The number of the input we're updating
  544. * @param {object} inputData.data - The new data object for the input
  545. */
  546. self.updateInputObj = function(inputData) {
  547. const inputNum = inputData.inputNum;
  548. const data = inputData.data;
  549. if (self.getInputObj(inputNum) === undefined) return;
  550. self.inputs[inputNum].data = data;
  551. };
  552. /**
  553. * Get the index of a loader worker object.
  554. * Returns -1 if the worker could not be found
  555. *
  556. * @param {number} workerId - The ID of the worker we're searching for
  557. * @returns {number}
  558. */
  559. self.getLoaderWorkerIdx = function(workerId) {
  560. for (let i = 0; i < self.loaderWorkers.length; i++) {
  561. if (self.loaderWorkers[i].id === workerId) {
  562. return i;
  563. }
  564. }
  565. return -1;
  566. };
  567. /**
  568. * Fires when a loaderWorker is ready to load files.
  569. * Stores data about the new loaderWorker in the loaderWorkers array,
  570. * and sends the next file to the loaderWorker to be loaded.
  571. *
  572. * @param {object} workerData
  573. * @param {number} workerData.id - The ID of the new loaderWorker
  574. */
  575. self.loaderWorkerReady = function(workerData) {
  576. const newWorkerObj = {
  577. id: workerData.id,
  578. inputNum: -1,
  579. active: true
  580. };
  581. self.loaderWorkers.push(newWorkerObj);
  582. self.loadNextFile(self.loaderWorkers.indexOf(newWorkerObj));
  583. };
  584. /**
  585. * Handler for messages sent by loaderWorkers.
  586. * (Messages are sent between the inputWorker and
  587. * loaderWorkers via the main thread)
  588. *
  589. * @param {object} r - The data sent by the loaderWorker
  590. * @param {number} r.inputNum - The inputNum which the message corresponds to
  591. * @param {string} r.error - Present if an error is fired by the loaderWorker. Contains the error message string.
  592. * @param {ArrayBuffer} r.fileBuffer - Present if a file has finished loading. Contains the loaded file buffer.
  593. */
  594. self.handleLoaderMessage = function(r) {
  595. let inputNum = 0;
  596. if ("inputNum" in r) {
  597. inputNum = r.inputNum;
  598. }
  599. if ("error" in r) {
  600. self.updateInputProgress(r.inputNum, 0);
  601. self.updateInputStatus(r.inputNum, "error");
  602. log.error(r.error);
  603. self.loadingInputs--;
  604. self.terminateLoaderWorker(r.id);
  605. self.activateLoaderWorker();
  606. self.setInput({inputNum: inputNum, silent: true});
  607. return;
  608. }
  609. if ("fileBuffer" in r) {
  610. log.debug(`Input file ${inputNum} loaded.`);
  611. self.loadingInputs--;
  612. self.updateInputValue({
  613. inputNum: inputNum,
  614. value: r.fileBuffer
  615. });
  616. self.postMessage({
  617. action: "fileLoaded",
  618. data: {
  619. inputNum: inputNum
  620. }
  621. });
  622. const idx = self.getLoaderWorkerIdx(r.id);
  623. self.loadNextFile(idx);
  624. } else if ("progress" in r) {
  625. self.updateInputProgress(r);
  626. }
  627. };
  628. /**
  629. * Loads the next file using a loaderWorker
  630. *
  631. * @param {number} - The loaderWorker which will load the file
  632. */
  633. self.loadNextFile = function(workerIdx) {
  634. if (workerIdx === -1) return;
  635. const id = self.loaderWorkers[workerIdx].id;
  636. if (self.pendingFiles.length === 0) {
  637. const workerObj = self.loaderWorkers.splice(workerIdx, 1)[0];
  638. self.terminateLoaderWorker(workerObj.id);
  639. return;
  640. }
  641. const nextFile = self.pendingFiles.splice(0, 1)[0];
  642. self.loaderWorkers[workerIdx].inputNum = nextFile.inputNum;
  643. self.loadingInputs++;
  644. self.postMessage({
  645. action: "loadInput",
  646. data: {
  647. file: nextFile.file,
  648. inputNum: nextFile.inputNum,
  649. workerId: id
  650. }
  651. });
  652. };
  653. /**
  654. * Sends a message to the inputWaiter to create a new loaderWorker.
  655. * If there's an inactive loaderWorker that already exists, use that instead.
  656. */
  657. self.activateLoaderWorker = function() {
  658. for (let i = 0; i < self.loaderWorkers.length; i++) {
  659. if (!self.loaderWorkers[i].active) {
  660. self.loaderWorkers[i].active = true;
  661. self.loadNextFile(i);
  662. return;
  663. }
  664. }
  665. self.postMessage({
  666. action: "activateLoaderWorker"
  667. });
  668. };
  669. /**
  670. * Sends a message to the inputWaiter to terminate a loaderWorker.
  671. *
  672. * @param {number} id - The ID of the worker to be terminated
  673. */
  674. self.terminateLoaderWorker = function(id) {
  675. self.postMessage({
  676. action: "terminateLoaderWorker",
  677. data: id
  678. });
  679. // If we still have pending files, spawn a worker
  680. if (self.pendingFiles.length > 0) {
  681. self.activateLoaderWorker();
  682. }
  683. };
  684. /**
  685. * Loads files using LoaderWorkers
  686. *
  687. * @param {object} filesData
  688. * @param {FileList} filesData.files - The list of files to be loaded
  689. * @param {number} filesData.activeTab - The active tab in the UI
  690. */
  691. self.loadFiles = function(filesData) {
  692. const files = filesData.files;
  693. const activeTab = filesData.activeTab;
  694. let lastInputNum = -1;
  695. const inputNums = [];
  696. for (let i = 0; i < files.length; i++) {
  697. if (i === 0 && self.getInputValue(activeTab) === "") {
  698. self.removeInput({
  699. inputNum: activeTab,
  700. refreshTabs: false,
  701. removeChefWorker: false
  702. });
  703. lastInputNum = self.addInput(false, "file", {
  704. name: files[i].name,
  705. size: files[i].size.toLocaleString(),
  706. type: files[i].type || "unknown"
  707. }, activeTab);
  708. } else {
  709. lastInputNum = self.addInput(false, "file", {
  710. name: files[i].name,
  711. size: files[i].size.toLocaleString(),
  712. type: files[i].type || "unknown"
  713. });
  714. }
  715. inputNums.push(lastInputNum);
  716. self.pendingFiles.push({
  717. file: files[i],
  718. inputNum: lastInputNum
  719. });
  720. }
  721. let max = self.maxWorkers;
  722. if (self.pendingFiles.length < self.maxWorkers) max = self.pendingFiles.length;
  723. // Create loaderWorkers to load the new files
  724. for (let i = 0; i < max; i++) {
  725. self.activateLoaderWorker();
  726. }
  727. self.getLoadProgress();
  728. self.setInput({inputNum: activeTab, silent: true});
  729. };
  730. /**
  731. * Adds an input to the input dictionary
  732. *
  733. * @param {boolean} [changetab=false] - Whether or not to change to the new input
  734. * @param {string} type - Either "string" or "file"
  735. * @param {Object} fileData - Contains information about the file to be added to the input (only used when type is "file")
  736. * @param {string} fileData.name - The filename of the input being added
  737. * @param {number} fileData.size - The file size (in bytes) of the input being added
  738. * @param {string} fileData.type - The MIME type of the input being added
  739. * @param {number} inputNum - Defaults to auto-incrementing self.currentInputNum
  740. */
  741. self.addInput = function(
  742. changeTab = false,
  743. type,
  744. fileData = {
  745. name: "unknown",
  746. size: "unknown",
  747. type: "unknown"
  748. },
  749. inputNum = self.currentInputNum++
  750. ) {
  751. self.numInputs++;
  752. const newInputObj = {
  753. inputNum: inputNum
  754. };
  755. switch (type) {
  756. case "string":
  757. newInputObj.data = "";
  758. newInputObj.status = "loaded";
  759. newInputObj.progress = 100;
  760. break;
  761. case "file":
  762. newInputObj.data = {
  763. fileBuffer: new ArrayBuffer(),
  764. name: fileData.name,
  765. size: fileData.size,
  766. type: fileData.type
  767. };
  768. newInputObj.status = "pending";
  769. newInputObj.progress = 0;
  770. break;
  771. default:
  772. log.error(`Invalid type '${type}'.`);
  773. return -1;
  774. }
  775. self.inputs[inputNum] = newInputObj;
  776. // Tell the inputWaiter we've added an input, so it can create a tab to display it
  777. self.postMessage({
  778. action: "inputAdded",
  779. data: {
  780. changeTab: changeTab,
  781. inputNum: inputNum
  782. }
  783. });
  784. return inputNum;
  785. };
  786. /**
  787. * Remove an input from the inputs dictionary
  788. *
  789. * @param {object} removeInputData
  790. * @param {number} removeInputData.inputNum - The number of the input to be removed
  791. * @param {boolean} removeInputData.refreshTabs - If true, refresh the tabs after removing the input
  792. * @param {boolean} removeInputData.removeChefWorker - If true, remove a chefWorker from the WorkerWaiter
  793. */
  794. self.removeInput = function(removeInputData) {
  795. const inputNum = removeInputData.inputNum;
  796. const refreshTabs = removeInputData.refreshTabs;
  797. self.numInputs--;
  798. for (let i = 0; i < self.loaderWorkers.length; i++) {
  799. if (self.loaderWorkers[i].inputNum === inputNum) {
  800. // Terminate any loaderWorker that's loading the removed input
  801. self.loadingInputs--;
  802. self.terminateLoaderWorker(self.loaderWorkers[i].id);
  803. break;
  804. }
  805. }
  806. for (let i = 0; i < self.pendingFiles.length; i++) {
  807. // Remove the input from the pending files list
  808. if (self.pendingFiles[i].inputNum === inputNum) {
  809. self.pendingFiles.splice(i, 1);
  810. break;
  811. }
  812. }
  813. delete self.inputs[inputNum];
  814. if (refreshTabs) {
  815. self.refreshTabs(self.getPreviousInputNum(inputNum), "left");
  816. }
  817. if (self.numInputs < self.maxWorkers && removeInputData.removeChefWorker) {
  818. self.postMessage({
  819. action: "removeChefWorker"
  820. });
  821. }
  822. };
  823. /**
  824. * Change to the next tab.
  825. *
  826. * @param {number} inputNum - The inputNum of the tab to change to
  827. */
  828. self.changeTabRight = function(inputNum) {
  829. const newInput = self.getNextInputNum(inputNum);
  830. self.postMessage({
  831. action: "changeTab",
  832. data: newInput
  833. });
  834. };
  835. /**
  836. * Change to the previous tab.
  837. *
  838. * @param {number} inputNum - The inputNum of the tab to change to
  839. */
  840. self.changeTabLeft = function(inputNum) {
  841. const newInput = self.getPreviousInputNum(inputNum);
  842. self.postMessage({
  843. action: "changeTab",
  844. data: newInput
  845. });
  846. };
  847. /**
  848. * Updates the maximum number of tabs, and refreshes them if it changes
  849. *
  850. * @param {number} maxTabs - The new max number of tabs
  851. * @param {number} activeTab - The currently selected tab
  852. */
  853. self.updateMaxTabs = function(maxTabs, activeTab) {
  854. if (self.maxTabs !== maxTabs) {
  855. self.maxTabs = maxTabs;
  856. self.refreshTabs(activeTab, "right");
  857. }
  858. };
  859. /**
  860. * Search the inputs for any that match the filters provided,
  861. * posting the results back to the inputWaiter
  862. *
  863. * @param {object} searchData - Object containing the search filters
  864. * @param {boolean} searchData.showPending - If true, include pending inputs in the results
  865. * @param {boolean} searchData.showLoading - If true, include loading inputs in the results
  866. * @param {boolean} searchData.showLoaded - If true, include loaded inputs in the results
  867. * @param {string} searchData.filter - A regular expression to match the inputs on
  868. * @param {string} searchData.filterType - Either "CONTENT" or "FILENAME". Detemines what should be matched with filter
  869. * @param {number} searchData.numResults - The maximum number of results to be returned
  870. */
  871. self.filterTabs = function(searchData) {
  872. const showPending = searchData.showPending,
  873. showLoading = searchData.showLoading,
  874. showLoaded = searchData.showLoaded,
  875. filterType = searchData.filterType;
  876. let filterExp;
  877. try {
  878. filterExp = new RegExp(searchData.filter, "i");
  879. } catch (error) {
  880. self.postMessage({
  881. action: "filterTabError",
  882. data: error.message
  883. });
  884. return;
  885. }
  886. const numResults = searchData.numResults;
  887. const inputs = [];
  888. const inputNums = Object.keys(self.inputs);
  889. for (let i = 0; i < inputNums.length; i++) {
  890. const iNum = inputNums[i];
  891. let textDisplay = "";
  892. let addInput = false;
  893. if (self.inputs[iNum].status === "pending" && showPending ||
  894. self.inputs[iNum].status === "loading" && showLoading ||
  895. self.inputs[iNum].status === "loaded" && showLoaded) {
  896. try {
  897. if (typeof self.inputs[iNum].data === "string") {
  898. if (filterType.toLowerCase() === "content" &&
  899. filterExp.test(self.inputs[iNum].data.slice(0, 4096))) {
  900. textDisplay = self.inputs[iNum].data.slice(0, 4096);
  901. addInput = true;
  902. }
  903. } else {
  904. if ((filterType.toLowerCase() === "filename" &&
  905. filterExp.test(self.inputs[iNum].data.name)) ||
  906. filterType.toLowerCase() === "content" &&
  907. filterExp.test(Utils.arrayBufferToStr(self.inputs[iNum].data.fileBuffer.slice(0, 4096)))) {
  908. textDisplay = self.inputs[iNum].data.name;
  909. addInput = true;
  910. }
  911. }
  912. } catch (error) {
  913. self.postMessage({
  914. action: "filterTabError",
  915. data: error.message
  916. });
  917. return;
  918. }
  919. }
  920. if (addInput) {
  921. if (textDisplay === "" || textDisplay === undefined) {
  922. textDisplay = "New Tab";
  923. }
  924. const inputItem = {
  925. inputNum: iNum,
  926. textDisplay: textDisplay
  927. };
  928. inputs.push(inputItem);
  929. }
  930. if (inputs.length >= numResults) {
  931. break;
  932. }
  933. }
  934. // Send the results back to the inputWaiter
  935. self.postMessage({
  936. action: "displayTabSearchResults",
  937. data: inputs
  938. });
  939. };
  940. /**
  941. * Swaps the input and outputs, and sends the old input back to the main thread.
  942. *
  943. * @param {object} switchData
  944. * @param {number} switchData.inputNum - The inputNum of the input to be switched to
  945. * @param {string | ArrayBuffer} switchData.outputData - The data to switch to
  946. */
  947. self.inputSwitch = function(switchData) {
  948. const currentInput = self.getInputObj(switchData.inputNum);
  949. const currentData = currentInput.data;
  950. if (currentInput === undefined || currentInput === null) return;
  951. if (typeof switchData.outputData !== "string") {
  952. const output = new Uint8Array(switchData.outputData),
  953. types = detectFileType(output);
  954. let type = "unknown",
  955. ext = "dat";
  956. if (types.length) {
  957. type = types[0].mime;
  958. ext = types[0].extension.split(",", 1)[0];
  959. }
  960. // ArrayBuffer
  961. self.updateInputObj({
  962. inputNum: switchData.inputNum,
  963. data: {
  964. fileBuffer: switchData.outputData,
  965. name: `output.${ext}`,
  966. size: switchData.outputData.byteLength.toLocaleString(),
  967. type: type
  968. }
  969. });
  970. } else {
  971. // String
  972. self.updateInputValue({
  973. inputNum: switchData.inputNum,
  974. value: switchData.outputData,
  975. force: true
  976. });
  977. }
  978. self.postMessage({
  979. action: "inputSwitch",
  980. data: {
  981. data: currentData,
  982. inputNum: switchData.inputNum
  983. }
  984. });
  985. self.postMessage({
  986. action: "fileLoaded",
  987. data: {
  988. inputNum: switchData.inputNum
  989. }
  990. });
  991. };