From a93459d01ef63262443b7a855134d9d1a96d4618 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juho=20H=C3=A4nninen?= Date: Thu, 20 Jan 2022 10:12:10 +0200 Subject: [PATCH] Merge pull request #303 from HSLdevcom/MM-314 MM-314 Add worker queue --- .env | 1 + .env.dev | 1 + .env.production | 1 + .env.stage | 1 + Dockerfile | 1 + README.md | 39 ++++-- constants.js | 1 + package.json | 4 + scripts/fileHandler.js | 82 ++++++++++++ scripts/server.js | 56 +++----- scripts/{generator.js => worker.js} | 162 +++++++++++------------ yarn.lock | 194 +++++++++++++++++++++++++++- 12 files changed, 411 insertions(+), 132 deletions(-) create mode 100644 scripts/fileHandler.js rename scripts/{generator.js => worker.js} (58%) diff --git a/.env b/.env index d13ae591..a06142d2 100644 --- a/.env +++ b/.env @@ -1,4 +1,5 @@ PG_CONNECTION_STRING= +REDIS_CONNECTION_STRING= JORE_GRAPHQL_URL=https://dev.kartat.hsl.fi/jore/graphql GENERATE_API_URL=http://localhost:8000 diff --git a/.env.dev b/.env.dev index 463cf024..defaead9 100644 --- a/.env.dev +++ b/.env.dev @@ -1,4 +1,5 @@ PG_CONNECTION_STRING= +REDIS_CONNECTION_STRING= JORE_GRAPHQL_URL=https://dev.kartat.hsl.fi/jore/graphql GENERATE_API_URL=https://dev.kartat.hsl.fi/map-generator diff --git a/.env.production b/.env.production index e5ca33d2..5aa7bf62 100644 --- a/.env.production +++ b/.env.production @@ -1,4 +1,5 @@ PG_CONNECTION_STRING= +REDIS_CONNECTION_STRING= JORE_GRAPHQL_URL=https://prod.kartat.hsl.fi/jore/graphql GENERATE_API_URL=https://prod.kartat.hsl.fi/map-generator diff --git a/.env.stage b/.env.stage index 80ea70a7..2bfa00b4 100644 --- a/.env.stage +++ b/.env.stage @@ -1,4 +1,5 @@ PG_CONNECTION_STRING= +REDIS_CONNECTION_STRING= JORE_GRAPHQL_URL=https://stage.kartat.hsl.fi/jore/graphql GENERATE_API_URL=https://stage.kartat.hsl.fi/map-generator diff --git a/Dockerfile b/Dockerfile index 4d64f3bb..9a3ab6b6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -42,5 +42,6 @@ CMD \ fc-cache -f -v && \ yarn run start:production && \ yarn run server:production && \ + yarn run worker:production && \ sleep 3 && \ node_modules/.bin/forever -f logs 1 diff --git a/README.md b/README.md index 6b39c178..0faffea2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# HSL Map Publisher +# HSL Map Publisher (Julistegeneraattori) This project is the server-side component of the poster publisher (the UI can be found in [HSLdevcom/hsl-map-publisher-ui](https://github.com/HSLdevcom/hsl-map-publisher-ui)) that generates the stop posters that you see on public transport stops around the Helsinki area. It uses PostgreSQL as the database where the poster data and generation status is stored, React for rendering the posters, Puppeteer for generating PDF's of the poster app and Koa as the server to tie everything together. @@ -38,11 +38,11 @@ It will make two layout passes to check if the map can be rendered. The ads (gra Each component will add itself to the "render queue" when mounted, an operation of which there are multiple examples in the code. Once the component has finished its own data fetching and layout procedures it removes itself from the render queue. Once the render queue is empty, the poster is deemed finished and the server will instruct Puppeteer to create a PDF of the page. The component can also pass an error when removing itself which triggers the whole poster to fail. -### Running the apps +### Running the app Server and REST API for printing components to PDF files and managing their metadata in a Postgres database. -Start Postgres: +#### 1. Start Postgres ``` docker run -p 0.0.0.0:5432:5432 --env POSTGRES_PASSWORD=postgres --name publisher-postgres postgres @@ -56,24 +56,41 @@ PG_CONNECTION_STRING=postgres://postgres:postgres@localhost:5432/postgres Again, adjust the port if you are running your Publisher Postgres instance on an other port. -In development, start the Publisher server like this, prepending the Postgres connection string: +#### 2. Redis +Start Redis +``` +docker run --name redis --rm -p 6379:6379 -d redis +``` + +For default configuration, place the following to `.env`: +``` +REDIS_CONNECTION_STRING=redis://localhost:6379 +``` + +#### 3. Backend, worker and poster UI + +In development, start the Publisher backend server like this, prepending the Postgres connection string: +(or place the connection string to `.env`) ```bash -PG_CONNECTION_STRING=postgres://postgres:postgres@localhost:5432/postgres npm run server:hot +PG_CONNECTION_STRING=postgres://postgres:postgres@localhost:5432/postgres yarn run server:hot ``` That command will run a Forever instance that watches for changes and restarts the server when they happen. Alternatively, to run the server with plain Node, leave off `hot`: - ```bash -PG_CONNECTION_STRING=postgres://postgres:postgres@localhost:5432/postgres npm run server +PG_CONNECTION_STRING=postgres://postgres:postgres@localhost:5432/postgres yarn run server ``` -The React app needs to be started separately: +Then, start generator worker. (You can start multiple workers.) +``` +PG_CONNECTION_STRING=postgres://postgres:postgres@localhost:5432/postgres yarn worker +``` +Finally, start the React app ```bash -npm start +yarn start ``` Now you can use the UI with the server, or open a poster separately in your browser. The poster app needs `component` and `props` query parameters, and the server will echo the currently rendering URL in its console. But if you just need to open the poster app, you can use this link that will show H0454, Snellmaninkatu: @@ -84,6 +101,10 @@ You will have to create a template using the Publisher UI. The poster app will d If Azure credentials are not set in the .env file the posters will be stored locally. +#### 4. Start frontend + +See [hsl-map-publisher-ui](https://github.com/HSLdevcom/hsl-map-publisher-ui) for UI. + ### Running in Docker As before, make sure you are running a database for the publisher: diff --git a/constants.js b/constants.js index d018e4e6..b9bfe452 100644 --- a/constants.js +++ b/constants.js @@ -32,6 +32,7 @@ const secretsEnv = mapValues(process.env, (value, key) => { module.exports = { PG_CONNECTION_STRING: secretsEnv.PG_CONNECTION_STRING || '', + REDIS_CONNECTION_STRING: secretsEnv.REDIS_CONNECTION_STRING || '', JORE_GRAPHQL_URL: secretsEnv.JORE_GRAPHQL_URL || '', GENERATE_API_URL: secretsEnv.GENERATE_API_URL || '', AZURE_UPLOAD_CONTAINER: secretsEnv.AZURE_UPLOAD_CONTAINER || 'publisher-prod', diff --git a/package.json b/package.json index 99b287e0..8f62bd7b 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,8 @@ "server": "node -r dotenv/config scripts/server", "server:production": "forever start -c \"node --max_old_space_size=8192 -r dotenv/config\" scripts/server.js", "server:hot": "forever -w -t --killSignal=SIGTERM --watchDirectory scripts -c \"node --max_old_space_size=8192 -r dotenv/config\" scripts/server.js", + "worker": "node -r dotenv/config scripts/worker", + "worker:production": "forever start -c \"node -r dotenv/config\" scripts/worker.js", "knex": "PG_CONNECTION_STRING=postgres://postgres:postgres@localhost:5432/postgres knex", "docker:build": "docker build -t hsl-map-publisher .", "docker:run": "docker run -d -p 4000:4000 --name publisher -v $(pwd)/output:/output -v $(pwd)/fonts:/fonts --link publisher-postgres -e \"PG_CONNECTION_STRING=postgres://postgres:postgres@publisher-postgres:5432/postgres\" --shm-size=1G hsl-map-publisher", @@ -89,6 +91,7 @@ "babel-plugin-transform-runtime": "^6.23.0", "babel-polyfill": "^6.26.0", "babel-preset-stage-0": "^6.24.1", + "bullmq": "^1.57.0", "cheerio": "^1.0.0-rc.2", "classnames": "^2.2.5", "clean-webpack-plugin": "^0.1.19", @@ -101,6 +104,7 @@ "hsl-map-style": "HSLdevcom/hsl-map-style#master", "html-webpack-plugin": "^4.0.0-alpha", "iconv-lite": "^0.4.19", + "ioredis": "^4.28.2", "knex": "^0.14.6", "koa": "^2.5.3", "koa-json-body": "^5.3.0", diff --git a/scripts/fileHandler.js b/scripts/fileHandler.js new file mode 100644 index 00000000..db443642 --- /dev/null +++ b/scripts/fileHandler.js @@ -0,0 +1,82 @@ +const fs = require('fs-extra'); +const path = require('path'); +const { spawn } = require('child_process'); + +const { AZURE_STORAGE_ACCOUNT, AZURE_STORAGE_KEY } = require('../constants'); + +const cwd = process.cwd(); + +const pdfOutputDir = path.join(cwd, 'output'); +const concatOutputDir = path.join(pdfOutputDir, 'concatenated'); + +fs.ensureDirSync(concatOutputDir); + +const pdfPath = id => path.join(pdfOutputDir, `${id}.pdf`); + +/** + * Concatenates posters to a multi-page PDF + * @param {string[]} ids - Ids to concatate + * @returns {Readable} - PDF stream + * @param ids + * @param title + */ +async function concatenate(ids, title) { + const concatFolderExists = await fs.pathExists(concatOutputDir); + if (!concatFolderExists) { + fs.ensureDirSync(concatOutputDir); + } + + const filenames = ids.map(id => pdfPath(id)); + const parsedTitle = title.replace('/', ''); + const filepath = path.join(concatOutputDir, `${parsedTitle}.pdf`); + const fileExists = await fs.pathExists(filepath); + + if (!fileExists) { + return new Promise((resolve, reject) => { + const pdftk = spawn('pdftk', [...filenames, 'cat', 'output', filepath]); + + pdftk.on('error', err => { + reject(err); + }); + + pdftk.on('close', code => { + if (code === 0) { + resolve(filepath); + } else { + reject(new Error(`PDFTK closed with code ${code}`)); + } + }); + }); + } + + return filepath; +} + +async function removeFiles(ids) { + if (!AZURE_STORAGE_ACCOUNT || !AZURE_STORAGE_KEY) { + console.log('Azure credentials not set. Not removing files.'); + return; + } + const filenames = ids.map(id => pdfPath(id)); + const removePromises = []; + + filenames.forEach(filename => { + const createPromise = async () => { + try { + await fs.remove(filename); + } catch (err) { + console.log(`Pdf ${filename} removal unsuccessful.`); + console.error(err); + } + }; + + removePromises.push(createPromise()); + }); + + await Promise.all(removePromises); +} + +module.exports = { + concatenate, + removeFiles, +}; diff --git a/scripts/server.js b/scripts/server.js index e1ba8907..9be9cf64 100644 --- a/scripts/server.js +++ b/scripts/server.js @@ -6,16 +6,21 @@ const Router = require('koa-router'); const cors = require('@koa/cors'); const jsonBody = require('koa-json-body'); const fs = require('fs-extra'); +const { Queue } = require('bullmq'); +const Redis = require('ioredis'); -const generator = require('./generator'); +const fileHandler = require('./fileHandler'); const authEndpoints = require('./auth/authEndpoints'); const { matchStopDataToRules } = require('./util/rules'); -const { DOMAINS_ALLOWED_TO_GENERATE, PUBLISHER_TEST_GROUP } = require('../constants'); +const { + DOMAINS_ALLOWED_TO_GENERATE, + PUBLISHER_TEST_GROUP, + REDIS_CONNECTION_STRING, +} = require('../constants'); const { migrate, - addEvent, getBuilds, getBuild, addBuild, @@ -23,7 +28,6 @@ const { removeBuild, getPoster, addPoster, - updatePoster, removePoster, getTemplates, addTemplate, @@ -39,6 +43,12 @@ const { downloadPostersFromCloud } = require('./cloudService'); const PORT = 4000; +const bullRedisConnection = new Redis(REDIS_CONNECTION_STRING, { + maxRetriesPerRequest: null, + enableReadyCheck: false, +}); +const queue = new Queue('publisher', { connection: bullRedisConnection }); + async function generatePoster(buildId, component, props, index) { const { stopId, date, template, selectedRuleTemplates } = props; const data = await getStopInfo({ stopId, date }); @@ -68,40 +78,14 @@ async function generatePoster(buildId, component, props, index) { order: index, }); - const onInfo = (message = 'No message.') => { - console.log(`${id}: ${message}`); // eslint-disable-line no-console - addEvent({ - posterId: id, - type: 'INFO', - message, - }); - }; - const onError = error => { - console.error(`${id}: ${error.message} ${error.stack}`); // eslint-disable-line no-console - addEvent({ - posterId: id, - type: 'ERROR', - message: error.message, - }); - }; - const options = { id, component, props: renderProps, template: chosenTemplate, - onInfo, - onError, }; - generator - .generate(options) - .then(({ success, uploaded }) => { - updatePoster({ - id, - status: success && uploaded ? 'READY' : 'FAILED', - }); - }) - .catch(error => console.error(error)); // eslint-disable-line no-console + + queue.add('generate', { options }, { jobId: id }); return { id }; } @@ -261,8 +245,8 @@ async function main() { ctx.throw(404, 'Poster ids not found.'); } try { - filename = await generator.concatenate(downloadedPosterIds, `${component}-${id}`); - await generator.removeFiles(downloadedPosterIds); + filename = await fileHandler.concatenate(downloadedPosterIds, `${component}-${id}`); + await fileHandler.removeFiles(downloadedPosterIds); } catch (err) { ctx.throw(500, err.message || 'PDF concatenation failed.'); } @@ -308,11 +292,11 @@ async function main() { 'asc', ); - filename = await generator.concatenate( + filename = await fileHandler.concatenate( orderedPosters.map(poster => poster.id), parsedTitle, ); - await generator.removeFiles(downloadedPosterIds); + await fileHandler.removeFiles(downloadedPosterIds); } catch (err) { ctx.throw(500, err.message || 'PDF concatenation failed.'); } diff --git a/scripts/generator.js b/scripts/worker.js similarity index 58% rename from scripts/generator.js rename to scripts/worker.js index 84d3cab1..38174de8 100644 --- a/scripts/generator.js +++ b/scripts/worker.js @@ -1,14 +1,19 @@ +const { Worker, QueueScheduler } = require('bullmq'); +const Redis = require('ioredis'); const fs = require('fs-extra'); const path = require('path'); const puppeteer = require('puppeteer'); const qs = require('qs'); -const { promisify } = require('util'); -const { spawn } = require('child_process'); const log = require('./util/log'); -const get = require('lodash/get'); const { uploadPosterToCloud } = require('./cloudService'); -const { AZURE_STORAGE_ACCOUNT, AZURE_STORAGE_KEY } = require('../constants'); +const { addEvent, updatePoster } = require('./store'); + +const { + AZURE_STORAGE_ACCOUNT, + AZURE_STORAGE_KEY, + REDIS_CONNECTION_STRING, +} = require('../constants'); const CLIENT_URL = 'http://localhost:5000'; const RENDER_TIMEOUT = 10 * 60 * 1000; @@ -16,13 +21,9 @@ const MAX_RENDER_ATTEMPTS = 3; const SCALE = 96 / 72; let browser = null; -let previous = Promise.resolve(); const cwd = process.cwd(); const pdfOutputDir = path.join(cwd, 'output'); -const concatOutputDir = path.join(pdfOutputDir, 'concatenated'); - -fs.ensureDirSync(concatOutputDir); const pdfPath = id => path.join(pdfOutputDir, `${id}.pdf`); @@ -142,86 +143,79 @@ async function renderComponentRetry(options) { return { success: false }; } -/** - * Adds component to render queue - * @param {Object} options - * @param {string} options.id - Unique id - * @param {string} options.component - React component to render - * @param {Object} options.props - Props to pass to component - * @param {function} options.onInfo - Callback (string) - * @param {function} options.onError - Callback (Error) - * @returns {Promise} - Always resolves with { success } - */ -function generate(options) { - previous = previous.then(() => renderComponentRetry(options)); - return previous; -} - -/** - * Concatenates posters to a multi-page PDF - * @param {string[]} ids - Ids to concatate - * @returns {Readable} - PDF stream - * @param ids - * @param title - */ -async function concatenate(ids, title) { - const concatFolderExists = await fs.pathExists(concatOutputDir); - if (!concatFolderExists) { - fs.ensureDirSync(concatOutputDir); - } +async function generate(options) { + const { id } = options; - const filenames = ids.map(id => pdfPath(id)); - const parsedTitle = title.replace('/', ''); - const filepath = path.join(concatOutputDir, `${parsedTitle}.pdf`); - const fileExists = await fs.pathExists(filepath); - - if (!fileExists) { - return new Promise((resolve, reject) => { - const pdftk = spawn('pdftk', [...filenames, 'cat', 'output', filepath]); - - pdftk.on('error', err => { - reject(err); - }); - - pdftk.on('close', code => { - if (code === 0) { - resolve(filepath); - } else { - reject(new Error(`PDFTK closed with code ${code}`)); - } - }); + const onInfo = (message = 'No message.') => { + console.log(`${id}: ${message}`); // eslint-disable-line no-console + addEvent({ + posterId: id, + type: 'INFO', + message, }); - } - - return filepath; -} - -async function removeFiles(ids) { - if (!AZURE_STORAGE_ACCOUNT || !AZURE_STORAGE_KEY) { - console.log('Azure credentials not set. Not removing files.'); - return; - } - const filenames = ids.map(id => pdfPath(id)); - const removePromises = []; - - filenames.forEach(filename => { - const createPromise = async () => { - try { - await fs.remove(filename); - } catch (err) { - console.log(`Pdf ${filename} removal unsuccessful.`); - console.error(err); - } - }; + }; + const onError = error => { + console.error(`${id}: ${error.message} ${error.stack}`); // eslint-disable-line no-console + addEvent({ + posterId: id, + type: 'ERROR', + message: error.message, + }); + }; - removePromises.push(createPromise()); + const { success, uploaded } = await renderComponentRetry({ + ...options, + onInfo, + onError, }); - await Promise.all(removePromises); + updatePoster({ + id, + status: success && uploaded ? 'READY' : 'FAILED', + }); } -module.exports = { - generate, - concatenate, - removeFiles, -}; +const bullRedisConnection = new Redis(REDIS_CONNECTION_STRING, { + maxRetriesPerRequest: null, + enableReadyCheck: false, +}); + +// Queue scheduler to restart stopped jobs. +const queueScheduler = new QueueScheduler('publisher', { + connection: bullRedisConnection, +}); + +// Worker implementation +const worker = new Worker( + 'publisher', + async job => { + const { options } = job.data; + await generate(options); + }, + { + connection: bullRedisConnection, + }, +); + +console.log('Worker ready for jobs!'); + +worker.on('active', job => { + console.log(`Started to process ${job.id}`); +}); + +worker.on('completed', job => { + console.log(`${job.id} has completed!`); +}); + +worker.on('failed', (job, err) => { + console.log(`${job.id} has failed with ${err.message}`); +}); + +worker.on('drained', () => console.log('Job queue empty! Waiting for new jobs...')); + +process.on('SIGINT', () => { + console.log('Shutting down worker...'); + worker.close(true); + queueScheduler.close(); + process.exit(0); +}); diff --git a/yarn.lock b/yarn.lock index 143352de..4ce667b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2015,6 +2015,21 @@ builtin-status-codes@^3.0.0: resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= +bullmq@^1.57.0: + version "1.57.0" + resolved "https://registry.yarnpkg.com/bullmq/-/bullmq-1.57.0.tgz#7e9d0e2c63aff082597d3ca349a52e33b4357ff4" + integrity sha512-W5pjoOyjGh1C3LqSWbNmzH/LETV2mmZt1JDDVF0dw1U61BbyWz01TX1sNb+sPt58UoaZKTwhYuT5CaaNkTHrYQ== + dependencies: + cron-parser "^2.18.0" + get-port "^5.1.1" + glob "^7.2.0" + ioredis "^4.27.9" + lodash "^4.17.21" + msgpackr "^1.4.6" + semver "^6.3.0" + tslib "^1.14.1" + uuid "^8.3.2" + bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" @@ -2069,6 +2084,14 @@ cache-content-type@^1.0.0: mime-types "^2.1.18" ylru "^1.2.0" +call-bind@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + caller-callsite@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" @@ -2429,6 +2452,11 @@ clone@^1.0.2: resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= +cluster-key-slot@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d" + integrity sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw== + co-body@^5.0.0: version "5.2.0" resolved "https://registry.yarnpkg.com/co-body/-/co-body-5.2.0.tgz#5a0a658c46029131e0e3a306f67647302f71c124" @@ -2739,6 +2767,14 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: safe-buffer "^5.0.1" sha.js "^2.4.8" +cron-parser@^2.18.0: + version "2.18.0" + resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-2.18.0.tgz#de1bb0ad528c815548371993f81a54e5a089edcf" + integrity sha512-s4odpheTyydAbTBQepsqd2rNWGa2iV3cyo8g7zbI2QQYGLVsfbhmwukayS1XHppe02Oy1fg7mg6xoaraVJeEcg== + dependencies: + is-nan "^1.3.0" + moment-timezone "^0.5.31" + cross-env@^5.1.1: version "5.2.1" resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.2.1.tgz#b2c76c1ca7add66dc874d11798466094f551b34d" @@ -2983,6 +3019,13 @@ debug@^4.0.1, debug@^4.1.0: dependencies: ms "^2.1.1" +debug@^4.3.1: + version "4.3.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" + integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== + dependencies: + ms "2.1.2" + decamelize-keys@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9" @@ -3112,6 +3155,11 @@ delegates@^1.0.0: resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= +denque@^1.1.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.1.tgz#07f670e29c9a78f8faecb2566a1e2c11929c5cbf" + integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw== + depd@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" @@ -4354,6 +4402,15 @@ get-caller-file@^2.0.1: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== +get-intrinsic@^1.0.2: + version "1.1.1" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" + integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + get-node-dimensions@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/get-node-dimensions/-/get-node-dimensions-1.2.1.tgz#fb7b4bb57060fb4247dd51c9d690dfbec56b0823" @@ -4369,6 +4426,11 @@ get-port@^3.2.0: resolved "https://registry.yarnpkg.com/get-port/-/get-port-3.2.0.tgz#dd7ce7de187c06c8bf353796ac71e099f0980ebc" integrity sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw= +get-port@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193" + integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ== + get-stdin@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b" @@ -4453,6 +4515,18 @@ glob@^7.0.3, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + global-dirs@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445" @@ -4997,6 +5071,23 @@ invariant@^2.2.2: dependencies: loose-envify "^1.0.0" +ioredis@^4.27.9, ioredis@^4.28.2: + version "4.28.2" + resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.28.2.tgz#493ccd5d869fd0ec86c96498192718171f6c9203" + integrity sha512-kQ+Iv7+c6HsDdPP2XUHaMv8DhnSeAeKEwMbaoqsXYbO+03dItXt7+5jGQDRyjdRUV2rFJbzg7P4Qt1iX2tqkOg== + dependencies: + cluster-key-slot "^1.1.0" + debug "^4.3.1" + denque "^1.1.0" + lodash.defaults "^4.2.0" + lodash.flatten "^4.4.0" + lodash.isarguments "^3.1.0" + p-map "^2.1.0" + redis-commands "1.7.0" + redis-errors "^1.2.0" + redis-parser "^3.0.0" + standard-as-callback "^2.1.0" + ip-regex@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" @@ -5229,6 +5320,14 @@ is-map@^2.0.1: resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1" integrity sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw== +is-nan@^1.3.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d" + integrity sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + is-npm@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4" @@ -5920,11 +6019,26 @@ lodash.camelcase@^4.3.0: resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= +lodash.defaults@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" + integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw= + +lodash.flatten@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" + integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8= + lodash.get@~4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= +lodash.isarguments@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" + integrity sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo= + lodash.isequal@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" @@ -5945,7 +6059,7 @@ lodash@^4.15.0, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17. resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== -lodash@^4.17.4: +lodash@^4.17.21, lodash@^4.17.4: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -6332,6 +6446,18 @@ mkdirp@0.x.x, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.4, mkdirp@~0.5.1: dependencies: minimist "^1.2.5" +moment-timezone@^0.5.31: + version "0.5.34" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.34.tgz#a75938f7476b88f155d3504a9343f7519d9a405c" + integrity sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg== + dependencies: + moment ">= 2.9.0" + +"moment@>= 2.9.0": + version "2.29.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" + integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== + moment@^2.19.2: version "2.27.0" resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d" @@ -6359,11 +6485,26 @@ ms@2.0.0: resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= -ms@^2.1.1: +ms@2.1.2, ms@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +msgpackr-extract@^1.0.14: + version "1.0.16" + resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-1.0.16.tgz#701c4f6e6f25c100ae84557092274e8fffeefe45" + integrity sha512-fxdRfQUxPrL/TizyfYfMn09dK58e+d65bRD/fcaVH4052vj30QOzzqxcQIS7B0NsqlypEQ/6Du3QmP2DhWFfCA== + dependencies: + nan "^2.14.2" + node-gyp-build "^4.2.3" + +msgpackr@^1.4.6: + version "1.5.1" + resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.5.1.tgz#2a8e39d25458406034b8cb50dc7d6a7abd3dfff2" + integrity sha512-I1CXFG8BYYSeIhtDlHpUVMsdDiyvP9JAh1d9QoBnkPx3ETPeH/1lR14hweM9GETs09wCWlaOyhtXxIc9boxAAA== + optionalDependencies: + msgpackr-extract "^1.0.14" + mute-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" @@ -6388,6 +6529,11 @@ nan@^2.12.1: resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw== +nan@^2.14.2: + version "2.15.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" + integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== + nanoassert@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/nanoassert/-/nanoassert-1.1.0.tgz#4f3152e09540fde28c76f44b19bbcd1d5a42478d" @@ -6499,6 +6645,11 @@ node-fetch@^2.6.0: resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== +node-gyp-build@^4.2.3: + version "4.3.0" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" + integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q== + "node-libs-browser@^1.0.0 || ^2.0.0", node-libs-browser@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" @@ -6892,7 +7043,7 @@ p-map@^1.1.1, p-map@^1.2.0: resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b" integrity sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA== -p-map@^2.0.0: +p-map@^2.0.0, p-map@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== @@ -8056,6 +8207,23 @@ redent@^2.0.0: indent-string "^3.0.0" strip-indent "^2.0.0" +redis-commands@1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.7.0.tgz#15a6fea2d58281e27b1cd1acfb4b293e278c3a89" + integrity sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ== + +redis-errors@^1.0.0, redis-errors@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" + integrity sha1-62LSrbFeTq9GEMBK/hUpOEJQq60= + +redis-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4" + integrity sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ= + dependencies: + redis-errors "^1.0.0" + reduce-css-calc@^1.2.6: version "1.3.0" resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz#747c914e049614a4c9cfbba629871ad1d2927716" @@ -8476,6 +8644,11 @@ semver@4.3.2: resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.2.tgz#c7a07158a80bedd052355b770d82d6640f803be7" integrity sha1-x6BxWKgL7dBSNVt3DYLWZA+AO+c= +semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + send@0.16.2: version "0.16.2" resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1" @@ -8804,6 +8977,11 @@ staged-git-files@1.1.2: resolved "https://registry.yarnpkg.com/staged-git-files/-/staged-git-files-1.1.2.tgz#4326d33886dc9ecfa29a6193bf511ba90a46454b" integrity sha512-0Eyrk6uXW6tg9PYkhi/V/J4zHp33aNyi2hOCmhFLqLTIhbgqWn5jlSzI+IU0VqrZq6+DbHcabQl/WP6P3BG0QA== +standard-as-callback@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" + integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== + static-extend@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" @@ -9283,6 +9461,11 @@ tslib@^1.10.0, tslib@^1.9.0, tslib@^1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== +tslib@^1.14.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + tsscmp@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb" @@ -9572,6 +9755,11 @@ uuid@^3.1.0, uuid@^3.2.1, uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + v8-compile-cache@^2.0.0, v8-compile-cache@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745"