From 7f933addff985c872106a873aea1445ce3ed4835 Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Fri, 3 May 2024 02:56:03 +0100 Subject: [PATCH] Implement BullMQ for background job processing * new REDIS_URL environment variable --- .env.example | 2 +- apps/server/package.json | 2 + apps/server/src/app.module.ts | 2 + apps/server/src/helpers/utils.ts | 13 ++ .../environment/environment.service.ts | 7 + .../mail/drivers/postmark.driver.ts | 1 + .../integrations/mail/drivers/smtp.driver.ts | 1 + .../src/integrations/mail/mail.module.ts | 5 +- .../src/integrations/mail/mail.service.ts | 10 +- .../mail/processors/email.processor..ts | 39 ++++ .../src/integrations/queue/constants/index.ts | 1 + .../queue/constants/queue.constants.ts | 7 + .../src/integrations/queue/queue.module.ts | 36 ++++ apps/server/src/kysely/database.module.ts | 4 +- pnpm-lock.yaml | 190 +++++++++++++++++- 15 files changed, 314 insertions(+), 6 deletions(-) create mode 100644 apps/server/src/integrations/mail/processors/email.processor..ts create mode 100644 apps/server/src/integrations/queue/constants/index.ts create mode 100644 apps/server/src/integrations/queue/constants/queue.constants.ts create mode 100644 apps/server/src/integrations/queue/queue.module.ts diff --git a/.env.example b/.env.example index 90add6a..70790aa 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,7 @@ JWT_SECRET_KEY=ba8642edbed7f6c450e46875e8c835c7e417031abe1f7b03f3e56bb7481706d8 JWT_TOKEN_EXPIRES_IN=30d DATABASE_URL="postgresql://postgres:password@localhost:5432/dc?schema=public" +REDIS_URL=redis://@127.0.0.1:6379 # local | s3 STORAGE_DRIVER=local @@ -22,7 +23,6 @@ AWS_S3_ENDPOINT= AWS_S3_URL= AWS_S3_USE_PATH_STYLE_ENDPOINT=false - # EMAIL drivers: smtp / postmark / log MAIL_DRIVER=smtp MAIL_HOST=127.0.0.1 diff --git a/apps/server/package.json b/apps/server/package.json index ff5ca97..561e222 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -31,6 +31,7 @@ "@casl/ability": "^6.7.1", "@fastify/multipart": "^8.2.0", "@fastify/static": "^7.0.3", + "@nestjs/bullmq": "^10.1.1", "@nestjs/common": "^10.3.8", "@nestjs/config": "^3.2.2", "@nestjs/core": "^10.3.8", @@ -44,6 +45,7 @@ "@react-email/render": "^0.0.13", "@types/pg": "^8.11.5", "bcrypt": "^5.1.1", + "bullmq": "^5.7.8", "bytes": "^3.1.2", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", diff --git a/apps/server/src/app.module.ts b/apps/server/src/app.module.ts index a0bf01c..cd11e8e 100644 --- a/apps/server/src/app.module.ts +++ b/apps/server/src/app.module.ts @@ -11,6 +11,7 @@ import { DatabaseModule } from '@docmost/db/database.module'; import * as fs from 'fs'; import { StorageModule } from './integrations/storage/storage.module'; import { MailModule } from './integrations/mail/mail.module'; +import { QueueModule } from './integrations/queue/queue.module'; const clientDistPath = join(__dirname, '..', '..', 'client/dist'); @@ -32,6 +33,7 @@ function getServeStaticModule() { EnvironmentModule, CollaborationModule, WsModule, + QueueModule, ...getServeStaticModule(), StorageModule.forRootAsync({ imports: [EnvironmentModule], diff --git a/apps/server/src/helpers/utils.ts b/apps/server/src/helpers/utils.ts index 9cb2240..24c6bc0 100644 --- a/apps/server/src/helpers/utils.ts +++ b/apps/server/src/helpers/utils.ts @@ -24,3 +24,16 @@ export async function comparePasswordHash( export function getRandomInt(min = 4, max = 5) { return Math.floor(Math.random() * (max - min + 1)) + min; } + +export type RedisConfig = { + host: string; + port: number; + password?: string; +}; +export function parseRedisUrl(redisUrl: string): RedisConfig { + // format - redis[s]://[[username][:password]@][host][:port][/db-number] + const { hostname, port, password } = new URL(redisUrl); + const portInt = parseInt(port, 10); + + return { host: hostname, port: portInt, password }; +} diff --git a/apps/server/src/integrations/environment/environment.service.ts b/apps/server/src/integrations/environment/environment.service.ts index d746637..a770e56 100644 --- a/apps/server/src/integrations/environment/environment.service.ts +++ b/apps/server/src/integrations/environment/environment.service.ts @@ -102,4 +102,11 @@ export class EnvironmentService { getPostmarkToken(): string { return this.configService.get('POSTMARK_TOKEN'); } + + getRedisUrl(): string { + return this.configService.get( + 'REDIS_URL', + 'redis://@127.0.0.1:6379', + ); + } } diff --git a/apps/server/src/integrations/mail/drivers/postmark.driver.ts b/apps/server/src/integrations/mail/drivers/postmark.driver.ts index 7fec05f..ba6dd33 100644 --- a/apps/server/src/integrations/mail/drivers/postmark.driver.ts +++ b/apps/server/src/integrations/mail/drivers/postmark.driver.ts @@ -25,6 +25,7 @@ export class PostmarkDriver implements MailDriver { this.logger.debug(`Sent mail to ${message.to}`); } catch (err) { this.logger.warn(`Failed to send mail to ${message.to}: ${err}`); + throw err; } } } diff --git a/apps/server/src/integrations/mail/drivers/smtp.driver.ts b/apps/server/src/integrations/mail/drivers/smtp.driver.ts index c276b19..af8dccc 100644 --- a/apps/server/src/integrations/mail/drivers/smtp.driver.ts +++ b/apps/server/src/integrations/mail/drivers/smtp.driver.ts @@ -27,6 +27,7 @@ export class SmtpDriver implements MailDriver { this.logger.debug(`Sent mail to ${message.to}`); } catch (err) { this.logger.warn(`Failed to send mail to ${message.to}: ${err}`); + throw err; } } } diff --git a/apps/server/src/integrations/mail/mail.module.ts b/apps/server/src/integrations/mail/mail.module.ts index d6acdb5..6fb902c 100644 --- a/apps/server/src/integrations/mail/mail.module.ts +++ b/apps/server/src/integrations/mail/mail.module.ts @@ -5,9 +5,12 @@ import { } from './providers/mail.provider'; import { MailModuleOptions } from './interfaces'; import { MailService } from './mail.service'; +import { EmailProcessor } from './processors/email.processor.'; @Global() -@Module({}) +@Module({ + providers: [EmailProcessor], +}) export class MailModule { static forRootAsync(options: MailModuleOptions): DynamicModule { return { diff --git a/apps/server/src/integrations/mail/mail.service.ts b/apps/server/src/integrations/mail/mail.service.ts index 16d2712..6af6986 100644 --- a/apps/server/src/integrations/mail/mail.service.ts +++ b/apps/server/src/integrations/mail/mail.service.ts @@ -3,16 +3,24 @@ import { MAIL_DRIVER_TOKEN } from './mail.constants'; import { MailDriver } from './drivers/interfaces/mail-driver.interface'; import { MailMessage } from './interfaces/mail.message'; import { EnvironmentService } from '../environment/environment.service'; +import { InjectQueue } from '@nestjs/bullmq'; +import { QueueName, QueueJob } from '../queue/constants'; +import { Queue } from 'bullmq'; @Injectable() export class MailService { constructor( @Inject(MAIL_DRIVER_TOKEN) private mailDriver: MailDriver, private readonly environmentService: EnvironmentService, + @InjectQueue(QueueName.EMAIL_QUEUE) private emailQueue: Queue, ) {} - async sendMail(message: Omit): Promise { + async sendEmail(message: Omit): Promise { const sender = `${this.environmentService.getMailFromName()} <${this.environmentService.getMailFromAddress()}> `; await this.mailDriver.sendMail({ from: sender, ...message }); } + + async sendToQueue(message: Omit): Promise { + await this.emailQueue.add(QueueJob.SEND_EMAIL, message); + } } diff --git a/apps/server/src/integrations/mail/processors/email.processor..ts b/apps/server/src/integrations/mail/processors/email.processor..ts new file mode 100644 index 0000000..e845ff2 --- /dev/null +++ b/apps/server/src/integrations/mail/processors/email.processor..ts @@ -0,0 +1,39 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { OnWorkerEvent, Processor, WorkerHost } from '@nestjs/bullmq'; +import { QueueName } from '../../queue/constants'; +import { Job } from 'bullmq'; +import { MailService } from '../mail.service'; + +@Injectable() +@Processor(QueueName.EMAIL_QUEUE) +export class EmailProcessor extends WorkerHost { + private readonly logger = new Logger(EmailProcessor.name); + constructor(private readonly mailService: MailService) { + super(); + } + + async process(job: Job): Promise { + try { + await this.mailService.sendEmail(job.data); + } catch (err) { + throw err; + } + } + + @OnWorkerEvent('active') + onActive(job: Job) { + this.logger.debug(`Processing ${job.name} job`); + } + + @OnWorkerEvent('failed') + onError(job: Job) { + this.logger.warn( + `Error processing ${job.name} job. Reason: ${job.failedReason}`, + ); + } + + @OnWorkerEvent('completed') + onCompleted(job: Job) { + this.logger.debug(`Completed ${job.name} job`); + } +} diff --git a/apps/server/src/integrations/queue/constants/index.ts b/apps/server/src/integrations/queue/constants/index.ts new file mode 100644 index 0000000..d25edb5 --- /dev/null +++ b/apps/server/src/integrations/queue/constants/index.ts @@ -0,0 +1 @@ +export * from './queue.constants'; diff --git a/apps/server/src/integrations/queue/constants/queue.constants.ts b/apps/server/src/integrations/queue/constants/queue.constants.ts new file mode 100644 index 0000000..bf0c2ad --- /dev/null +++ b/apps/server/src/integrations/queue/constants/queue.constants.ts @@ -0,0 +1,7 @@ +export enum QueueName { + EMAIL_QUEUE = '{email-queue}', +} + +export enum QueueJob { + SEND_EMAIL = 'send-email', +} diff --git a/apps/server/src/integrations/queue/queue.module.ts b/apps/server/src/integrations/queue/queue.module.ts new file mode 100644 index 0000000..d09c9ec --- /dev/null +++ b/apps/server/src/integrations/queue/queue.module.ts @@ -0,0 +1,36 @@ +import { Global, Module } from '@nestjs/common'; +import { BullModule } from '@nestjs/bullmq'; +import { EnvironmentService } from '../environment/environment.service'; +import { parseRedisUrl } from '../../helpers'; +import { QueueName } from './constants'; + +@Global() +@Module({ + imports: [ + BullModule.forRootAsync({ + useFactory: (environmentService: EnvironmentService) => { + const redisConfig = parseRedisUrl(environmentService.getRedisUrl()); + return { + connection: { + host: redisConfig.host, + port: redisConfig.port, + password: redisConfig.password, + }, + defaultJobOptions: { + attempts: 3, + backoff: { + type: 'exponential', + delay: 10000, + }, + }, + }; + }, + inject: [EnvironmentService], + }), + BullModule.registerQueue({ + name: QueueName.EMAIL_QUEUE, + }), + ], + exports: [BullModule], +}) +export class QueueModule {} diff --git a/apps/server/src/kysely/database.module.ts b/apps/server/src/kysely/database.module.ts index 1b049f7..5d5e366 100644 --- a/apps/server/src/kysely/database.module.ts +++ b/apps/server/src/kysely/database.module.ts @@ -41,11 +41,11 @@ types.setTypeParser(types.builtins.INT8, (val) => Number(val)); log: (event: LogEvent) => { if (environmentService.getEnv() !== 'development') return; if (event.level === 'query') { - console.log(event.query.sql); + // console.log(event.query.sql); //if (event.query.parameters.length > 0) { //console.log('parameters: ' + event.query.parameters); //} - console.log('time: ' + event.queryDurationMillis); + // console.log('time: ' + event.queryDurationMillis); } }, }), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b9aeca7..c55074d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -277,6 +277,9 @@ importers: '@fastify/static': specifier: ^7.0.3 version: 7.0.3 + '@nestjs/bullmq': + specifier: ^10.1.1 + version: 10.1.1(@nestjs/common@10.3.8)(@nestjs/core@10.3.8)(bullmq@5.7.8) '@nestjs/common': specifier: ^10.3.8 version: 10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -316,6 +319,9 @@ importers: bcrypt: specifier: ^5.1.1 version: 5.1.1 + bullmq: + specifier: ^5.7.8 + version: 5.7.8 bytes: specifier: ^3.1.2 version: 3.1.2 @@ -3270,6 +3276,10 @@ packages: /@humanwhocodes/object-schema@2.0.3: resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + /@ioredis/commands@1.2.0: + resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} + dev: false + /@isaacs/cliui@8.0.2: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -3702,6 +3712,79 @@ packages: - supports-color dev: false + /@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.2: + resolution: {integrity: sha512-9bfjwDxIDWmmOKusUcqdS4Rw+SETlp9Dy39Xui9BEGEk19dDwH0jhipwFzEff/pFg95NKymc6TOTbRKcWeRqyQ==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.2: + resolution: {integrity: sha512-lwriRAHm1Yg4iDf23Oxm9n/t5Zpw1lVnxYU3HnJPTi2lJRkKTrps1KVgvL6m7WvmhYVt/FIsssWay+k45QHeuw==} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.2: + resolution: {integrity: sha512-FU20Bo66/f7He9Fp9sP2zaJ1Q8L9uLPZQDub/WlUip78JlPeMbVL8546HbZfcW9LNciEXc8d+tThSJjSC+tmsg==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@msgpackr-extract/msgpackr-extract-linux-arm@3.0.2: + resolution: {integrity: sha512-MOI9Dlfrpi2Cuc7i5dXdxPbFIgbDBGgKR5F2yWEa6FVEtSWncfVNKW5AKjImAQ6CZlBK9tympdsZJ2xThBiWWA==} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@msgpackr-extract/msgpackr-extract-linux-x64@3.0.2: + resolution: {integrity: sha512-gsWNDCklNy7Ajk0vBBf9jEx04RUxuDQfBse918Ww+Qb9HCPoGzS+XJTLe96iN3BVK7grnLiYghP/M4L8VsaHeA==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@msgpackr-extract/msgpackr-extract-win32-x64@3.0.2: + resolution: {integrity: sha512-O+6Gs8UeDbyFpbSh2CPEz/UOrrdWPTBYNblZK5CxxLisYt4kGX3Sc+czffFonyjiGSq3jWLwJS/CCJc7tBr4sQ==} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@nestjs/bull-shared@10.1.1(@nestjs/common@10.3.8)(@nestjs/core@10.3.8): + resolution: {integrity: sha512-su7eThDrSz1oQagEi8l+1CyZ7N6nMgmyAX0DuZoXqT1KEVEDqGX7x80RlPVF60m/8SYOskckGMjJROSfNQcErw==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + '@nestjs/core': ^8.0.0 || ^9.0.0 || ^10.0.0 + dependencies: + '@nestjs/common': 10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.3.8(@nestjs/common@10.3.8)(@nestjs/websockets@10.3.8)(reflect-metadata@0.2.2)(rxjs@7.8.1) + tslib: 2.6.2 + dev: false + + /@nestjs/bullmq@10.1.1(@nestjs/common@10.3.8)(@nestjs/core@10.3.8)(bullmq@5.7.8): + resolution: {integrity: sha512-afYx1wYCKtXEu1p0S1+qw2o7QaZWr/EQgF7Wkt3YL8RBIECy5S4C450gv/cRGd8EZjlt6bw8hGCLqR2Q5VjHpQ==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + '@nestjs/core': ^8.0.0 || ^9.0.0 || ^10.0.0 + bullmq: ^3.0.0 || ^4.0.0 || ^5.0.0 + dependencies: + '@nestjs/bull-shared': 10.1.1(@nestjs/common@10.3.8)(@nestjs/core@10.3.8) + '@nestjs/common': 10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.3.8(@nestjs/common@10.3.8)(@nestjs/websockets@10.3.8)(reflect-metadata@0.2.2)(rxjs@7.8.1) + bullmq: 5.7.8 + tslib: 2.6.2 + dev: false + /@nestjs/cli@10.3.2: resolution: {integrity: sha512-aWmD1GLluWrbuC4a1Iz/XBk5p74Uj6nIVZj6Ov03JbTfgtWqGFLtXuMetvzMiHxfrHehx/myt2iKAPRhKdZvTg==} engines: {node: '>= 16.14'} @@ -7789,6 +7872,20 @@ packages: semver: 7.6.0 dev: true + /bullmq@5.7.8: + resolution: {integrity: sha512-F/Haeu6AVHkFrfeaU/kLOjhfrH6x3CaKAZlQQ+76fa8l3kfI9oaUHeFMW+1mYVz0NtYPF7PNTWFq4ylAHYcCgA==} + dependencies: + cron-parser: 4.9.0 + ioredis: 5.4.1 + msgpackr: 1.10.1 + node-abort-controller: 3.1.1 + semver: 7.6.0 + tslib: 2.6.2 + uuid: 9.0.1 + transitivePeerDependencies: + - supports-color + dev: false + /busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} @@ -7988,6 +8085,11 @@ packages: engines: {node: '>=6'} dev: false + /cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + dev: false + /co@4.6.0: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} @@ -8175,6 +8277,13 @@ packages: resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} dev: false + /cron-parser@4.9.0: + resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} + engines: {node: '>=12.0.0'} + dependencies: + luxon: 3.4.4 + dev: false + /cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -8263,6 +8372,11 @@ packages: resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} dev: false + /denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + dev: false + /depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -9546,6 +9660,23 @@ packages: loose-envify: 1.4.0 dev: false + /ioredis@5.4.1: + resolution: {integrity: sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==} + engines: {node: '>=12.22.0'} + dependencies: + '@ioredis/commands': 1.2.0 + cluster-key-slot: 1.1.2 + debug: 4.3.4 + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + dev: false + /ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -10471,10 +10602,18 @@ packages: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} dev: true + /lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + dev: false + /lodash.includes@4.3.0: resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} dev: false + /lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + dev: false + /lodash.isboolean@3.0.3: resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} dev: false @@ -10543,6 +10682,11 @@ packages: dependencies: yallist: 4.0.0 + /luxon@3.4.4: + resolution: {integrity: sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==} + engines: {node: '>=12'} + dev: false + /magic-string@0.30.5: resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==} engines: {node: '>=12'} @@ -10750,6 +10894,28 @@ packages: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} dev: false + /msgpackr-extract@3.0.2: + resolution: {integrity: sha512-SdzXp4kD/Qf8agZ9+iTu6eql0m3kWm1A2y1hkpTeVNENutaB0BwHlSvAIaMxwntmRUAUjon2V4L8Z/njd0Ct8A==} + hasBin: true + requiresBuild: true + dependencies: + node-gyp-build-optional-packages: 5.0.7 + optionalDependencies: + '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.2 + '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.2 + '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.2 + '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.2 + '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.2 + '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.2 + dev: false + optional: true + + /msgpackr@1.10.1: + resolution: {integrity: sha512-r5VRLv9qouXuLiIBrLpl2d5ZvPt8svdQTl5/vMvE4nzDMyEX4sgW5yWhuBBj5UmgwOTWj8CIdSXn5sAfsHAWIQ==} + optionalDependencies: + msgpackr-extract: 3.0.2 + dev: false + /mute-stream@0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} dev: true @@ -10837,7 +11003,6 @@ packages: /node-abort-controller@3.1.1: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} - dev: true /node-addon-api@5.1.0: resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==} @@ -10860,6 +11025,13 @@ packages: dependencies: whatwg-url: 5.0.0 + /node-gyp-build-optional-packages@5.0.7: + resolution: {integrity: sha512-YlCCc6Wffkx0kHkmam79GKvDQ6x+QZkMjFGrIMxgFNILFvGSbCp2fCBC55pGTT9gVaz8Na5CLmxt/urtzRv36w==} + hasBin: true + requiresBuild: true + dev: false + optional: true + /node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} dev: true @@ -12208,6 +12380,18 @@ packages: dependencies: resolve: 1.22.8 + /redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + dev: false + + /redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + dependencies: + redis-errors: 1.2.0 + dev: false + /redux@4.2.1: resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==} dependencies: @@ -12689,6 +12873,10 @@ packages: type-fest: 0.7.1 dev: false + /standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + dev: false + /statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'}