├── .editorconfig ├── .github └── workflows │ └── publish_npm.yml ├── .gitignore ├── .husky ├── pre-commit └── prepare-commit-msg ├── .npmignore ├── .prettierrc ├── CONTRIBUTING.MD ├── LICENSE ├── README.md ├── __tests__ ├── Crypt.spec.js ├── Helper.spec.js ├── Logger.spec.js ├── Node.spec.js ├── Queue.spec.js ├── index.spec.js └── utils.js ├── cli.js ├── env.sample ├── eslint.config.js ├── examples ├── basic │ ├── index.js │ ├── readme.md │ └── task.js ├── demo.gif └── web_scraper │ ├── .eslintrc.js │ ├── README.md │ ├── index.js │ ├── package.json │ ├── task.js │ └── yarn.lock ├── index.js ├── lint-staged.config.js ├── package.json ├── src ├── Crypt.js ├── Helper.js ├── Logger.js ├── Node.js ├── Queue.js ├── index.js └── task.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_size = 4 7 | end_of_line = lf 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | -------------------------------------------------------------------------------- /.github/workflows/publish_npm.yml: -------------------------------------------------------------------------------- 1 | name: Publish to NPM 2 | on: 3 | release: 4 | types: [created] 5 | 6 | jobs: 7 | 8 | test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-node@v3 13 | with: 14 | node-version: 16.x 15 | cache: 'yarn' 16 | - run: yarn --frozen-lockfile 17 | - run: yarn run test 18 | 19 | publish: 20 | needs: test 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v2 25 | - name: Setup Node 26 | uses: actions/setup-node@v2 27 | with: 28 | node-version: '16.x' 29 | registry-url: 'https://registry.npmjs.org' 30 | scope: '@t13' 31 | - run: yarn --frozen-lockfile 32 | - name: Publish package on NPM 📦 33 | run: npm publish 34 | env: 35 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | payload.json 3 | .env -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | yarn lint-staged 2 | CI=true yarn run test 3 | -------------------------------------------------------------------------------- /.husky/prepare-commit-msg: -------------------------------------------------------------------------------- 1 | exec < /dev/tty && node_modules/.bin/cz --hook || true 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | 4 | payload.json 5 | 6 | examples 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "proseWrap": "never", 3 | "singleQuote": true, 4 | "trailingComma": "es5" 5 | } 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.MD: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 🎉 Thank you for considering contributing to queue-xec! 🎉 3 | 4 | Contributors are always welcome! Any single line of code maters! Don't hesitate to correct even a typo! 5 | 6 | Before anything else, check issues. 7 | If issue exist in any condition (closed, assigned, stalled), comment and share your thoughts there. It's nice to have all subject related discussions, organized in one place. 8 | 9 | If you can't find any issue related to your current issue you want to contribute, go ahead and open it! After verification, issues will be assigned to you. 10 | 11 | Then you can start working on that issue. 12 | 13 | ### How to setup environment 14 | 15 | It is recommended to have both worker and master in their latest versions. So : 16 | Fork [master](https://github.com/queue-xec/maste) and [worker](https://github.com/queue-xec/worker) 17 | 18 | ```bash 19 | mkdir queue-xec 20 | cd queue-xec 21 | git clone https://github.com//master 22 | git clone https://github.com//worker 23 | cd worker ; yarn 24 | cd ../master ; yarn 25 | git checkout -b my_new_feature 26 | ... make your changes 27 | git commit 28 | ``` 29 | 30 | ### After environment setup 31 | Start working on your test in develpment mode by : 32 | ```js 33 | yarn run test:dev 34 | ``` 35 | Create unit test in parallel of actual feature. 36 | No pull request will be accepted if not contains new unit tests if introduce new features , or fixes of bugs! 37 | 38 | 39 | ### On git commit : 40 | After all test passed , commitizen will fire up and start asking questions about your commit.(Be honest) 41 | 42 | If you have any issue during this , check issues , if cant found something related , open a new issue , any other contributor can help you. 43 | 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | Copyright (c) 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Master 2 | 3 | [![Publish to NPM](https://github.com/queue-xec/master/actions/workflows/publish_npm.yml/badge.svg)](https://github.com/queue-xec/master/actions/workflows/publish_npm.yml) 4 | 5 | 6 | Master connects to a peer-to-peer 'room' data channel and shares with their peers (Workers) data about the jobs. Among them is job data( each job/task may have its own data to solve/work on) , job code to executed by workers and code dependencies which workers have to install before stat processing tasks. In other words Master defines the problem , defines the way to solve it, provides all the tools and code needed and adds data (if needed() to solve the problem.. 7 | 8 | Master and Workers can find each other in any network condition , as long they are connected online. This is possible from nature of peer to peer networks and [bugout](https://github.com/chr15m/bugout) ! Bugout offers message transfer encryption, but we encrypt all data i/o transfers on top of that , here as well. 9 | 10 | 11 | 12 | ## Installation 13 | 14 | ```bash 15 | git clone https://github.com/queue-xec/master 16 | cd master 17 | yarn # or npm install 18 | ``` 19 | 20 | ```bash 21 | node cli.js --setup 22 | ``` 23 | 24 | Will prompt user to enter following info: 25 | 26 | - `transferEncryptToken` Enter a 32 length alphanumeric string Or prompt to generate one on the fly 27 | - `token` Enter at least 20 length alphanumeric string Or prompt to generate one on the fly 28 | 29 | These info will be saved and loaded in .env file in the root folder of Master.It should be the same on all workers , to be able to communicate. 30 | 31 | ## How to set job code to run in workers 32 | 33 | Edit [taskFile](https://github.com/queue-xec/master/blob/devel/src/task.js) inner `run()` method 34 | 35 | ⚠️ Beware, this file should never be modified in a remote worker instance because it will be overwritten by Master the next time the worker instance is initiated 36 | 37 | ```javascript 38 | // Require here libs you passed from master as dependencies 39 | // if needed here. 40 | const { Big } = require('big.js'); 41 | const Helper = require('./Helper.js'); 42 | class Task { 43 | // task.js file will placed by default in WORKER_DIR /workplace/task.js 44 | constructor() { 45 | this.data = null; 46 | // this.helper = new Helper(); 47 | } 48 | 49 | /** 50 | * Description: This method is the job procces. Receives job data (job) 51 | * Must return job results (Object) , to delivered in master. 52 | * @param {Object} job data from Master about job {id: Number , data: String (needs JSON.parse)} 53 | * @returns {Object} result 54 | */ 55 | async run(job) { 56 | console.dir(job); 57 | const data = JSON.parse(job.data); 58 | // code to solve the problem , in remote Workers 59 | return {}; 60 | } 61 | } 62 | 63 | module.exports = Task; 64 | ``` 65 | 66 | ## Example: 67 | 68 | ```js 69 | const Master = require('queue-xec-master'); 70 | function resultCollect(result) { 71 | // handle here incoming results from workers.. 72 | console.dir(result); 73 | } 74 | async function run() { 75 | const mm = new Master({ 76 | onResults: resultCollect, 77 | execAssets: { 78 | dependencies: ['big.js', 'moment'], // pass Worker dependencies 79 | files: [ 80 | { 81 | masterPath: '/src/task.js', 82 | name: 'task.js', 83 | workerPath: '/workplace/task.js', 84 | }, // if task.js file not passed to workers , Master will use default one located in /src/task.js , here you can ovverride it by changing above details 85 | { 86 | masterPath: '/src/Logger.js', 87 | name: 'Logger.js', 88 | workerPath: '/workplace/Logger.js', 89 | }, 90 | { 91 | masterPath: '/src/Helper.js', 92 | name: 'Helper.js', 93 | workerPath: '/workplace/Helper.js', 94 | }, 95 | ], 96 | /* masterPath and workerPath are not absolute , NEVER access files out of their process.cwd() path. 97 | Are relative to their process current location , respectively */ 98 | }, 99 | }); 100 | const dummy = { 101 | x: 1, 102 | y: 2, 103 | }; 104 | 105 | let cnt = 0; 106 | for (let i = 0; i < 5; i++) { 107 | const payload = { 108 | id: cnt, 109 | data: JSON.stringify(dummy), 110 | }; 111 | await mm.pushNewJob(payload).catch((e) => console.log(e)); 112 | cnt += 1; 113 | } 114 | } 115 | ``` 116 | 117 | ### ⚠️ Under development ⚠️ 118 | 119 | [![MIT License](https://img.shields.io/apm/l/atomic-design-ui.svg?)](https://github.com/queue-xec/master/blob/devel/LICENSE) 120 | 121 | ## > Contributors < 122 | 123 | 124 | 125 | 126 | 127 | ### DISCLAIMER NOTICE: 128 | 129 | Queue-Xec Master is a library that allows users to connect to a peer-to-peer 'room' data channel and share data about jobs with other users (hereinafter referred to as "Workers"). This data may include job data, job code to be executed by Workers, and code dependencies that Workers must install before processing tasks. The Library also allows users to define the problem to be solved, provide the necessary tools and code, and add data (if needed) to solve the problem. 130 | 131 | Please be aware that the Library is provided "as is" and we make no representations or warranties of any kind, express or implied, about the completeness, accuracy, reliability, suitability or availability with respect to the Library or the information, products, services, or related graphics contained on the Library for any purpose. Any reliance you place on such information is therefore strictly at your own risk. 132 | 133 | In no event will we be liable for any loss or damage including without limitation, indirect or consequential loss or damage, or any loss or damage whatsoever arising from loss of data or profits arising out of, or in connection with, the use of the Library. 134 | -------------------------------------------------------------------------------- /__tests__/Crypt.spec.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | const Crypt = require('../src/Crypt'); 3 | 4 | describe('Crypt class', () => { 5 | const key = 'ulo6Ohcoos0mieboh0mukah7nohch5ae'; 6 | const text = 'Hello, World!'; 7 | 8 | describe('constructor', () => { 9 | it('should throw an error if key is not provided', () => { 10 | expect(() => new Crypt()).toThrow( 11 | 'transferEncryptToken not specified..' 12 | ); 13 | }); 14 | 15 | it('should initialize key and iv when provided', () => { 16 | const crypt = new Crypt(key); 17 | expect(crypt.key).toBe(key); 18 | expect(crypt.iv).toBeInstanceOf(Buffer); 19 | }); 20 | }); 21 | 22 | describe('encrypt method', () => { 23 | it('should encrypt text successfully', () => { 24 | const crypt = new Crypt(key); 25 | const encrypted = crypt.encrypt(text); 26 | expect(encrypted).toHaveProperty('iv'); 27 | expect(encrypted).toHaveProperty('encryptedData'); 28 | }); 29 | 30 | it('should handle number input', () => { 31 | const crypt = new Crypt(key); 32 | const encrypted = crypt.encrypt(123); 33 | expect(encrypted).toHaveProperty('iv'); 34 | expect(encrypted).toHaveProperty('encryptedData'); 35 | }); 36 | 37 | it('should throw an error if key is invalid', () => { 38 | const crypt = new Crypt('short_key'); 39 | const result = crypt.encrypt(text); 40 | expect(result).toBeNull(); 41 | }); 42 | 43 | it('should handle edge cases', () => { 44 | const crypt = new Crypt(key); 45 | const encrypted = crypt.encrypt(undefined); 46 | const encrypted2 = crypt.encrypt(null); 47 | 48 | expect(() => crypt.encrypt(null)).not.toThrow(); 49 | expect(() => crypt.encrypt(undefined)).not.toThrow(); 50 | expect(encrypted).toBe(null); 51 | expect(encrypted2).toBe(null); 52 | }); 53 | }); 54 | 55 | describe('decrypt method', () => { 56 | it('should decrypt text successfully', () => { 57 | const crypt = new Crypt(key); 58 | const encrypted = crypt.encrypt(text); 59 | const decrypted = crypt.decrypt(encrypted); 60 | expect(decrypted).toBe(text); 61 | }); 62 | 63 | it('should handle invalid input', () => { 64 | const crypt = new Crypt(key); 65 | const invalidEncrypted = { 66 | iv: 'invalid_iv', 67 | encryptedData: 'invalid_data', 68 | }; 69 | const result = crypt.decrypt(invalidEncrypted); 70 | expect(result).toBeNull(); 71 | }); 72 | 73 | it('should handle edge cases', () => { 74 | const crypt = new Crypt(key); 75 | const decrypted = crypt.decrypt(undefined); 76 | const decrypted2 = crypt.decrypt(null); 77 | 78 | expect(() => crypt.decrypt(null)).not.toThrow(); 79 | expect(() => crypt.decrypt(undefined)).not.toThrow(); 80 | expect(decrypted).toBe(null); 81 | expect(decrypted2).toBe(null); 82 | }); 83 | }); 84 | 85 | describe('getKey and getIv methods', () => { 86 | it('should return the key', () => { 87 | const crypt = new Crypt(key); 88 | expect(crypt.getKey()).toBe(key); 89 | }); 90 | 91 | it('should return the iv', () => { 92 | const crypt = new Crypt(key); 93 | expect(crypt.getIv()).toBeInstanceOf(Buffer); 94 | }); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /__tests__/Helper.spec.js: -------------------------------------------------------------------------------- 1 | // helper.test.js 2 | const { Helper } = require('../src/Helper'); 3 | const moment = require('moment'); 4 | const fs = require('fs'); 5 | const crypto = require('crypto'); 6 | 7 | describe('Helper', () => { 8 | describe('LocalTime', () => { 9 | it('returns expected local time for valid timestamp', () => { 10 | const timestamp = 1737155257000; // Representing 2022-02-01 12:30:00 11 | const expectedTime = '18-01 01:07:37'; 12 | expect(Helper.LocalTime(timestamp)).toBe(expectedTime); 13 | }); 14 | 15 | it('returns expected local time for current timestamp', () => { 16 | const currentTimestamp = Math.floor(Date.now() / 1000); 17 | const currentTime = 18 | moment(currentTimestamp).format('DD-MM HH:mm:ss'); 19 | expect(Helper.LocalTime(currentTimestamp)).toBe(currentTime); 20 | }); 21 | }); 22 | 23 | describe('getSha256', () => { 24 | it('returns expected SHA-256 hash for existing file', async () => { 25 | const file = 'test.txt'; 26 | fs.writeFileSync(file, 'Hello, World!'); 27 | const hash = await Helper.getSha256(file); 28 | const expectedHash = crypto 29 | .createHash('sha256') 30 | .update('Hello, World!') 31 | .digest('hex'); 32 | expect(hash).toBe(expectedHash); 33 | fs.unlinkSync(file); // Clean up the test file 34 | }); 35 | 36 | it('rejects promise for non-existent file', async () => { 37 | const file = 'non_existent_file.txt'; 38 | await expect(Helper.getSha256(file)).rejects.toThrowError( 39 | 'File not found' 40 | ); 41 | }); 42 | 43 | it('rejects promise for invalid file path', async () => { 44 | const file = './invalid/path'; 45 | await expect(Helper.getSha256(file)).rejects.toThrowError(); 46 | }); 47 | }); 48 | 49 | describe('sleep', () => { 50 | it('resolves promise after specified time', async () => { 51 | const startTime = Date.now(); 52 | await Helper.sleep(1000); // Sleep for 1 second 53 | const endTime = Date.now(); 54 | expect(endTime - startTime).toBeGreaterThan(900); // Tolerance of 100ms 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /__tests__/Logger.spec.js: -------------------------------------------------------------------------------- 1 | // Logger.test.js 2 | const { Logger } = require('../src/Logger'); 3 | const fs = require('fs'); 4 | const { PassThrough } = require('stream'); 5 | 6 | jest.mock('fs'); 7 | 8 | describe('Logger class', () => { 9 | let logger; 10 | 11 | beforeEach(() => { 12 | logger = new Logger({ 13 | level: 'debug', 14 | writeStream: null, 15 | }); 16 | }); 17 | 18 | afterEach(() => { 19 | jest.restoreAllMocks(); 20 | }); 21 | 22 | describe('translateToCode method', () => { 23 | it('should return 0 for "off" log level', () => { 24 | const result = logger.translateToCode('off'); 25 | expect(result).toBe(0); 26 | }); 27 | 28 | it('should return 1 for "info" log level', () => { 29 | const result = logger.translateToCode('info'); 30 | expect(result).toBe(1); 31 | }); 32 | 33 | it('should return 2 for "warn" log level', () => { 34 | const result = logger.translateToCode('warn'); 35 | expect(result).toBe(2); 36 | }); 37 | 38 | it('should return 3 for "error" log level', () => { 39 | const result = logger.translateToCode('error'); 40 | expect(result).toBe(3); 41 | }); 42 | 43 | it('should return 4 for "debug" log level', () => { 44 | const result = logger.translateToCode('debug'); 45 | expect(result).toBe(4); 46 | }); 47 | 48 | it('should return 0 for invalid log level', () => { 49 | const result = logger.translateToCode('invalid'); 50 | expect(result).toBe(0); 51 | }); 52 | }); 53 | 54 | describe('info method', () => { 55 | it('should not log if log level is "off"', () => { 56 | logger.level = 0; 57 | const consoleLogSpy = jest.spyOn(console, 'log'); 58 | logger.info('Test message'); 59 | expect(consoleLogSpy).not.toHaveBeenCalled(); 60 | }); 61 | 62 | it('should log if log level is "info"', () => { 63 | logger.level = 1; 64 | const consoleLogSpy = jest.spyOn(console, 'log'); 65 | logger.info('Test message'); 66 | expect(consoleLogSpy).toHaveBeenCalledTimes(1); 67 | }); 68 | 69 | it('should log with extra information', () => { 70 | logger.level = 1; 71 | const consoleLogSpy = jest.spyOn(console, 'log'); 72 | logger.info('Test message', { extra: 'information' }); 73 | expect(consoleLogSpy).toHaveBeenCalledTimes(1); 74 | }); 75 | }); 76 | 77 | describe('warn method', () => { 78 | it('should not log if log level is "off"', () => { 79 | logger.level = 0; 80 | const consoleLogSpy = jest.spyOn(console, 'log'); 81 | logger.warn('Test message'); 82 | expect(consoleLogSpy).not.toHaveBeenCalled(); 83 | }); 84 | 85 | it('should not log if log level is "info"', () => { 86 | logger.level = 1; 87 | const consoleLogSpy = jest.spyOn(console, 'log'); 88 | logger.warn('Test message'); 89 | expect(consoleLogSpy).not.toHaveBeenCalled(); 90 | }); 91 | 92 | it('should log if log level is "warn"', () => { 93 | logger.level = 2; 94 | const consoleLogSpy = jest.spyOn(console, 'log'); 95 | logger.warn('Test message'); 96 | expect(consoleLogSpy).toHaveBeenCalledTimes(1); 97 | }); 98 | 99 | it('should log with extra information', () => { 100 | logger.level = 2; 101 | const consoleLogSpy = jest.spyOn(console, 'log'); 102 | logger.warn('Test message', { extra: 'information' }); 103 | expect(consoleLogSpy).toHaveBeenCalledTimes(1); 104 | }); 105 | }); 106 | 107 | describe('error method', () => { 108 | it('should not log if log level is "off"', () => { 109 | logger.level = 0; 110 | const consoleLogSpy = jest.spyOn(console, 'log'); 111 | logger.error('Test message'); 112 | expect(consoleLogSpy).not.toHaveBeenCalled(); 113 | }); 114 | 115 | it('should not log if log level is "info"', () => { 116 | logger.level = 1; 117 | const consoleLogSpy = jest.spyOn(console, 'log'); 118 | logger.error('Test message'); 119 | expect(consoleLogSpy).not.toHaveBeenCalled(); 120 | }); 121 | 122 | it('should not log if log level is "warn"', () => { 123 | logger.level = 2; 124 | const consoleLogSpy = jest.spyOn(console, 'log'); 125 | logger.error('Test message'); 126 | expect(consoleLogSpy).not.toHaveBeenCalled(); 127 | }); 128 | 129 | it('should log if log level is "error"', () => { 130 | logger.level = 3; 131 | const consoleLogSpy = jest.spyOn(console, 'log'); 132 | logger.error('Test message'); 133 | logger.error('Test message', { mock: 'all' }); 134 | logger.error({ mock: 'all' }); 135 | 136 | expect(consoleLogSpy).toHaveBeenCalledTimes(3); 137 | }); 138 | }); 139 | 140 | describe('fatal method', () => { 141 | it('should exit the process with code -1 if no message is provided', () => { 142 | const processExitSpy = jest.spyOn(process, 'exit'); 143 | const mockExit = jest 144 | .spyOn(process, 'exit') 145 | .mockImplementation((number) => { 146 | throw new Error('process.exit: ' + number); 147 | }); 148 | expect(() => { 149 | logger.fatal(); 150 | }).toThrow(); 151 | expect(() => { 152 | logger.fatal('error'); 153 | }).toThrow(); 154 | expect(() => { 155 | logger.fatal('error', 'extra'); 156 | }).toThrow(); 157 | 158 | expect(() => { 159 | logger.fatal('error', { mock: 'me' }); 160 | }).toThrow(); 161 | 162 | expect(() => { 163 | logger.fatal({ mock: 'me' }); 164 | }).toThrow(); 165 | 166 | expect(() => { 167 | logger.fatal({ mock: 'me' }, '22'); 168 | }).toThrow(); 169 | 170 | expect(mockExit).toHaveBeenCalledWith(-1); 171 | expect(processExitSpy).toHaveBeenCalledTimes(6); 172 | mockExit.mockRestore(); 173 | }); 174 | }); 175 | 176 | describe('debug method', () => { 177 | it('should not log if log level is "off"', () => { 178 | logger.level = 0; 179 | const consoleDebugSpy = jest.spyOn(console, 'debug'); 180 | logger.debug('Test message'); 181 | expect(consoleDebugSpy).not.toHaveBeenCalled(); 182 | }); 183 | 184 | it('should not log if log level is "info"', () => { 185 | logger.level = 1; 186 | const consoleDebugSpy = jest.spyOn(console, 'debug'); 187 | logger.debug('Test message'); 188 | expect(consoleDebugSpy).not.toHaveBeenCalled(); 189 | }); 190 | 191 | it('should not log if log level is "warn"', () => { 192 | logger.level = 2; 193 | const consoleDebugSpy = jest.spyOn(console, 'debug'); 194 | logger.debug('Test message'); 195 | expect(consoleDebugSpy).not.toHaveBeenCalled(); 196 | }); 197 | 198 | it('should not log if log level is "error"', () => { 199 | logger.level = 3; 200 | const consoleDebugSpy = jest.spyOn(console, 'debug'); 201 | logger.debug('Test message'); 202 | expect(consoleDebugSpy).not.toHaveBeenCalled(); 203 | }); 204 | 205 | it('should log if log level is "debug"', () => { 206 | logger.level = 4; 207 | const consoleDebugSpy = jest.spyOn(console, 'debug'); 208 | logger.debug('Test message'); 209 | expect(consoleDebugSpy).toHaveBeenCalledTimes(1); 210 | }); 211 | 212 | it('should log if log level is "debug" with extra', () => { 213 | logger.level = 4; 214 | const consoleDebugSpy = jest.spyOn(console, 'log'); 215 | logger.debug('Test message', 'extraaa'); 216 | logger.debug('Test message', { test: 'me' }); 217 | logger.debug({ test: 'me' }, { test: 'me' }); 218 | logger.debug({ test: 'me' }, 'me'); 219 | logger.debug({ test: 'me' }); 220 | 221 | expect(consoleDebugSpy).toHaveBeenCalledTimes(5); 222 | }); 223 | }); 224 | 225 | describe('writeToFile method', () => { 226 | it('should stringify ', () => { 227 | const mockWriteable = new PassThrough(); 228 | fs.createWriteStream.mockReturnValueOnce(mockWriteable); 229 | 230 | const log = new Logger({ 231 | level: 'debug', 232 | writeStream: fs.createWriteStream('here.txt'), 233 | }); 234 | const consoleDebugSpy = jest.spyOn(log, 'writeToFile'); 235 | log.writeToFile('hello'); 236 | log.writeToFile({ pro: 'name' }); 237 | 238 | log.writeToFile({ pro: 'name' }, { another: 1 }); 239 | log.writeToFile({ pro: 'name' }, 1); 240 | 241 | expect(consoleDebugSpy).toHaveBeenCalledTimes(4); 242 | }); 243 | }); 244 | }); 245 | -------------------------------------------------------------------------------- /__tests__/Node.spec.js: -------------------------------------------------------------------------------- 1 | // node.test.js 2 | const Node = require('../src/Node'); 3 | 4 | describe('Node class', () => { 5 | it('should create a new node with the given value', () => { 6 | const node = new Node('test value'); 7 | expect(node.value).toBe('test value'); 8 | }); 9 | 10 | it('should initialize next and prev properties to null', () => { 11 | const node = new Node('test value'); 12 | expect(node.next).toBeNull(); 13 | expect(node.prev).toBeNull(); 14 | }); 15 | 16 | it('should allow setting the next property', () => { 17 | const node1 = new Node('test value 1'); 18 | const node2 = new Node('test value 2'); 19 | node1.next = node2; 20 | expect(node1.next).toBe(node2); 21 | }); 22 | 23 | it('should allow setting the prev property', () => { 24 | const node1 = new Node('test value 1'); 25 | const node2 = new Node('test value 2'); 26 | node2.prev = node1; 27 | expect(node2.prev).toBe(node1); 28 | }); 29 | 30 | describe('edge cases', () => { 31 | it('should create a new node with a null value', () => { 32 | const node = new Node(null); 33 | expect(node.value).toBeNull(); 34 | }); 35 | 36 | it('should create a new node with an undefined value', () => { 37 | const node = new Node(undefined); 38 | expect(node.value).toBeUndefined(); 39 | }); 40 | 41 | it('should create a new node with an empty string value', () => { 42 | const node = new Node(''); 43 | expect(node.value).toBe(''); 44 | }); 45 | 46 | it('should create a new node with a number value', () => { 47 | const node = new Node(123); 48 | expect(node.value).toBe(123); 49 | }); 50 | 51 | it('should create a new node with an object value', () => { 52 | const obj = { foo: 'bar' }; 53 | const node = new Node(obj); 54 | expect(node.value).toBe(obj); 55 | }); 56 | 57 | it('should create a new node with an array value', () => { 58 | const arr = [1, 2, 3]; 59 | const node = new Node(arr); 60 | expect(node.value).toBe(arr); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /__tests__/Queue.spec.js: -------------------------------------------------------------------------------- 1 | const Node = require('../src/Node'); 2 | const Queue = require('../src/Queue'); 3 | 4 | describe('Queue', () => { 5 | let queue; 6 | 7 | beforeEach(() => { 8 | queue = new Queue(); 9 | }); 10 | 11 | describe('Initialization', () => { 12 | it('should initialize with empty queue', () => { 13 | expect(queue.first).toBeNull(); 14 | expect(queue.last).toBeNull(); 15 | expect(queue.size).toBe(0); 16 | }); 17 | }); 18 | 19 | describe('isEmpty()', () => { 20 | it('should return true when queue is empty', () => { 21 | expect(queue.isEmpty()).toBe(true); 22 | }); 23 | 24 | it('should return false when queue is not empty', () => { 25 | queue.enqueue(1); 26 | expect(queue.isEmpty()).toBe(false); 27 | }); 28 | }); 29 | 30 | describe('enqueue()', () => { 31 | it('should add item to the end of the queue', () => { 32 | queue.enqueue(1); 33 | expect(queue.first.value).toBe(1); 34 | expect(queue.last.value).toBe(1); 35 | }); 36 | 37 | it('should maintain correct order of elements', () => { 38 | queue.enqueue(1); 39 | queue.enqueue(2); 40 | queue.enqueue(3); 41 | expect(queue.first.value).toBe(1); 42 | expect(queue.last.value).toBe(3); 43 | }); 44 | 45 | it('should update size correctly', () => { 46 | queue.enqueue(1); 47 | expect(queue.size).toBe(1); 48 | queue.enqueue(2); 49 | expect(queue.size).toBe(2); 50 | queue.enqueue(3); 51 | expect(queue.size).toBe(3); 52 | }); 53 | }); 54 | 55 | describe('dequeue()', () => { 56 | it('should remove and return the first element from the queue', () => { 57 | queue.enqueue(1); 58 | queue.enqueue(2); 59 | expect(queue.dequeue().value).toBe(1); 60 | expect(queue.first.value).toBe(2); 61 | }); 62 | 63 | it('should update size correctly', () => { 64 | queue.enqueue(1); 65 | queue.enqueue(2); 66 | queue.dequeue(); 67 | expect(queue.size).toBe(1); 68 | }); 69 | 70 | it('should return null when queue is empty', () => { 71 | expect(queue.dequeue()).toBeNull(); 72 | }); 73 | }); 74 | 75 | describe('search()', () => { 76 | it('should find an element in the queue', () => { 77 | queue.enqueue(1); 78 | queue.enqueue(2); 79 | queue.enqueue(3); 80 | const foundNode = queue.search(2); 81 | expect(foundNode.value).toBe(2); 82 | }); 83 | 84 | it('should not find an element not in the queue', () => { 85 | queue.enqueue(1); 86 | queue.enqueue(2); 87 | queue.enqueue(3); 88 | const foundNode = queue.search(4); 89 | expect(foundNode).toBeNull(); 90 | }); 91 | 92 | it('should find an object in the queue', () => { 93 | queue.enqueue({ a: 1, b: 2 }); 94 | queue.enqueue({ c: 3, d: 4 }); 95 | const foundNode = queue.search({ a: 1, b: 2 }); 96 | expect(foundNode.value).toEqual({ a: 1, b: 2 }); 97 | }); 98 | 99 | it('should not find an object not in the queue', () => { 100 | queue.enqueue({ a: 1, b: 2 }); 101 | queue.enqueue({ c: 3, d: 4 }); 102 | const foundNode = queue.search({ e: 5, f: 6 }); 103 | expect(foundNode).toBeNull(); 104 | }); 105 | }); 106 | 107 | describe('bubbleSort()', () => { 108 | it('should sort the queue in ascending order', () => { 109 | queue.enqueue(3); 110 | queue.enqueue(1); 111 | queue.enqueue(2); 112 | queue.bubbleSort(); 113 | expect(queue.first.value).toBe(1); 114 | expect(queue.last.value).toBe(3); 115 | }); 116 | 117 | it('should sort the queue in ascending order with objects', () => { 118 | queue.enqueue({ a: 3 }); 119 | queue.enqueue({ a: 1 }); 120 | queue.enqueue({ a: 2 }); 121 | queue.bubbleSort(); 122 | expect(queue.first.value).toEqual({ a: 1 }); 123 | expect(queue.last.value).toEqual({ a: 3 }); 124 | }); 125 | 126 | it('should sort the queue in ascending order with mixed types', () => { 127 | queue.enqueue(3); 128 | queue.enqueue('a'); 129 | queue.enqueue(1); 130 | queue.enqueue('b'); 131 | queue.enqueue(2); 132 | queue.bubbleSort(); 133 | expect(queue.first.value).toBe(3); 134 | expect(queue.last.value).toBe(2); 135 | }); 136 | }); 137 | 138 | describe('peek()', () => { 139 | it('should return the first element from the queue', () => { 140 | queue.enqueue(1); 141 | expect(queue.peek().value).toBe(1); 142 | }); 143 | 144 | it('should return null when queue is empty', () => { 145 | expect(queue.peek()).toBeNull(); 146 | }); 147 | }); 148 | 149 | describe('tail()', () => { 150 | it('should return the last element from the queue', () => { 151 | queue.enqueue(1); 152 | queue.enqueue(2); 153 | expect(queue.tail().value).toBe(2); 154 | }); 155 | 156 | it('should return null when queue is empty', () => { 157 | expect(queue.tail()).toBeNull(); 158 | }); 159 | }); 160 | }); 161 | 162 | describe('Queue tests', () => { 163 | let queue; 164 | 165 | beforeEach(() => { 166 | queue = new Queue(); 167 | }); 168 | 169 | it('should create an empty queue', () => { 170 | expect(queue.isEmpty()).toBe(true); 171 | expect(queue.first).toBe(null); 172 | expect(queue.last).toBe(null); 173 | expect(queue.size).toBe(0); 174 | }); 175 | 176 | it('should not be empty after enqueuing an item', () => { 177 | queue.enqueue('item'); 178 | expect(queue.isEmpty()).toBe(false); 179 | expect(queue.first).not.toBe(null); 180 | expect(queue.last).not.toBe(null); 181 | expect(queue.size).toBe(1); 182 | }); 183 | 184 | it('should enqueue multiple items', () => { 185 | queue.enqueue('item1'); 186 | queue.enqueue('item2'); 187 | queue.enqueue('item3'); 188 | expect(queue.size).toBe(3); 189 | expect(queue.first.value).toBe('item1'); 190 | expect(queue.last.value).toBe('item3'); 191 | }); 192 | 193 | it('should dequeue an item', () => { 194 | queue.enqueue('item'); 195 | const dequeuedItem = queue.dequeue(); 196 | expect(dequeuedItem.value).toBe('item'); 197 | expect(queue.isEmpty()).toBe(true); 198 | expect(queue.first).toBe(null); 199 | expect(queue.last).toBe(null); 200 | expect(queue.size).toBe(0); 201 | }); 202 | 203 | it('should dequeue multiple items', () => { 204 | queue.enqueue('item1'); 205 | queue.enqueue('item2'); 206 | queue.enqueue('item3'); 207 | const dequeuedItem1 = queue.dequeue(); 208 | const dequeuedItem2 = queue.dequeue(); 209 | const dequeuedItem3 = queue.dequeue(); 210 | expect(dequeuedItem1.value).toBe('item1'); 211 | expect(dequeuedItem2.value).toBe('item2'); 212 | expect(dequeuedItem3.value).toBe('item3'); 213 | expect(queue.isEmpty()).toBe(true); 214 | expect(queue.first).toBe(null); 215 | expect(queue.last).toBe(null); 216 | expect(queue.size).toBe(0); 217 | }); 218 | 219 | it('should return null when trying to dequeue from an empty queue', () => { 220 | const dequeuedItem = queue.dequeue(); 221 | expect(dequeuedItem).toBe(null); 222 | }); 223 | 224 | it('should search for an item in the queue', () => { 225 | queue.enqueue('item'); 226 | const searchResult = queue.search('item'); 227 | expect(searchResult.value).toBe('item'); 228 | }); 229 | 230 | it('should not find an item that is not in the queue', () => { 231 | const searchResult = queue.search('item'); 232 | expect(searchResult).toBe(null); 233 | }); 234 | 235 | it('should search for an object in the queue', () => { 236 | const item = { id: 1, name: 'John' }; 237 | queue.enqueue(item); 238 | const searchResult = queue.search(item); 239 | expect(searchResult.value).toEqual(item); 240 | }); 241 | 242 | it('should bubble sort the queue', () => { 243 | queue.enqueue(5); 244 | queue.enqueue(1); 245 | queue.enqueue(3); 246 | queue.enqueue(2); 247 | queue.bubbleSort(); 248 | let current = queue.first; 249 | let values = []; 250 | while (current) { 251 | values.push(current.value); 252 | current = current.next; 253 | } 254 | expect(values).toEqual([1, 2, 3, 5]); 255 | }); 256 | 257 | it('should peek at the next item to be dequeued', () => { 258 | queue.enqueue('item'); 259 | const nextItem = queue.peek(); 260 | expect(nextItem.value).toBe('item'); 261 | }); 262 | 263 | it('should return the last item in the queue', () => { 264 | queue.enqueue('item1'); 265 | queue.enqueue('item2'); 266 | const lastItem = queue.tail(); 267 | expect(lastItem.value).toBe('item2'); 268 | }); 269 | 270 | it('should return null from search function when types do not match', () => { 271 | const queue = new Queue(); 272 | queue.enqueue(1); 273 | expect(queue.search('1')).toBeNull(); 274 | }); 275 | 276 | it('check() does not throw an error', () => { 277 | const queue = new Queue(); 278 | expect(() => queue.check()).not.toThrow(); 279 | }); 280 | 281 | it('check() does not iterate if the queue is empty', () => { 282 | const queue = new Queue(); 283 | const spy = jest.spyOn(console, 'log'); 284 | queue.check = function () { 285 | let current = this.first; 286 | while (current) { 287 | current = current.next; 288 | } 289 | }; 290 | 291 | queue.check(); 292 | 293 | expect(spy).not.toHaveBeenCalled(); 294 | spy.mockRestore(); 295 | }); 296 | }); 297 | -------------------------------------------------------------------------------- /__tests__/index.spec.js: -------------------------------------------------------------------------------- 1 | const mockeData = require('./utils'); 2 | const Master = require('../index'); 3 | 4 | process.env.transferEncryptToken = '00000000000000000000000000000000'; 5 | process.env.token = 'demoCHannel0'; 6 | process.env.LOG_LEVEL = 'off'; 7 | 8 | let master; 9 | beforeEach(() => { 10 | master = new Master(); 11 | }); 12 | 13 | describe('pushNewJob method', () => { 14 | it('Reject push job if not pass payload', async () => { 15 | await master.pushNewJob().catch((e) => { 16 | expect(e.message).toBe('payload is undefined'); 17 | }); 18 | }); 19 | it('Push job succesfully and enrcypted , if job is object', async () => { 20 | const encryptedPayload = master.crypt.encrypt( 21 | JSON.stringify(mockeData.payloadObject) 22 | ); 23 | await master.pushNewJob(mockeData.payloadObject); 24 | 25 | expect(encryptedPayload).toHaveProperty('iv'); 26 | expect(encryptedPayload).toHaveProperty('encryptedData'); 27 | expect(master.jobs.size).toEqual(1); 28 | expect(JSON.parse(master.jobs.dequeue().value)).toEqual( 29 | encryptedPayload 30 | ); 31 | }); 32 | it('Push job succesfully and enrcypted , if job is array', async () => { 33 | const encryptedPayload = master.crypt.encrypt( 34 | JSON.stringify(mockeData.payloadArray) 35 | ); 36 | await master.pushNewJob(mockeData.payloadArray); 37 | 38 | expect(encryptedPayload).toHaveProperty('iv'); 39 | expect(encryptedPayload).toHaveProperty('encryptedData'); 40 | expect(master.jobs.size).toEqual(1); 41 | expect(JSON.parse(master.jobs.dequeue().value)).toEqual( 42 | encryptedPayload 43 | ); 44 | }); 45 | it('Push job succesfully and enrcypted , if job is string', async () => { 46 | const jobData = '50'; 47 | const encryptedPayload = master.crypt.encrypt(jobData); 48 | await master.pushNewJob(jobData); 49 | 50 | expect(encryptedPayload).toHaveProperty('iv'); 51 | expect(encryptedPayload).toHaveProperty('encryptedData'); 52 | expect(master.jobs.size).toEqual(1); 53 | expect(JSON.parse(master.jobs.dequeue().value)).toEqual( 54 | encryptedPayload 55 | ); 56 | }); 57 | it('Push job succesfully and enrcypted , if job is number', async () => { 58 | const jobData = 50; 59 | const encryptedPayload = master.crypt.encrypt(jobData); 60 | await master.pushNewJob(jobData); 61 | 62 | expect(encryptedPayload).toHaveProperty('iv'); 63 | expect(encryptedPayload).toHaveProperty('encryptedData'); 64 | expect(master.jobs.size).toEqual(1); 65 | expect(JSON.parse(master.jobs.dequeue().value)).toEqual( 66 | encryptedPayload 67 | ); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /__tests__/utils.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | payloadObject: { 3 | a: 1, 4 | b: 2, 5 | }, 6 | payloadArray: [ 7 | { a: 1, b: 2 }, 8 | { a: 10, b: 20 }, 9 | ], 10 | }; 11 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | const prompts = require('prompts'); 3 | const { program } = require('commander'); 4 | const fs = require('fs'); 5 | const envfile = require('envfile'); 6 | const { version } = require('./package.json'); 7 | const Master = require('./src/index'); 8 | 9 | const { FgYellow, Reset } = require('./src/Logger'); 10 | 11 | const sourcePath = '.env'; 12 | 13 | program.version(version); 14 | 15 | function makeid(length) { 16 | let result = ''; 17 | const characters = 18 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 19 | const charactersLength = characters.length; 20 | for (let i = 0; i < length; i += 1) { 21 | result += characters.charAt( 22 | Math.floor(Math.random() * charactersLength) 23 | ); 24 | } 25 | return result; 26 | } 27 | async function setup() { 28 | console.log( 29 | 'Transfer Encrypt Token is used to encrypt data communications with workers.' 30 | ); 31 | console.log( 32 | 'Master and Workers have to pick the same Transfer Encrypt Token ..' 33 | ); 34 | console.log('Dont share it with dont trusty parties.'); 35 | const questions = [ 36 | { 37 | type: 'confirm', 38 | name: 'choice', 39 | message: 'Generate new random Transfer Encrypt Token ?', 40 | initial: true, 41 | }, 42 | ]; 43 | const genTransferEncryptToken = await prompts(questions); 44 | if (genTransferEncryptToken.choice) { 45 | const transferEncryptToken = { transferEncryptToken: makeid(32) }; 46 | console.log( 47 | `Share this token with your workers ${FgYellow}transferEncryptToken${Reset}: ${transferEncryptToken.transferEncryptToken}` 48 | ); 49 | fs.appendFileSync(sourcePath, envfile.stringify(transferEncryptToken)); 50 | } else { 51 | const transferEncryptTokenFromUser = await prompts({ 52 | type: 'text', 53 | name: 'transferEncryptToken', 54 | message: 'ENTER Transfer Encrypt Token [32 length string]', 55 | validate: (transferEncryptToken) => 56 | transferEncryptToken.length < 32 57 | ? 'Minimum length is 32' 58 | : true, 59 | }); 60 | fs.appendFileSync( 61 | sourcePath, 62 | envfile.stringify(transferEncryptTokenFromUser) 63 | ); 64 | } 65 | 66 | const genKeys = await prompts({ 67 | type: 'confirm', 68 | name: 'gen_keys', 69 | message: 70 | 'Create new random token [Will used to find your workers and them you]?', 71 | initial: true, 72 | }); 73 | if (!genKeys.gen_keys) { 74 | const hashFromUser = await prompts({ 75 | type: 'text', 76 | name: 'token', 77 | message: 'Enter your token [atleast 20 length string]', 78 | validate: (token) => 79 | token.length < 20 ? 'Minimum length is 20' : true, 80 | }); 81 | fs.appendFileSync(sourcePath, envfile.stringify(hashFromUser)); 82 | return; 83 | } 84 | const key = { token: makeid(20) }; 85 | console.log( 86 | `Share this token with your workers ${FgYellow}token${Reset}: ${key.token}` 87 | ); 88 | fs.appendFileSync(sourcePath, envfile.stringify(key)); 89 | 90 | console.log('Settings stored in .env'); 91 | } 92 | 93 | function resultCollect(result) { 94 | console.dir(result); 95 | } 96 | async function run() { 97 | const mm = new Master({ 98 | onResults: resultCollect, 99 | execAssets: { 100 | dependencies: [], // pass Worker dependencies like : ['big.js', 'moment'] 101 | files: [ 102 | // { masterPath: '/src/Logger.js', name: 'Logger.js', workerPath: '/workplace/Logger.js' }, 103 | // { masterPath: '/src/Helper.js', name: 'Helper.js', workerPath: '/workplace/Helper.js' }, 104 | // { masterPath: '/src/task.js', name: 'task.js', workerPath: '/workplace/task.js' }, 105 | ], 106 | }, 107 | }); 108 | const dummy = {}; 109 | 110 | let cnt = 0; 111 | for (let i = 0; i < 5; i += 1) { 112 | const payload = { 113 | id: cnt, 114 | data: JSON.stringify(dummy), 115 | }; 116 | // eslint-disable-next-line no-await-in-loop 117 | await mm.pushNewJob(payload).catch((e) => console.log(e)); 118 | // mm.pushNewJob(); 119 | cnt += 1; 120 | } 121 | // setInterval(async () => { 122 | // // console.dir(mm.getQueueLength()); 123 | // // const file = getBase64(path.join(process.cwd(), '/task.js')); 124 | 125 | // }, 100); 126 | } 127 | 128 | (async function main() { 129 | program.option('-s, --setup', 'Setup/Register master settings'); 130 | 131 | program.parse(process.argv); 132 | const options = program.opts(); 133 | switch (true) { 134 | case options.setup: 135 | await setup(); 136 | break; 137 | default: 138 | await run(); 139 | break; 140 | } 141 | })(); 142 | 143 | process.on('uncaughtException', (error) => { 144 | console.log(error.message); 145 | }); 146 | -------------------------------------------------------------------------------- /env.sample: -------------------------------------------------------------------------------- 1 | 2 | # token should be same for Master-Workers use something long above 20 chars , to avoid conflicts 3 | token= 4 | # transferEncryptToken should be 32 characters long 5 | transferEncryptToken= 6 | 7 | # LOG_LEVEL=off 8 | LOG_LEVEL=info 9 | # LOG_LEVEL=warn 10 | # LOG_LEVEL=error 11 | # LOG_LEVEL=fatal 12 | #LOG_LEVEL=debug 13 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | const airbnbBase = require('eslint-config-airbnb-base'); 2 | const prettier = require('prettier'); 3 | const globals = require('globals'); 4 | const jestPlugin = require('eslint-plugin-jest'); 5 | 6 | module.exports = [ 7 | { 8 | languageOptions: { 9 | ecmaVersion: 'latest', 10 | globals: { 11 | ...globals.commonjs, 12 | ...globals.es2021, 13 | ...globals.node, 14 | ...globals.jest, 15 | jest: true, 16 | describe: true, 17 | it: true, 18 | expect: true, 19 | beforeEach: true, 20 | afterEach: true, 21 | }, 22 | }, 23 | plugins: { 24 | jest: jestPlugin, 25 | prettier: prettier, 26 | airbnbBase, 27 | }, 28 | files: ['**/*.js', '**/*.ts'], 29 | rules: { 30 | 'jest/expect-expect': 'error', 31 | }, 32 | }, 33 | ]; 34 | -------------------------------------------------------------------------------- /examples/basic/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* eslint-disable import/no-unresolved */ 3 | const Master = require('../../index'); 4 | // const Master = require('queue-xec-master') 5 | 6 | // set here only for example , transferEncryptToken 7 | process.env.transferEncryptToken = '00000000000000000000000000000000'; 8 | process.env.token = 'demoCHannel0'; 9 | process.env.LOG_LEVEL = 'debug'; 10 | 11 | function resultCollect(result) { 12 | // handle here incoming results from workers.. 13 | console.dir(result); 14 | } 15 | async function run() { 16 | const mm = new Master({ 17 | onResults: resultCollect, 18 | execAssets: { 19 | dependencies: ['big.js', 'moment'], // pass Worker dependencies 20 | files: [ 21 | // { masterPath: '/../src/task.js', name: 'task.js', workerPath: '/workplace/task.js' }, // if task.js file not passed to workers , Master will use default one located in /src/task.js , here you can ovverride it by changing above details 22 | { 23 | masterPath: '/task.js', 24 | name: 'task.js', 25 | workerPath: '/workplace/task.js', 26 | }, // if task.js file not passed to workers , Master will use default one located in /src/task.js , here you can ovverride it by changing above details 27 | ], 28 | /* masterPath and workerPath are not absolute , NEVER access files out of their process.cwd() path. 29 | Are relative to their process current location , respectively */ 30 | }, 31 | }); 32 | const dummy = { 33 | x: 1, 34 | y: 2, 35 | }; 36 | 37 | let cnt = 0; 38 | for (let i = 0; i < 500; i += 1) { 39 | const payload = { 40 | id: cnt, 41 | data: JSON.stringify(dummy), 42 | }; 43 | // eslint-disable-next-line no-await-in-loop 44 | await mm.pushNewJob(payload).catch((e) => console.log(e)); 45 | cnt += 1; 46 | } 47 | } 48 | run(); 49 | -------------------------------------------------------------------------------- /examples/basic/readme.md: -------------------------------------------------------------------------------- 1 | # Basic Example 2 | 3 | ``` 4 | git clone https://github.com/queue-xec/master && cd master 5 | yarn --prod # npm install --only=prod 6 | cd examples/basic 7 | node index.js 8 | ``` 9 | 10 | pushNewJob returns a promise , if you need to throttle some how pushing new tasks in queue , use await , will resolves proportional of the current queue size. 11 | 12 | This is usefull for hardware limitations , for big data sets. Otherwise dont await at all , create the promise and leave it. 13 | -------------------------------------------------------------------------------- /examples/basic/task.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* eslint-disable import/no-unresolved */ 3 | /* 4 | only needed to sinulate work / delay 5 | const timer = (ms) => new Promise((res) => setTimeout(res, ms)); 6 | function randomIntFromInterval(min, max) { // min and max included 7 | return Math.floor(Math.random() * (max - min + 1) + min); 8 | } 9 | */ 10 | const moment = require('moment'); 11 | 12 | class Task { 13 | constructor() { 14 | this.data = null; 15 | } 16 | 17 | /** 18 | * Description: This method is the job procces. Receives job data (job) 19 | * Must return job results (Object) , to delivered in master. 20 | * @param {Object} job data from Master about job {id: Number , data: String (needs JSON.parse)} 21 | * @returns {Object} result 22 | */ 23 | async run(job) { 24 | console.dir(job); 25 | this.data = JSON.parse(job.data); 26 | 27 | // require your dependencies here or in any other class method is need... 28 | 29 | // code to run in workers 30 | return `-- ${Math.random()} on ${moment().format( 31 | 'MMMM Do YYYY, h:mm:ss a' 32 | )} -- result `; 33 | } 34 | } 35 | 36 | module.exports = Task; 37 | -------------------------------------------------------------------------------- /examples/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/queue-xec/master/b03864297d296f2bac9e50fabef221fbb6f6d4ef/examples/demo.gif -------------------------------------------------------------------------------- /examples/web_scraper/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | commonjs: true, 4 | es2021: true, 5 | node: true, 6 | jest: true, 7 | browser: true, 8 | }, 9 | extends: ['airbnb-base', 'prettier'], 10 | overrides: [], 11 | parserOptions: { 12 | ecmaVersion: 'latest', 13 | }, 14 | rules: {}, 15 | }; 16 | -------------------------------------------------------------------------------- /examples/web_scraper/README.md: -------------------------------------------------------------------------------- 1 | # Distributed Web Scraper 2 | 3 | In this example , we can see queue-xec lib in action , in a more real case scenario. 4 | We will target [dev.to](https://dev.to) website , to fetch latest posts including keyword `javascript`. 5 | * In first step , master visits , target page , discovers last 60 post urls , then assign as tasks these urls to workers. 6 | * Workers , will receive target urls , will visit each one, will fetch author name , post tile , post content and finally return that data to master. 7 | 8 | Master visiting target url to discover urls in first step , is not mandatory , you can use whatever source of target urls is available ( e.g pull data from a db ). 9 | 10 | ### Steps to run it 11 | 12 | ### Master 13 | 14 | * git clone https://github.com/queue-xec/master 15 | * cd master ; yarn 16 | * cd examples/web_scraper 17 | * yarn 18 | * node index.js 19 | 20 | ### Worker1 21 | 22 | * git clone https://github.com/queue-xec/worker 23 | * cd worker ; yarn 24 | * node index.js 25 | 26 | 27 | **Disclaimer:** 28 | *This project is for educational and research purposes only. We do not endorse or encourage any illegal or unauthorized use of web scraping. The purpose of this project is to demonstrate the capabilities and limitations of web scraping techniques and to provide a platform for learning and experimentation. The data and information collected through web scraping may be protected by copyright and other intellectual property laws. We are not responsible for any legal issues that may arise from the use of this project or the information collected through it. By using this project, you agree to adhere to all applicable laws and respect the intellectual property rights of others.* 29 | -------------------------------------------------------------------------------- /examples/web_scraper/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* eslint-disable import/no-unresolved */ 3 | const puppeteer = require('puppeteer'); 4 | const Master = require('../../index'); 5 | // const Master = require('queue-xec-master') 6 | 7 | // set here only for example , transferEncryptToken 8 | process.env.transferEncryptToken = '00000000000000000000000000000000'; 9 | process.env.token = 'demoCHannel0'; // In case someone else running this example , you may want 10 | // to change this token (here and in workers too), for privacy and not end up doing someone else tasks. 11 | process.env.LOG_LEVEL = 'debug'; 12 | 13 | function resultCollect(result) { 14 | // handle here incoming results from workers.. 15 | console.dir(result); 16 | } 17 | 18 | async function run() { 19 | const mm = new Master({ 20 | onResults: resultCollect, 21 | execAssets: { 22 | dependencies: ['puppeteer'], // pass Worker dependencies 23 | files: [ 24 | // { masterPath: '/../src/task.js', name: 'task.js', workerPath: '/workplace/task.js' }, // if task.js file not passed to workers , Master will use default one located in /src/task.js , here you can ovverride it by changing above details 25 | { 26 | masterPath: '/task.js', 27 | name: 'task.js', 28 | workerPath: '/workplace/task.js', 29 | }, // if task.js file not passed to workers , Master will use default one located in /src/task.js , here you can ovverride it by changing above details 30 | ], 31 | /* masterPath and workerPath are not absolute , NEVER access files out of their process.cwd() path. 32 | Are relative to their process current location , respectively */ 33 | }, 34 | }); 35 | 36 | const browser = await puppeteer.launch({ 37 | headless: true, 38 | args: [ 39 | '--no-sandbox', 40 | '--disable-setuid-sandbox', 41 | '--disable-gpu-sandbox', 42 | ], 43 | }); 44 | const page = await browser.newPage(); 45 | page.setViewport({ 46 | width: 1920, 47 | height: 1080, 48 | }); 49 | 50 | const targetTag = 'https://dev.to/t/javascript'; 51 | await page.goto(targetTag); 52 | const latestPosts = await page.evaluate(() => { 53 | const ans = []; 54 | // get latest post urls 55 | const posts = document.querySelectorAll( 56 | '.substories .crayons-story__body ' 57 | ); 58 | posts.forEach((body) => { 59 | const url = body.querySelector('.crayons-story__title a').href; 60 | if (url) ans.push({ author: null, title: null, url }); 61 | }); 62 | return ans; 63 | }); 64 | // loop on all posts found , and send them to workers 65 | for (let i = 0; i < latestPosts.length; i += 1) { 66 | const payload = { 67 | id: i, 68 | data: JSON.stringify(latestPosts[i]), 69 | }; 70 | // eslint-disable-next-line no-await-in-loop 71 | await mm.pushNewJob(payload).catch((e) => console.log(e)); 72 | } 73 | } 74 | run(); 75 | -------------------------------------------------------------------------------- /examples/web_scraper/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web_scraper", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "puppeteer": "^22.1.0" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/web_scraper/task.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* eslint-disable import/no-unresolved */ 3 | /* 4 | only needed to sinulate work / delay 5 | const timer = (ms) => new Promise((res) => setTimeout(res, ms)); 6 | function randomIntFromInterval(min, max) { // min and max included 7 | return Math.floor(Math.random() * (max - min + 1) + min); 8 | } 9 | */ 10 | const puppeteer = require('puppeteer'); 11 | 12 | class Task { 13 | constructor() { 14 | this.data = null; 15 | this.browser = null; 16 | this.page = null; 17 | this.setBrowser(); 18 | } 19 | 20 | async setBrowser() { 21 | if (!this.browser) 22 | this.browser = await puppeteer.launch({ 23 | headless: true, 24 | args: [ 25 | '--no-sandbox', 26 | '--disable-setuid-sandbox', 27 | '--disable-gpu-sandbox', 28 | ], 29 | }); 30 | if (!this.page) this.page = await this.browser.newPage(); 31 | this.page.setViewport({ 32 | width: 360, 33 | height: 760, 34 | }); 35 | } 36 | 37 | visitNFetch() { 38 | return new Promise((resolve, reject) => { 39 | this.page 40 | .goto(this.data.url) 41 | .then(() => { 42 | const content = this.page.evaluate((url) => { 43 | const ans = {}; 44 | ans.contentTxt = 45 | document.querySelector('.crayons-article__main') 46 | ?.innerText || ''; 47 | ans.title = 48 | document.querySelector( 49 | '.crayons-article__header__meta h1' 50 | )?.innerText || ''; 51 | ans.author = 52 | document.querySelector( 53 | '.crayons-article__header__meta .crayons-link' 54 | )?.innerText || ''; 55 | ans.url = url; 56 | return ans; 57 | }, this.data.url); 58 | resolve(content); 59 | }) 60 | .catch((e) => reject(e)); 61 | }); 62 | } 63 | 64 | /** 65 | * Description: This method is the job procces. Receives job data (job) 66 | * Must return job results (Object) , to delivered in master. 67 | * @param {Object} job data from Master about job {id: Number , data: String (needs JSON.parse)} 68 | * @returns {Object} result 69 | */ 70 | run(job) { 71 | return new Promise((resolve, reject) => { 72 | this.data = JSON.parse(job.data); 73 | console.log('Received task data: ', this.data); 74 | if (!this.page) { 75 | this.setBrowser().then(() => { 76 | this.visitNFetch() 77 | .then((content) => { 78 | resolve(content); 79 | }) 80 | .catch((e) => reject(e)); 81 | }); 82 | } else { 83 | this.visitNFetch() 84 | .then((content) => { 85 | resolve(content); 86 | }) 87 | .catch((e) => reject(e)); 88 | } 89 | }); 90 | } 91 | } 92 | 93 | module.exports = Task; 94 | -------------------------------------------------------------------------------- /examples/web_scraper/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@babel/code-frame@^7.0.0": 6 | version "7.23.5" 7 | resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244" 8 | integrity sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA== 9 | dependencies: 10 | "@babel/highlight" "^7.23.4" 11 | chalk "^2.4.2" 12 | 13 | "@babel/helper-validator-identifier@^7.22.20": 14 | version "7.22.20" 15 | resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" 16 | integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== 17 | 18 | "@babel/highlight@^7.23.4": 19 | version "7.23.4" 20 | resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.23.4.tgz#edaadf4d8232e1a961432db785091207ead0621b" 21 | integrity sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A== 22 | dependencies: 23 | "@babel/helper-validator-identifier" "^7.22.20" 24 | chalk "^2.4.2" 25 | js-tokens "^4.0.0" 26 | 27 | "@puppeteer/browsers@2.0.1": 28 | version "2.0.1" 29 | resolved "https://registry.yarnpkg.com/@puppeteer/browsers/-/browsers-2.0.1.tgz#ecae91d06cba83c58ce1e748b050cfc93d6861b2" 30 | integrity sha512-IQj/rJY1MNfZ6Z2ERu+6S0LkIPBSXRGddgmvODqjm1afHy04aJIiWmoohuFtL78SPSlbjpIMuFVfhyqsR5Ng4A== 31 | dependencies: 32 | debug "4.3.4" 33 | extract-zip "2.0.1" 34 | progress "2.0.3" 35 | proxy-agent "6.4.0" 36 | tar-fs "3.0.5" 37 | unbzip2-stream "1.4.3" 38 | yargs "17.7.2" 39 | 40 | "@tootallnate/quickjs-emscripten@^0.23.0": 41 | version "0.23.0" 42 | resolved "https://registry.yarnpkg.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz#db4ecfd499a9765ab24002c3b696d02e6d32a12c" 43 | integrity sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA== 44 | 45 | "@types/node@*": 46 | version "20.11.19" 47 | resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.19.tgz#b466de054e9cb5b3831bee38938de64ac7f81195" 48 | integrity sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ== 49 | dependencies: 50 | undici-types "~5.26.4" 51 | 52 | "@types/yauzl@^2.9.1": 53 | version "2.10.3" 54 | resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.3.tgz#e9b2808b4f109504a03cda958259876f61017999" 55 | integrity sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q== 56 | dependencies: 57 | "@types/node" "*" 58 | 59 | agent-base@^7.0.2, agent-base@^7.1.0: 60 | version "7.1.0" 61 | resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.0.tgz#536802b76bc0b34aa50195eb2442276d613e3434" 62 | integrity sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg== 63 | dependencies: 64 | debug "^4.3.4" 65 | 66 | ansi-regex@^5.0.1: 67 | version "5.0.1" 68 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" 69 | integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== 70 | 71 | ansi-styles@^3.2.1: 72 | version "3.2.1" 73 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" 74 | integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== 75 | dependencies: 76 | color-convert "^1.9.0" 77 | 78 | ansi-styles@^4.0.0: 79 | version "4.3.0" 80 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" 81 | integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== 82 | dependencies: 83 | color-convert "^2.0.1" 84 | 85 | argparse@^2.0.1: 86 | version "2.0.1" 87 | resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" 88 | integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== 89 | 90 | ast-types@^0.13.4: 91 | version "0.13.4" 92 | resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.4.tgz#ee0d77b343263965ecc3fb62da16e7222b2b6782" 93 | integrity sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w== 94 | dependencies: 95 | tslib "^2.0.1" 96 | 97 | b4a@^1.6.4: 98 | version "1.6.6" 99 | resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.6.6.tgz#a4cc349a3851987c3c4ac2d7785c18744f6da9ba" 100 | integrity sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg== 101 | 102 | bare-events@^2.0.0, bare-events@^2.2.0: 103 | version "2.2.0" 104 | resolved "https://registry.yarnpkg.com/bare-events/-/bare-events-2.2.0.tgz#a7a7263c107daf8b85adf0b64f908503454ab26e" 105 | integrity sha512-Yyyqff4PIFfSuthCZqLlPISTWHmnQxoPuAvkmgzsJEmG3CesdIv6Xweayl0JkCZJSB2yYIdJyEz97tpxNhgjbg== 106 | 107 | bare-fs@^2.1.1: 108 | version "2.1.5" 109 | resolved "https://registry.yarnpkg.com/bare-fs/-/bare-fs-2.1.5.tgz#55aae5f1c7701a83d7fbe62b0a57cfbee89a1726" 110 | integrity sha512-5t0nlecX+N2uJqdxe9d18A98cp2u9BETelbjKpiVgQqzzmVNFYWEAjQHqS+2Khgto1vcwhik9cXucaj5ve2WWA== 111 | dependencies: 112 | bare-events "^2.0.0" 113 | bare-os "^2.0.0" 114 | bare-path "^2.0.0" 115 | streamx "^2.13.0" 116 | 117 | bare-os@^2.0.0, bare-os@^2.1.0: 118 | version "2.2.0" 119 | resolved "https://registry.yarnpkg.com/bare-os/-/bare-os-2.2.0.tgz#24364692984d0bd507621754781b31d7872736b2" 120 | integrity sha512-hD0rOPfYWOMpVirTACt4/nK8mC55La12K5fY1ij8HAdfQakD62M+H4o4tpfKzVGLgRDTuk3vjA4GqGXXCeFbag== 121 | 122 | bare-path@^2.0.0, bare-path@^2.1.0: 123 | version "2.1.0" 124 | resolved "https://registry.yarnpkg.com/bare-path/-/bare-path-2.1.0.tgz#830f17fd39842813ca77d211ebbabe238a88cb4c" 125 | integrity sha512-DIIg7ts8bdRKwJRJrUMy/PICEaQZaPGZ26lsSx9MJSwIhSrcdHn7/C8W+XmnG/rKi6BaRcz+JO00CjZteybDtw== 126 | dependencies: 127 | bare-os "^2.1.0" 128 | 129 | base64-js@^1.3.1: 130 | version "1.5.1" 131 | resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" 132 | integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== 133 | 134 | basic-ftp@^5.0.2: 135 | version "5.0.4" 136 | resolved "https://registry.yarnpkg.com/basic-ftp/-/basic-ftp-5.0.4.tgz#28aeab7bfbbde5f5d0159cd8bb3b8e633bbb091d" 137 | integrity sha512-8PzkB0arJFV4jJWSGOYR+OEic6aeKMu/osRhBULN6RY0ykby6LKhbmuQ5ublvaas5BOwboah5D87nrHyuh8PPA== 138 | 139 | buffer-crc32@~0.2.3: 140 | version "0.2.13" 141 | resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" 142 | integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== 143 | 144 | buffer@^5.2.1: 145 | version "5.7.1" 146 | resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" 147 | integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== 148 | dependencies: 149 | base64-js "^1.3.1" 150 | ieee754 "^1.1.13" 151 | 152 | callsites@^3.0.0: 153 | version "3.1.0" 154 | resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" 155 | integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== 156 | 157 | chalk@^2.4.2: 158 | version "2.4.2" 159 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" 160 | integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== 161 | dependencies: 162 | ansi-styles "^3.2.1" 163 | escape-string-regexp "^1.0.5" 164 | supports-color "^5.3.0" 165 | 166 | chromium-bidi@0.5.9: 167 | version "0.5.9" 168 | resolved "https://registry.yarnpkg.com/chromium-bidi/-/chromium-bidi-0.5.9.tgz#f068bd10e8a3007bc842ae99f5038c8689bf58a5" 169 | integrity sha512-wOTX3m2zuHX0zRX4h7Ol1DAGz0cqHzo2IrAPvOqBxdd4ZR32vxg4FKNjmBihi1oP9b1QGSBBG5VNUUXUCsxDfg== 170 | dependencies: 171 | mitt "3.0.1" 172 | urlpattern-polyfill "10.0.0" 173 | 174 | cliui@^8.0.1: 175 | version "8.0.1" 176 | resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" 177 | integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== 178 | dependencies: 179 | string-width "^4.2.0" 180 | strip-ansi "^6.0.1" 181 | wrap-ansi "^7.0.0" 182 | 183 | color-convert@^1.9.0: 184 | version "1.9.3" 185 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" 186 | integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== 187 | dependencies: 188 | color-name "1.1.3" 189 | 190 | color-convert@^2.0.1: 191 | version "2.0.1" 192 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" 193 | integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== 194 | dependencies: 195 | color-name "~1.1.4" 196 | 197 | color-name@1.1.3: 198 | version "1.1.3" 199 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" 200 | integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== 201 | 202 | color-name@~1.1.4: 203 | version "1.1.4" 204 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" 205 | integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== 206 | 207 | cosmiconfig@9.0.0: 208 | version "9.0.0" 209 | resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-9.0.0.tgz#34c3fc58287b915f3ae905ab6dc3de258b55ad9d" 210 | integrity sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg== 211 | dependencies: 212 | env-paths "^2.2.1" 213 | import-fresh "^3.3.0" 214 | js-yaml "^4.1.0" 215 | parse-json "^5.2.0" 216 | 217 | cross-fetch@4.0.0: 218 | version "4.0.0" 219 | resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-4.0.0.tgz#f037aef1580bb3a1a35164ea2a848ba81b445983" 220 | integrity sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g== 221 | dependencies: 222 | node-fetch "^2.6.12" 223 | 224 | data-uri-to-buffer@^6.0.2: 225 | version "6.0.2" 226 | resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz#8a58bb67384b261a38ef18bea1810cb01badd28b" 227 | integrity sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw== 228 | 229 | debug@4, debug@4.3.4, debug@^4.1.1, debug@^4.3.4: 230 | version "4.3.4" 231 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" 232 | integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== 233 | dependencies: 234 | ms "2.1.2" 235 | 236 | degenerator@^5.0.0: 237 | version "5.0.1" 238 | resolved "https://registry.yarnpkg.com/degenerator/-/degenerator-5.0.1.tgz#9403bf297c6dad9a1ece409b37db27954f91f2f5" 239 | integrity sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ== 240 | dependencies: 241 | ast-types "^0.13.4" 242 | escodegen "^2.1.0" 243 | esprima "^4.0.1" 244 | 245 | devtools-protocol@0.0.1232444: 246 | version "0.0.1232444" 247 | resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1232444.tgz#406345a90a871ba852c530d73482275234936eed" 248 | integrity sha512-pM27vqEfxSxRkTMnF+XCmxSEb6duO5R+t8A9DEEJgy4Wz2RVanje2mmj99B6A3zv2r/qGfYlOvYznUhuokizmg== 249 | 250 | emoji-regex@^8.0.0: 251 | version "8.0.0" 252 | resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" 253 | integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== 254 | 255 | end-of-stream@^1.1.0: 256 | version "1.4.4" 257 | resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" 258 | integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== 259 | dependencies: 260 | once "^1.4.0" 261 | 262 | env-paths@^2.2.1: 263 | version "2.2.1" 264 | resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" 265 | integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== 266 | 267 | error-ex@^1.3.1: 268 | version "1.3.2" 269 | resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" 270 | integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== 271 | dependencies: 272 | is-arrayish "^0.2.1" 273 | 274 | escalade@^3.1.1: 275 | version "3.1.2" 276 | resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" 277 | integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== 278 | 279 | escape-string-regexp@^1.0.5: 280 | version "1.0.5" 281 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" 282 | integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== 283 | 284 | escodegen@^2.1.0: 285 | version "2.1.0" 286 | resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.1.0.tgz#ba93bbb7a43986d29d6041f99f5262da773e2e17" 287 | integrity sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w== 288 | dependencies: 289 | esprima "^4.0.1" 290 | estraverse "^5.2.0" 291 | esutils "^2.0.2" 292 | optionalDependencies: 293 | source-map "~0.6.1" 294 | 295 | esprima@^4.0.1: 296 | version "4.0.1" 297 | resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" 298 | integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== 299 | 300 | estraverse@^5.2.0: 301 | version "5.3.0" 302 | resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" 303 | integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== 304 | 305 | esutils@^2.0.2: 306 | version "2.0.3" 307 | resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" 308 | integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== 309 | 310 | extract-zip@2.0.1: 311 | version "2.0.1" 312 | resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" 313 | integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== 314 | dependencies: 315 | debug "^4.1.1" 316 | get-stream "^5.1.0" 317 | yauzl "^2.10.0" 318 | optionalDependencies: 319 | "@types/yauzl" "^2.9.1" 320 | 321 | fast-fifo@^1.1.0, fast-fifo@^1.2.0: 322 | version "1.3.2" 323 | resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c" 324 | integrity sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ== 325 | 326 | fd-slicer@~1.1.0: 327 | version "1.1.0" 328 | resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" 329 | integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g== 330 | dependencies: 331 | pend "~1.2.0" 332 | 333 | fs-extra@^11.2.0: 334 | version "11.2.0" 335 | resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" 336 | integrity sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw== 337 | dependencies: 338 | graceful-fs "^4.2.0" 339 | jsonfile "^6.0.1" 340 | universalify "^2.0.0" 341 | 342 | get-caller-file@^2.0.5: 343 | version "2.0.5" 344 | resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" 345 | integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== 346 | 347 | get-stream@^5.1.0: 348 | version "5.2.0" 349 | resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" 350 | integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== 351 | dependencies: 352 | pump "^3.0.0" 353 | 354 | get-uri@^6.0.1: 355 | version "6.0.3" 356 | resolved "https://registry.yarnpkg.com/get-uri/-/get-uri-6.0.3.tgz#0d26697bc13cf91092e519aa63aa60ee5b6f385a" 357 | integrity sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw== 358 | dependencies: 359 | basic-ftp "^5.0.2" 360 | data-uri-to-buffer "^6.0.2" 361 | debug "^4.3.4" 362 | fs-extra "^11.2.0" 363 | 364 | graceful-fs@^4.1.6, graceful-fs@^4.2.0: 365 | version "4.2.11" 366 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" 367 | integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== 368 | 369 | has-flag@^3.0.0: 370 | version "3.0.0" 371 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" 372 | integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== 373 | 374 | http-proxy-agent@^7.0.0, http-proxy-agent@^7.0.1: 375 | version "7.0.2" 376 | resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e" 377 | integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig== 378 | dependencies: 379 | agent-base "^7.1.0" 380 | debug "^4.3.4" 381 | 382 | https-proxy-agent@^7.0.2, https-proxy-agent@^7.0.3: 383 | version "7.0.4" 384 | resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz#8e97b841a029ad8ddc8731f26595bad868cb4168" 385 | integrity sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg== 386 | dependencies: 387 | agent-base "^7.0.2" 388 | debug "4" 389 | 390 | ieee754@^1.1.13: 391 | version "1.2.1" 392 | resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" 393 | integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== 394 | 395 | import-fresh@^3.3.0: 396 | version "3.3.0" 397 | resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" 398 | integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== 399 | dependencies: 400 | parent-module "^1.0.0" 401 | resolve-from "^4.0.0" 402 | 403 | ip-address@^9.0.5: 404 | version "9.0.5" 405 | resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-9.0.5.tgz#117a960819b08780c3bd1f14ef3c1cc1d3f3ea5a" 406 | integrity sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g== 407 | dependencies: 408 | jsbn "1.1.0" 409 | sprintf-js "^1.1.3" 410 | 411 | is-arrayish@^0.2.1: 412 | version "0.2.1" 413 | resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" 414 | integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== 415 | 416 | is-fullwidth-code-point@^3.0.0: 417 | version "3.0.0" 418 | resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" 419 | integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== 420 | 421 | js-tokens@^4.0.0: 422 | version "4.0.0" 423 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" 424 | integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== 425 | 426 | js-yaml@^4.1.0: 427 | version "4.1.0" 428 | resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" 429 | integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== 430 | dependencies: 431 | argparse "^2.0.1" 432 | 433 | jsbn@1.1.0: 434 | version "1.1.0" 435 | resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" 436 | integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== 437 | 438 | json-parse-even-better-errors@^2.3.0: 439 | version "2.3.1" 440 | resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" 441 | integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== 442 | 443 | jsonfile@^6.0.1: 444 | version "6.1.0" 445 | resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" 446 | integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== 447 | dependencies: 448 | universalify "^2.0.0" 449 | optionalDependencies: 450 | graceful-fs "^4.1.6" 451 | 452 | lines-and-columns@^1.1.6: 453 | version "1.2.4" 454 | resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" 455 | integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== 456 | 457 | lru-cache@^7.14.1: 458 | version "7.18.3" 459 | resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" 460 | integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== 461 | 462 | mitt@3.0.1: 463 | version "3.0.1" 464 | resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1" 465 | integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw== 466 | 467 | ms@2.1.2: 468 | version "2.1.2" 469 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" 470 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== 471 | 472 | netmask@^2.0.2: 473 | version "2.0.2" 474 | resolved "https://registry.yarnpkg.com/netmask/-/netmask-2.0.2.tgz#8b01a07644065d536383835823bc52004ebac5e7" 475 | integrity sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg== 476 | 477 | node-fetch@^2.6.12: 478 | version "2.7.0" 479 | resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" 480 | integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== 481 | dependencies: 482 | whatwg-url "^5.0.0" 483 | 484 | once@^1.3.1, once@^1.4.0: 485 | version "1.4.0" 486 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 487 | integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== 488 | dependencies: 489 | wrappy "1" 490 | 491 | pac-proxy-agent@^7.0.1: 492 | version "7.0.1" 493 | resolved "https://registry.yarnpkg.com/pac-proxy-agent/-/pac-proxy-agent-7.0.1.tgz#6b9ddc002ec3ff0ba5fdf4a8a21d363bcc612d75" 494 | integrity sha512-ASV8yU4LLKBAjqIPMbrgtaKIvxQri/yh2OpI+S6hVa9JRkUI3Y3NPFbfngDtY7oFtSMD3w31Xns89mDa3Feo5A== 495 | dependencies: 496 | "@tootallnate/quickjs-emscripten" "^0.23.0" 497 | agent-base "^7.0.2" 498 | debug "^4.3.4" 499 | get-uri "^6.0.1" 500 | http-proxy-agent "^7.0.0" 501 | https-proxy-agent "^7.0.2" 502 | pac-resolver "^7.0.0" 503 | socks-proxy-agent "^8.0.2" 504 | 505 | pac-resolver@^7.0.0: 506 | version "7.0.1" 507 | resolved "https://registry.yarnpkg.com/pac-resolver/-/pac-resolver-7.0.1.tgz#54675558ea368b64d210fd9c92a640b5f3b8abb6" 508 | integrity sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg== 509 | dependencies: 510 | degenerator "^5.0.0" 511 | netmask "^2.0.2" 512 | 513 | parent-module@^1.0.0: 514 | version "1.0.1" 515 | resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" 516 | integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== 517 | dependencies: 518 | callsites "^3.0.0" 519 | 520 | parse-json@^5.2.0: 521 | version "5.2.0" 522 | resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" 523 | integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== 524 | dependencies: 525 | "@babel/code-frame" "^7.0.0" 526 | error-ex "^1.3.1" 527 | json-parse-even-better-errors "^2.3.0" 528 | lines-and-columns "^1.1.6" 529 | 530 | pend@~1.2.0: 531 | version "1.2.0" 532 | resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" 533 | integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== 534 | 535 | progress@2.0.3: 536 | version "2.0.3" 537 | resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" 538 | integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== 539 | 540 | proxy-agent@6.4.0: 541 | version "6.4.0" 542 | resolved "https://registry.yarnpkg.com/proxy-agent/-/proxy-agent-6.4.0.tgz#b4e2dd51dee2b377748aef8d45604c2d7608652d" 543 | integrity sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ== 544 | dependencies: 545 | agent-base "^7.0.2" 546 | debug "^4.3.4" 547 | http-proxy-agent "^7.0.1" 548 | https-proxy-agent "^7.0.3" 549 | lru-cache "^7.14.1" 550 | pac-proxy-agent "^7.0.1" 551 | proxy-from-env "^1.1.0" 552 | socks-proxy-agent "^8.0.2" 553 | 554 | proxy-from-env@^1.1.0: 555 | version "1.1.0" 556 | resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" 557 | integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== 558 | 559 | pump@^3.0.0: 560 | version "3.0.0" 561 | resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" 562 | integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== 563 | dependencies: 564 | end-of-stream "^1.1.0" 565 | once "^1.3.1" 566 | 567 | puppeteer-core@22.1.0: 568 | version "22.1.0" 569 | resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-22.1.0.tgz#92faad0f9e6a7f9b1f488dc02e640254606730e6" 570 | integrity sha512-LdsQxslPf0Rpk6gLvkyyrraad2S4PUjGCT2CAKS2EnrRPpzIb6fsrFnoPNZxLlMkU7apU1g4Nf5wYePdSjxLkQ== 571 | dependencies: 572 | "@puppeteer/browsers" "2.0.1" 573 | chromium-bidi "0.5.9" 574 | cross-fetch "4.0.0" 575 | debug "4.3.4" 576 | devtools-protocol "0.0.1232444" 577 | ws "8.16.0" 578 | 579 | puppeteer@^22.1.0: 580 | version "22.1.0" 581 | resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-22.1.0.tgz#a3c839f51e0660f17b1623f0ae9dcc2282d4c40d" 582 | integrity sha512-suatHy6A48YkoykjrJNkJaixWVrvnPtzIgngK17V/P0MvgSyJzuu21PyR+0lWIK0cfZqKqmR8CHZvHzOCd4MIg== 583 | dependencies: 584 | "@puppeteer/browsers" "2.0.1" 585 | cosmiconfig "9.0.0" 586 | puppeteer-core "22.1.0" 587 | 588 | queue-tick@^1.0.1: 589 | version "1.0.1" 590 | resolved "https://registry.yarnpkg.com/queue-tick/-/queue-tick-1.0.1.tgz#f6f07ac82c1fd60f82e098b417a80e52f1f4c142" 591 | integrity sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag== 592 | 593 | require-directory@^2.1.1: 594 | version "2.1.1" 595 | resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" 596 | integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== 597 | 598 | resolve-from@^4.0.0: 599 | version "4.0.0" 600 | resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" 601 | integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== 602 | 603 | smart-buffer@^4.2.0: 604 | version "4.2.0" 605 | resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" 606 | integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== 607 | 608 | socks-proxy-agent@^8.0.2: 609 | version "8.0.2" 610 | resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-8.0.2.tgz#5acbd7be7baf18c46a3f293a840109a430a640ad" 611 | integrity sha512-8zuqoLv1aP/66PHF5TqwJ7Czm3Yv32urJQHrVyhD7mmA6d61Zv8cIXQYPTWwmg6qlupnPvs/QKDmfa4P/qct2g== 612 | dependencies: 613 | agent-base "^7.0.2" 614 | debug "^4.3.4" 615 | socks "^2.7.1" 616 | 617 | socks@^2.7.1: 618 | version "2.7.3" 619 | resolved "https://registry.yarnpkg.com/socks/-/socks-2.7.3.tgz#7d8a75d7ce845c0a96f710917174dba0d543a785" 620 | integrity sha512-vfuYK48HXCTFD03G/1/zkIls3Ebr2YNa4qU9gHDZdblHLiqhJrJGkY3+0Nx0JpN9qBhJbVObc1CNciT1bIZJxw== 621 | dependencies: 622 | ip-address "^9.0.5" 623 | smart-buffer "^4.2.0" 624 | 625 | source-map@~0.6.1: 626 | version "0.6.1" 627 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" 628 | integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== 629 | 630 | sprintf-js@^1.1.3: 631 | version "1.1.3" 632 | resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" 633 | integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== 634 | 635 | streamx@^2.13.0, streamx@^2.15.0: 636 | version "2.16.0" 637 | resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.16.0.tgz#b52e204b4e14c9225cb53f87e9e95bf5169c7cb8" 638 | integrity sha512-a7Fi0PoUeusrUcMS4+HxivnZqYsw2MFEP841TIyLxTcEIucHcJsk+0ARcq3tGq1xDn+xK7sKHetvfMzI1/CzMA== 639 | dependencies: 640 | fast-fifo "^1.1.0" 641 | queue-tick "^1.0.1" 642 | optionalDependencies: 643 | bare-events "^2.2.0" 644 | 645 | string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: 646 | version "4.2.3" 647 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" 648 | integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== 649 | dependencies: 650 | emoji-regex "^8.0.0" 651 | is-fullwidth-code-point "^3.0.0" 652 | strip-ansi "^6.0.1" 653 | 654 | strip-ansi@^6.0.0, strip-ansi@^6.0.1: 655 | version "6.0.1" 656 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" 657 | integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== 658 | dependencies: 659 | ansi-regex "^5.0.1" 660 | 661 | supports-color@^5.3.0: 662 | version "5.5.0" 663 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" 664 | integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== 665 | dependencies: 666 | has-flag "^3.0.0" 667 | 668 | tar-fs@3.0.5: 669 | version "3.0.5" 670 | resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-3.0.5.tgz#f954d77767e4e6edf973384e1eb95f8f81d64ed9" 671 | integrity sha512-JOgGAmZyMgbqpLwct7ZV8VzkEB6pxXFBVErLtb+XCOqzc6w1xiWKI9GVd6bwk68EX7eJ4DWmfXVmq8K2ziZTGg== 672 | dependencies: 673 | pump "^3.0.0" 674 | tar-stream "^3.1.5" 675 | optionalDependencies: 676 | bare-fs "^2.1.1" 677 | bare-path "^2.1.0" 678 | 679 | tar-stream@^3.1.5: 680 | version "3.1.7" 681 | resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-3.1.7.tgz#24b3fb5eabada19fe7338ed6d26e5f7c482e792b" 682 | integrity sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ== 683 | dependencies: 684 | b4a "^1.6.4" 685 | fast-fifo "^1.2.0" 686 | streamx "^2.15.0" 687 | 688 | through@^2.3.8: 689 | version "2.3.8" 690 | resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" 691 | integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== 692 | 693 | tr46@~0.0.3: 694 | version "0.0.3" 695 | resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" 696 | integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== 697 | 698 | tslib@^2.0.1: 699 | version "2.6.2" 700 | resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" 701 | integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== 702 | 703 | unbzip2-stream@1.4.3: 704 | version "1.4.3" 705 | resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" 706 | integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg== 707 | dependencies: 708 | buffer "^5.2.1" 709 | through "^2.3.8" 710 | 711 | undici-types@~5.26.4: 712 | version "5.26.5" 713 | resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" 714 | integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== 715 | 716 | universalify@^2.0.0: 717 | version "2.0.1" 718 | resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" 719 | integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== 720 | 721 | urlpattern-polyfill@10.0.0: 722 | version "10.0.0" 723 | resolved "https://registry.yarnpkg.com/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz#f0a03a97bfb03cdf33553e5e79a2aadd22cac8ec" 724 | integrity sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg== 725 | 726 | webidl-conversions@^3.0.0: 727 | version "3.0.1" 728 | resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" 729 | integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== 730 | 731 | whatwg-url@^5.0.0: 732 | version "5.0.0" 733 | resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" 734 | integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== 735 | dependencies: 736 | tr46 "~0.0.3" 737 | webidl-conversions "^3.0.0" 738 | 739 | wrap-ansi@^7.0.0: 740 | version "7.0.0" 741 | resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" 742 | integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== 743 | dependencies: 744 | ansi-styles "^4.0.0" 745 | string-width "^4.1.0" 746 | strip-ansi "^6.0.0" 747 | 748 | wrappy@1: 749 | version "1.0.2" 750 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 751 | integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== 752 | 753 | ws@8.16.0: 754 | version "8.16.0" 755 | resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4" 756 | integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ== 757 | 758 | y18n@^5.0.5: 759 | version "5.0.8" 760 | resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" 761 | integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== 762 | 763 | yargs-parser@^21.1.1: 764 | version "21.1.1" 765 | resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" 766 | integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== 767 | 768 | yargs@17.7.2: 769 | version "17.7.2" 770 | resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" 771 | integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== 772 | dependencies: 773 | cliui "^8.0.1" 774 | escalade "^3.1.1" 775 | get-caller-file "^2.0.5" 776 | require-directory "^2.1.1" 777 | string-width "^4.2.3" 778 | y18n "^5.0.5" 779 | yargs-parser "^21.1.1" 780 | 781 | yauzl@^2.10.0: 782 | version "2.10.0" 783 | resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" 784 | integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g== 785 | dependencies: 786 | buffer-crc32 "~0.2.3" 787 | fd-slicer "~1.1.0" 788 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const Master = require('./src'); 2 | 3 | module.exports = Master; 4 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-commonjs */ 2 | module.exports = { 3 | '**/*.(ts|js)': (filenames) => [ 4 | `yarn lint --fix ${filenames.join(' ')}`, 5 | `yarn format ${filenames.join(' ')}`, 6 | ], 7 | }; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "queue-xec-master", 3 | "version": "2.1.0", 4 | "description": "Master queue, push jobs and gathers results from online workers.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest --forceExit", 8 | "test:dev": "jest --watch ", 9 | "lint": "eslint ", 10 | "format": "prettier --write ", 11 | "prepare": "node -e \"if (process.env.NODE_ENV !== 'production'){process.exit(1)} \" || husky" 12 | }, 13 | "author": "KosM", 14 | "license": "MIT", 15 | "dependencies": { 16 | "bugout": "^0.0.13", 17 | "commander": "^13.0.0", 18 | "crypto": "^1.0.1", 19 | "dotenv": "^16.4.7", 20 | "envfile": "^7.1.0", 21 | "eslint": "^9.18.0", 22 | "events": "^3.3.0", 23 | "moment": "^2.30.1", 24 | "prompts": "^2.4.1" 25 | }, 26 | "devDependencies": { 27 | "@types/jest": "^29.5.14", 28 | "cz-conventional-changelog": "^3.3.0", 29 | "eslint-config-airbnb-base": "^15.0.0", 30 | "eslint-config-prettier": "^10.0.1", 31 | "eslint-plugin-import": "^2.31.0", 32 | "eslint-plugin-jest": "^28.11.0", 33 | "husky": "^9.1.7", 34 | "jest": "^29.7.0", 35 | "lint-staged": "^15.4.1", 36 | "prettier": "3.4.2" 37 | }, 38 | "config": { 39 | "commitizen": { 40 | "path": "./node_modules/cz-conventional-changelog" 41 | } 42 | }, 43 | "husky": { 44 | "hooks": { 45 | "prepare-commit-msg": "exec < /dev/tty && npx cz --hook || true", 46 | "pre-commit": "echo \"[Husky] pre-commit\"" 47 | } 48 | }, 49 | "jest": { 50 | "modulePathIgnorePatterns": [ 51 | "utils" 52 | ] 53 | } 54 | } -------------------------------------------------------------------------------- /src/Crypt.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | 3 | class Crypt { 4 | constructor(key) { 5 | this.key = key; 6 | if (!key) { 7 | throw new Error('transferEncryptToken not specified..'); 8 | } 9 | this.iv = crypto.randomBytes(16); 10 | } 11 | 12 | encrypt(text) { 13 | let textToEncrypt = text; 14 | try { 15 | if (typeof textToEncrypt === 'number') 16 | textToEncrypt = textToEncrypt.toString(); 17 | const cipher = crypto.createCipheriv( 18 | 'aes-256-cbc', 19 | Buffer.from(this.getKey()), 20 | this.getIv() 21 | ); 22 | let encrypted = cipher.update(textToEncrypt); 23 | encrypted = Buffer.concat([encrypted, cipher.final()]); 24 | return { 25 | iv: this.getIv().toString('hex'), 26 | encryptedData: encrypted.toString('hex'), 27 | }; 28 | } catch (e) { 29 | if (e.message.includes('Invalid key length')) { 30 | console.log('Crypt:', e.message, ' pick a 32 length key'); 31 | } else console.log('Crypt:', e.message); 32 | return null; 33 | } 34 | } 35 | 36 | decrypt(text) { 37 | try { 38 | const iv = Buffer.from(text.iv, 'hex'); 39 | const encryptedText = Buffer.from(text.encryptedData, 'hex'); 40 | const decipher = crypto.createDecipheriv( 41 | 'aes-256-cbc', 42 | Buffer.from(this.getKey()), 43 | iv 44 | ); 45 | let decrypted = decipher.update(encryptedText); 46 | decrypted = Buffer.concat([decrypted, decipher.final()]); 47 | return decrypted.toString(); 48 | } catch (e) { 49 | console.log('Crypt:', e.message); 50 | return null; 51 | } 52 | } 53 | 54 | getKey() { 55 | return this.key; 56 | } 57 | 58 | getIv() { 59 | return this.iv; 60 | } 61 | } 62 | 63 | module.exports = Crypt; 64 | -------------------------------------------------------------------------------- /src/Helper.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'); 2 | const crypto = require('crypto'); 3 | const fs = require('fs'); 4 | 5 | class Helper { 6 | /** 7 | * Description: 8 | * @param {Number} timestamp 9 | */ 10 | static LocalTime(timestamp) { 11 | return moment(timestamp).format('DD-MM HH:mm:ss'); 12 | } 13 | 14 | /** 15 | * Description: 16 | * @param {String} file 17 | */ 18 | static getSha256(file) { 19 | return new Promise((resolve, reject) => { 20 | try { 21 | if (fs.existsSync(file)) { 22 | const fileBuffer = fs.readFileSync(file); 23 | const hashSum = crypto.createHash('sha256'); 24 | hashSum.update(fileBuffer); 25 | resolve(hashSum.digest('hex')); 26 | } else { 27 | reject(new Error('File not found')); 28 | } 29 | } catch (err) { 30 | reject(err); 31 | } 32 | }); 33 | } 34 | 35 | static sleep(ms) { 36 | return new Promise((resolve) => { 37 | setTimeout(resolve, ms); 38 | }); 39 | } 40 | } 41 | 42 | module.exports = { 43 | Helper, 44 | }; 45 | -------------------------------------------------------------------------------- /src/Logger.js: -------------------------------------------------------------------------------- 1 | const FgYellow = '\x1b[33m'; 2 | const FgRed = '\x1b[31m'; 3 | const FgCyan = '\x1b[36m'; 4 | const FgMagenta = '\x1b[35m'; 5 | const FgGreen = '\x1b[32m'; 6 | const Reset = '\x1b[0m'; 7 | 8 | const { Helper } = require('./Helper'); 9 | // LOG levels : Off (0) -> Info (1) -> Warn (2) -> Error (3) -> Debug (4) 10 | class Logger { 11 | constructor({ level = 'off', writeStream = false } = {}) { 12 | this.level = this.translateToCode(level); 13 | this.stream = writeStream; 14 | this.debug('Logger instantiated with loglevel:', this.level); 15 | } 16 | 17 | /** 18 | * Description: Convert human readable log level to level codes 19 | * @param {String} level 20 | */ 21 | translateToCode(level) { 22 | let code; 23 | switch (level.toLowerCase()) { 24 | case 'off': 25 | code = 0; 26 | break; 27 | case 'info': 28 | code = 1; 29 | break; 30 | case 'warn': 31 | code = 2; 32 | break; 33 | case 'error': 34 | code = 3; 35 | break; 36 | case 'debug': 37 | code = 4; 38 | break; 39 | default: 40 | code = 0; 41 | break; 42 | } 43 | return code; 44 | } 45 | 46 | /** 47 | * Description: Print messages for loglevel info 48 | * @param {String| Object} msg 49 | * @param {Object} extra 50 | * 51 | */ 52 | info(msg, extra) { 53 | // level 1 54 | if (this.level < 1) return; 55 | let message = msg; 56 | if (extra) message += ` ${extra}`; 57 | if (typeof msg === 'string') { 58 | console.log(`${Helper.LocalTime(Date.now())} [INFO] > ${message}`); 59 | this.writeToFile( 60 | `${Helper.LocalTime(Date.now())} ${message}`, 61 | extra 62 | ); 63 | } else { 64 | console.log(`${Helper.LocalTime(Date.now())} [INFO] > `, message); 65 | this.writeToFile( 66 | `${Helper.LocalTime(Date.now())} ${JSON.stringify(message)}`, 67 | extra 68 | ); 69 | } 70 | } 71 | 72 | warn(msg, extra) { 73 | // level 2 74 | if (this.level < 2) return; 75 | let message = msg; 76 | if (extra) { 77 | if (typeof extra === 'string' && typeof message === 'string') { 78 | message += ` ${extra}`; 79 | console.log( 80 | `${Helper.LocalTime( 81 | Date.now() 82 | )} [${FgYellow}WARN${Reset}] > ${FgYellow} ${message}`, 83 | Reset 84 | ); 85 | this.writeToFile(`[WARN] ${message}`, extra); 86 | } else { 87 | console.log( 88 | `${Helper.LocalTime( 89 | Date.now() 90 | )} [WARN] > ${FgRed} ${message} `, 91 | extra 92 | ); 93 | this.writeToFile(`[WARN] ${JSON.stringify(message)}`, extra); 94 | } 95 | return; 96 | } 97 | if (typeof msg === 'string') { 98 | console.log( 99 | `${Helper.LocalTime( 100 | Date.now() 101 | )} [${FgYellow}WARN${Reset}] > ${FgYellow} ${msg}`, 102 | Reset 103 | ); 104 | this.writeToFile(`[WARN] ${msg}`, extra); 105 | } else { 106 | console.log( 107 | `${Helper.LocalTime(Date.now())} [WARN] > ${FgRed} `, 108 | msg 109 | ); 110 | this.writeToFile(`[WARN] ${JSON.stringify(msg)}`, extra); 111 | } 112 | } 113 | 114 | error(msg, extra) { 115 | // level 3 116 | if (this.level < 3) return; 117 | let message = msg; 118 | if (extra) message += ` ${extra}`; 119 | if (typeof msg === 'string') 120 | console.log( 121 | `${Helper.LocalTime(Date.now())} [ERROR] > ${FgRed} ${message}`, 122 | Reset 123 | ); 124 | else 125 | console.log( 126 | `${Helper.LocalTime(Date.now())} [ERROR] > ${FgRed} `, 127 | message 128 | ); 129 | } 130 | 131 | fatal(msg, extra) { 132 | // 133 | if (!msg) { 134 | console.log(`[FATAL] > ${FgRed} `, 'Unknown msg passed to Logger'); 135 | process.exit(-1); 136 | } 137 | const message = msg; 138 | if (extra) { 139 | if (typeof message === 'string') 140 | console.log(`[FATAL] > ${FgRed} ${message}`, extra, Reset); 141 | else console.log(`[FATAL] > ${FgRed} `, message, extra, Reset); 142 | process.exit(-1); 143 | } 144 | if (typeof message === 'string') 145 | console.log(`[FATAL] > ${FgRed} ${message}`, Reset); 146 | else console.log(`[FATAL] > ${FgRed} `, message, Reset); 147 | process.exit(-1); 148 | } 149 | 150 | debug(msg, extra) { 151 | if (this.level < 4 || !msg) return; 152 | const message = msg; 153 | if (typeof message === 'string') { 154 | if (extra) { 155 | console.log( 156 | `${Helper.LocalTime( 157 | Date.now() 158 | )} [${FgMagenta}DEBUG${Reset}]> ${message}`, 159 | extra 160 | ); 161 | this.writeToFile( 162 | `${Helper.LocalTime(Date.now())} ${message}`, 163 | extra 164 | ); 165 | return; 166 | } 167 | console.debug( 168 | `${Helper.LocalTime( 169 | Date.now() 170 | )} [${FgMagenta}DEBUG${Reset}]> ${message}` 171 | ); 172 | this.writeToFile( 173 | `${Helper.LocalTime(Date.now())} ${message}`, 174 | extra 175 | ); 176 | } else { 177 | if (extra) { 178 | console.log( 179 | `${Helper.LocalTime( 180 | Date.now() 181 | )} [${FgMagenta}DEBUG${Reset}]> ${message}`, 182 | extra 183 | ); 184 | this.writeToFile(msg, extra); 185 | this.writeToFile( 186 | `${Helper.LocalTime(Date.now())} ${JSON.stringify( 187 | message 188 | )}`, 189 | extra 190 | ); 191 | return; 192 | } 193 | console.log( 194 | `${Helper.LocalTime( 195 | Date.now() 196 | )} [${FgMagenta}DEBUG${Reset}] > `, 197 | message 198 | ); 199 | this.writeToFile( 200 | `${Helper.LocalTime(Date.now())} ${JSON.stringify(message)}`, 201 | extra 202 | ); 203 | } 204 | } 205 | 206 | writeToFile(msg, extra = '') { 207 | if (this.stream) { 208 | if (typeof msg === 'object') { 209 | this.stream.write(`${JSON.stringify(msg)}`); 210 | } else { 211 | this.stream.write(msg); 212 | } 213 | if (typeof extra === 'object') { 214 | this.stream.write(`${JSON.stringify(extra)}`); 215 | } else { 216 | this.stream.write(`${extra}`); 217 | } 218 | this.stream.write('\n'); 219 | } 220 | } 221 | } 222 | 223 | module.exports = { 224 | Logger, 225 | FgRed, 226 | FgYellow, 227 | FgCyan, 228 | FgMagenta, 229 | FgGreen, 230 | Reset, 231 | }; 232 | -------------------------------------------------------------------------------- /src/Node.js: -------------------------------------------------------------------------------- 1 | class Node { 2 | constructor(value) { 3 | this.value = value; 4 | this.next = null; 5 | this.prev = null; 6 | } 7 | } 8 | 9 | module.exports = Node; 10 | -------------------------------------------------------------------------------- /src/Queue.js: -------------------------------------------------------------------------------- 1 | const Node = require('./Node'); 2 | 3 | class Queue { 4 | constructor() { 5 | this.first = null; 6 | this.last = null; 7 | this.size = 0; 8 | } 9 | 10 | isEmpty() { 11 | return !this.size; 12 | } 13 | 14 | enqueue(item) { 15 | // Create node 16 | const newNode = new Node(item); 17 | /** 18 | * * If our list is empty than both our 19 | * * first item and last item is going to point the new node. 20 | */ 21 | if (this.isEmpty()) { 22 | this.first = newNode; 23 | this.last = newNode; 24 | newNode.prev = null; 25 | } else { 26 | newNode.prev = this.last; 27 | 28 | this.last.next = newNode; 29 | this.last = newNode; 30 | } 31 | this.size += 1; 32 | return this; 33 | } 34 | /** 35 | * 36 | * @returns 37 | */ 38 | 39 | dequeue() { 40 | //* if our queue is empty we return null 41 | if (this.isEmpty()) return null; 42 | const itemToBeRemoved = this.first; 43 | /** 44 | * * if both our first and last node are pointing the same item 45 | * * we dequeued our last node. 46 | */ 47 | if (this.first === this.last) { 48 | this.last = null; 49 | } 50 | this.first = this.first.next; 51 | this.size -= 1; 52 | return itemToBeRemoved; 53 | } 54 | 55 | check() { 56 | let current = this.first; 57 | while (current) { 58 | // while not null 59 | current = current.next; 60 | } 61 | } 62 | 63 | /** 64 | * Searches inside list in linear fashion , 65 | * Time complexity O(n) 66 | * @param {*} item String , Number , Object 67 | * @returns Node , null 68 | */ 69 | search(item) { 70 | let current = this.first; 71 | if (current != null && typeof item !== typeof current.value) { 72 | return null; 73 | } 74 | while (current !== null) { 75 | if (current.value === item) { 76 | return current; 77 | } 78 | if (typeof item === 'object') { 79 | const itemKeys = Object.keys(item); 80 | for (let i = 0; i < itemKeys.length; i += 1) { 81 | // check all item properties agains current.value properties 82 | if ( 83 | Object.prototype.hasOwnProperty.call( 84 | current.value, 85 | itemKeys[i] 86 | ) 87 | ) { 88 | if (current.value[itemKeys[i]] === item[itemKeys[i]]) { 89 | return current; 90 | } 91 | } 92 | } 93 | } 94 | current = current.next; 95 | } 96 | return null; 97 | } 98 | 99 | bubbleSort() { 100 | let { last } = this; 101 | while (last) { 102 | let node = this.first; 103 | while (node !== last) { 104 | const { next } = node; 105 | if (typeof node.value === 'object') { 106 | // object comparision in first matched (common) property 107 | const itemKeys = Object.keys(node.value); 108 | for (let i = 0; i < itemKeys.length; i += 1) { 109 | if ( 110 | Object.prototype.hasOwnProperty.call( 111 | next.value, 112 | itemKeys[i] 113 | ) 114 | ) { 115 | if ( 116 | node.value[itemKeys[i]] > 117 | next.value[itemKeys[i]] 118 | ) { 119 | // swap 120 | [node.value, next.value] = [ 121 | next.value, 122 | node.value, 123 | ]; 124 | break; 125 | } 126 | } 127 | } 128 | } else if (typeof node.value === 'number') { 129 | if (node.value > next.value) { 130 | // swap 131 | [node.value, next.value] = [next.value, node.value]; 132 | } 133 | } else if (typeof node.value === 'string') { 134 | // Not Implemented yet 135 | } 136 | 137 | node = next; 138 | } 139 | last = last.prev; // shorten the range that must be bubbled through 140 | } 141 | } 142 | 143 | /** 144 | * * Returns the next element to be dequeued. 145 | * @returns 146 | */ 147 | peek() { 148 | return this.first; 149 | } 150 | 151 | tail() { 152 | return this.last; 153 | } 154 | } 155 | 156 | module.exports = Queue; 157 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | const Bugout = require('bugout'); 3 | const events = require('events'); 4 | const fs = require('fs'); 5 | const { Logger } = require('./Logger'); 6 | const { Helper } = require('./Helper'); 7 | const Crypt = require('./Crypt'); 8 | const Queue = require('./Queue'); 9 | 10 | const defaultExecAssets = [ 11 | { 12 | masterPath: '/src/task.js', // location of file starting from master's dir 13 | name: 'task.js', // name to save as in workers 14 | workerPath: '/workplace/task.js', // remote location of file starting from workers's dir 15 | sha256: null, // hash of file ,will generate whenever a workers 16 | // asks for assets.(to detect file changes) 17 | }, 18 | ]; 19 | require('dotenv').config(); 20 | 21 | class Master { 22 | constructor({ 23 | token = null, 24 | execAssets = [], 25 | onResults = () => {}, 26 | loglevel, 27 | transferEncryptToken = null, 28 | } = {}) { 29 | this.availableWorkers = []; 30 | this.jobs = new Queue(); 31 | 32 | this.event = new events.EventEmitter(); 33 | this.log = new Logger({ level: loglevel || process.env.LOG_LEVEL }); 34 | this.token = 35 | token || 36 | process.env.token || 37 | new Error( 38 | 'token not defined, pass it in constructor or with env variable "token" , or generate it with cli.' 39 | ); 40 | this.crypt = null; // instatiate in init method 41 | this.peer = Bugout(this.token); 42 | this.onResults = onResults; 43 | this.execAssets = execAssets; 44 | 45 | this.onSeen = this.onSeen.bind(this); 46 | this.init = this.init.bind(this); 47 | this.requestExecAssets = this.requestExecAssets.bind(this); 48 | this.encryptFiles = this.encryptFiles.bind(this); 49 | 50 | // this.peer.on('rpc', this.onRpcCall); 51 | this.peer.on('seen', this.onSeen); 52 | this.peer.on('message', this.onMessage); 53 | // this.peer.on('rpc-response', (address, nonce, response)=> ); 54 | // this.peer.on('left', (address)=> ); 55 | 56 | this.event.addListener('init', this.init); 57 | this.event.addListener('resultsShared', this.onResults); 58 | this.event.emit('init', transferEncryptToken); 59 | } 60 | 61 | async init(transferEncryptToken) { 62 | this.log.info('Address:', this.peer.address()); 63 | this.log.info('Seed:', this.peer.seed); 64 | this.log.info('Announcing to trackers...'); 65 | if (Array.isArray(this.execAssets.files)) { 66 | const existTaskFromUser = this.execAssets.files.filter( 67 | (file) => file.name === 'task.js' 68 | ); 69 | if (existTaskFromUser.length > 1) 70 | this.log.fatal( 71 | 'You can pass only one file with name task.js check execAssets.files!' 72 | ); 73 | if (existTaskFromUser.length === 0) { 74 | this.log.debug( 75 | 'populating execAssets with default values , not found task.js from user.' 76 | ); 77 | this.execAssets.files = [ 78 | ...this.execAssets.files, 79 | ...defaultExecAssets, 80 | ]; 81 | } 82 | } 83 | this.crypt = new Crypt( 84 | transferEncryptToken || process.env.transferEncryptToken 85 | ); 86 | if (this.crypt.key instanceof Error) { 87 | this.log.fatal('Crypt: ', this.crypt); 88 | } 89 | this.registerRPC(); 90 | } 91 | 92 | registerRPC() { 93 | this.peer.register( 94 | 'ping', 95 | (pk, args, cb) => { 96 | args.pong = true; 97 | cb(args); 98 | }, 99 | "Respond to ping with 'pong'." 100 | ); 101 | this.peer.register('isMaster', (pk, args, cb) => { 102 | args.isMaster = 'yes'; 103 | cb(args); 104 | }); 105 | this.peer.register('requestWork', (pk, args, cb) => { 106 | switch (true) { 107 | case this.jobs.size > 1: { 108 | if (args.getBatch) { 109 | args.batchTasks = []; 110 | let totalJobsSend = args.batchSize; 111 | if (this.jobs.size <= args.batchSize) totalJobsSend = 1; // get only available 112 | for (let i = 0; i < totalJobsSend; i += 1) { 113 | const queuedJob = this.jobs.dequeue(); 114 | if (!queuedJob) break; 115 | args.batchTasks.push(queuedJob.value); 116 | } 117 | // args.batchTasks = this.jobs.splice(0, args.batchSize); // splice mutates original array which slices it 118 | this.log.debug( 119 | `task queue reduced:${this.jobs.size} - ${args.batchSize}` 120 | ); 121 | break; 122 | } 123 | const queuedJob = this.jobs.dequeue(); 124 | args.task = null; 125 | if (queuedJob) { 126 | args.task = queuedJob.value; 127 | this.log.debug(`task queue reduced:${this.jobs.size}`); 128 | } 129 | break; 130 | } 131 | case this.jobs.size === 1: { 132 | const queuedJob = this.jobs.dequeue(); 133 | args.task = null; 134 | if (queuedJob) { 135 | args.task = queuedJob.value; 136 | this.log.debug(`task queue reduced:${this.jobs.size}`); 137 | } 138 | break; 139 | } 140 | case this.jobs.size === 0: { 141 | args.task = null; 142 | this.log.debug(`task queue is empty:${this.jobs.size}`); 143 | break; 144 | } 145 | default: { 146 | args.task = null; 147 | this.log.warning(`task queue is empty:${this.jobs.size}`); 148 | 149 | break; 150 | } 151 | } 152 | cb(args); 153 | }); 154 | this.peer.register('shareResults', (pk, args) => { 155 | const results = JSON.parse(this.crypt.decrypt(JSON.parse(args))); 156 | this.event.emit('resultsShared', results); 157 | }); 158 | this.peer.register('requestExecAssets', (pk, args, cb) => { 159 | const currentHash = args?.currentHash; // hash of current assets array 160 | // if is same , not send again 161 | this.requestExecAssets(currentHash).then((ans) => { 162 | cb(ans); 163 | }); 164 | }); 165 | } 166 | 167 | /** Called when some of connected peers send us a message */ 168 | onMessage(address, message) { 169 | this.log.debug(`message from ${address} :`, message); 170 | } 171 | 172 | /** Called when a peer calls rpc on this node */ 173 | // onRpcCall(hash, call) { 174 | // } 175 | 176 | onSeen(newPeerAddress) { 177 | this.log.info('New peer seen : ', newPeerAddress); 178 | // this.event.emit('worker_joined', newPeerAddress) 179 | } 180 | 181 | /** 182 | * Description: Pushes new jobs to task querue 183 | * @param { { id: string , data: JSON }} payload 184 | * @returns Promise 185 | */ 186 | async pushNewJob(payload) { 187 | return new Promise((resolve, reject) => { 188 | if (typeof payload === 'undefined') { 189 | this.log.warn('pushNewJob:', 'payload is undefined'); 190 | reject(Error('payload is undefined')); 191 | } 192 | let payloadJson = payload; 193 | if (typeof payloadJson === 'object') 194 | payloadJson = JSON.stringify(payload); 195 | 196 | const encryptedPayload = this.crypt.encrypt(payloadJson); 197 | this.log.debug('pushNewJob payload: ', payload); 198 | if (this.jobs.size >= 1000) { 199 | Helper.sleep(this.jobs.size * 0.3).then(() => { 200 | this.jobs.enqueue(JSON.stringify(encryptedPayload)); 201 | resolve(); 202 | }); 203 | } else { 204 | this.jobs.enqueue(JSON.stringify(encryptedPayload)); 205 | resolve(); 206 | } 207 | }); 208 | } 209 | 210 | /** 211 | * Description: Call populateSha256OnAssets to geneate current hashs of all files , 212 | * and then compare all file hashes with given currentHash 213 | * @param {String} currentHash 214 | * @returns Promise 215 | * @resolves {String} same || changed 216 | */ 217 | async requestExecAssets(currentHash) { 218 | // const assets = await this.populateSha256OnAssets(this.execAssets); 219 | // this.execAssets = assets; // update global exec Assets 220 | let assets = this.execAssets.files; 221 | let unifiedHash = ''; 222 | for (let i = 0; i < assets.length; i += 1) { 223 | // 224 | const file = assets[i]; 225 | // eslint-disable-next-line no-await-in-loop 226 | const sha = await Helper.getSha256( 227 | `${process.cwd()}${file.masterPath}` 228 | ).catch((e) => { 229 | // usually not found reject error here 230 | if (e.message.includes('not found')) { 231 | this.log.warn( 232 | `File ${file.masterPath}`, 233 | `not found in ${process.cwd()}${file.masterPath}` 234 | ); 235 | } else { 236 | this.log.warn(file.masterPath, e.message); 237 | } 238 | }); 239 | if (sha) { 240 | // not sha -> file not found 241 | this.execAssets.files[i].sha256 = sha; 242 | unifiedHash += sha; // join all hashes in one , and compare these 243 | } 244 | } 245 | // eslint-disable-next-line no-prototype-builtins 246 | assets = assets.filter((item) => item.hasOwnProperty('sha256')); // remove not found files in master 247 | this.execAssets.files = JSON.parse(JSON.stringify(assets)); // deep copy object to class prop 248 | this.log.warn('assets info :', this.execAssets); 249 | this.log.debug('currentHash :', currentHash); 250 | this.log.debug('unnifiedHash :', unifiedHash); 251 | if (currentHash === unifiedHash) { 252 | // all joined hashes should 253 | // match workers remote joined hashes 254 | return { status: 'same', files: [] }; 255 | } 256 | const { encryptedFiles, dependencies } = this.encryptFiles( 257 | this.execAssets 258 | ); 259 | return { 260 | status: 'changed', 261 | dependencies, 262 | files: encryptedFiles, 263 | }; 264 | } 265 | 266 | /** 267 | * Description: Reads assets defined , if exist try to encrypt data from files. 268 | * @param {Array} files 269 | */ 270 | encryptFiles(execAssets) { 271 | const assetFiles = execAssets.files; 272 | for (let i = 0; i < assetFiles.length; i += 1) { 273 | const asset = assetFiles[i]; 274 | try { 275 | if (fs.existsSync(`${process.cwd()}${asset.masterPath}`)) { 276 | const fileContent = fs 277 | .readFileSync(`${process.cwd()}${asset.masterPath}`, { 278 | encoding: 'utf8', 279 | }) 280 | .toString(); 281 | const encrypted = this.crypt.encrypt(fileContent); 282 | asset.content = encrypted; 283 | } 284 | } catch (err) { 285 | this.log.warn(err); 286 | if (err.message === 'File not found') 287 | assetFiles.remove(assetFiles[i]); 288 | } 289 | } 290 | return { 291 | encryptedFiles: assetFiles, 292 | dependencies: execAssets.dependencies, 293 | }; 294 | } 295 | } 296 | 297 | module.exports = Master; 298 | -------------------------------------------------------------------------------- /src/task.js: -------------------------------------------------------------------------------- 1 | /* 2 | only needed to sinulate work / delay 3 | const timer = (ms) => new Promise((res) => setTimeout(res, ms)); 4 | function randomIntFromInterval(min, max) { // min and max included 5 | return Math.floor(Math.random() * (max - min + 1) + min); 6 | } 7 | */ 8 | // Requre here libs you passed from master as dependencies 9 | // if needed here. 10 | // const { Big } = require('big.js'); 11 | 12 | class Task { 13 | constructor() { 14 | this.data = null; 15 | } 16 | 17 | /** 18 | * Description: This method is the job procces. Receives job data (job) 19 | * Must return job results (Object) , to delivered in master. 20 | * @param {Object} job data from Master about job {id: Number , data: String (needs JSON.parse)} 21 | * @returns {Object} result 22 | */ 23 | async run(job) { 24 | const data = JSON.parse(job.data); 25 | this.data = data; 26 | // eslint-disable-next-line no-console 27 | console.dir(job); 28 | // code to run in workers 29 | return {}; 30 | } 31 | } 32 | 33 | module.exports = Task; 34 | --------------------------------------------------------------------------------