diff --git a/backend/package.json b/backend/package.json index 11b922ba..a3918ab8 100644 --- a/backend/package.json +++ b/backend/package.json @@ -25,6 +25,8 @@ "@nestjs/platform-express": "^10.0.0", "@nestjs/platform-socket.io": "^10.4.6", "@nestjs/websockets": "^10.4.6", + "dotenv": "^16.4.5", + "ioredis": "^5.4.1", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "socket.io": "^4.8.1" diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index f6e8a2e5..2e4f16bc 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -1,10 +1,14 @@ import { Module } from "@nestjs/common"; import { AppController } from "./app.controller"; import { AppService } from "./app.service"; -import { SocketModule } from "./socket/socket.module"; +import { SocketModule } from "./signaling-server/socket.module"; +import { RoomModule } from "./room/room.module"; +import { RedisModule } from "./redis/redis.module"; + +import "dotenv/config"; @Module({ - imports: [SocketModule], + imports: [SocketModule, RoomModule, RedisModule], controllers: [AppController], providers: [AppService], }) diff --git a/backend/src/config/redis.config.ts b/backend/src/config/redis.config.ts new file mode 100644 index 00000000..0b97e561 --- /dev/null +++ b/backend/src/config/redis.config.ts @@ -0,0 +1,7 @@ +import "dotenv/config"; + +export const redisConfig = { + host: process.env.REDIS_HOST, + port: parseInt(process.env.REDIS_PORT), + password: process.env.REDIS_PASSWORD, +}; diff --git a/backend/src/redis/redis.module.ts b/backend/src/redis/redis.module.ts new file mode 100644 index 00000000..77cec8eb --- /dev/null +++ b/backend/src/redis/redis.module.ts @@ -0,0 +1,7 @@ +import { Module } from "@nestjs/common"; +import { RedisService } from "./redis.service"; + +@Module({ + providers: [RedisService], +}) +export class RedisModule {} diff --git a/backend/src/redis/redis.service.spec.ts b/backend/src/redis/redis.service.spec.ts new file mode 100644 index 00000000..19faa0c1 --- /dev/null +++ b/backend/src/redis/redis.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import { RedisService } from "./redis.service"; + +describe("RedisService", () => { + let service: RedisService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [RedisService], + }).compile(); + + service = module.get(RedisService); + }); + + it("should be defined", () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/src/redis/redis.service.ts b/backend/src/redis/redis.service.ts new file mode 100644 index 00000000..d07f05b5 --- /dev/null +++ b/backend/src/redis/redis.service.ts @@ -0,0 +1,19 @@ +import { Injectable } from "@nestjs/common"; +import Redis from "ioredis"; +import { redisConfig } from "../config/redis.config"; + +@Injectable() +export class RedisService { + private readonly client: Redis; + + constructor() { + this.client = new Redis({ + host: redisConfig.host, + port: redisConfig.port, + password: redisConfig.password, + }); + } + getClient(): Redis { + return this.client; + } +} diff --git a/backend/src/room/room.gateway.spec.ts b/backend/src/room/room.gateway.spec.ts new file mode 100644 index 00000000..446b302b --- /dev/null +++ b/backend/src/room/room.gateway.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import { RoomGateway } from "./room.gateway"; + +describe("RoomGateway", () => { + let gateway: RoomGateway; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [RoomGateway], + }).compile(); + + gateway = module.get(RoomGateway); + }); + + it("should be defined", () => { + expect(gateway).toBeDefined(); + }); +}); diff --git a/backend/src/room/room.gateway.ts b/backend/src/room/room.gateway.ts new file mode 100644 index 00000000..aa7994d1 --- /dev/null +++ b/backend/src/room/room.gateway.ts @@ -0,0 +1,108 @@ +import { + WebSocketGateway, + WebSocketServer, + OnGatewayConnection, + OnGatewayDisconnect, + SubscribeMessage, + MessageBody, +} from "@nestjs/websockets"; +import { Server, Socket } from "socket.io"; +import { RoomService } from "./room.service"; + +const EVENT_NAME = { + CREATE_ROOM: "create_room", + JOIN_ROOM: "join_room", + MASTER_CHANGED: "master_changed", + FINISH_ROOM: "finish_room", + REACTION: "reaction", + + USER_EXIT: "user_exit", + ROOM_FULL: "room_full", + ALL_USERS: "all_users", +} as const; + +interface User { + id: string; + nickname: string; +} + +@WebSocketGateway({ + cors: { + origin: "*", // CORS 설정 + }, +}) +@WebSocketGateway() +export class RoomGateway implements OnGatewayConnection, OnGatewayDisconnect { + @WebSocketServer() + server: Server; + + constructor(private readonly roomService: RoomService) {} + + private users: { [key: string]: User[] } = {}; + private socketToRoom: { [key: string]: string } = {}; + private maximum = 5; + + handleConnection(client: Socket) { + console.log(`Client connected in room: ${client.id}`); + } + + handleDisconnect(client: Socket): any { + console.log(`Client disconnected in room: ${client.id}`); + const roomID = this.socketToRoom[client.id]; + if (roomID) { + const room = this.users[roomID]; + if (room) { + this.users[roomID] = room.filter( + (user) => user.id !== client.id + ); + if (this.users[roomID].length === 0) { + delete this.users[roomID]; + } else { + this.server.to(roomID).emit(EVENT_NAME.USER_EXIT, { id: client.id }); + } + } + } + } + + @SubscribeMessage(EVENT_NAME.CREATE_ROOM) + async handleCreateRoom(client: Socket, data: { title }) { + try { + const roomId = await this.roomService.createRoom( + data.title, + client.id + ); + this.server.emit(`room_created`, { roomId }); + } catch(error) { + console.error(error); + return null; + } + } + + + @SubscribeMessage(EVENT_NAME.JOIN_ROOM) + handleJoinRoom(client: Socket, data: { room: string; nickname: string }) { + if (this.users[data.room]) { + if (this.users[data.room].length === this.maximum) { + client.emit(EVENT_NAME.ROOM_FULL); + return; + } + this.users[data.room].push({ + id: client.id, + nickname: data.nickname, + }); + } else { + this.users[data.room] = [ + { id: client.id, nickname: data.nickname }, + ]; + } + + this.socketToRoom[client.id] = data.room; + client.join(data.room); + console.log(`[${data.room}]: ${client.id} enter`); + + const usersInThisRoom = this.users[data.room].filter( + (user) => user.id !== client.id + ); + client.emit(EVENT_NAME.ALL_USERS, usersInThisRoom); + } +} diff --git a/backend/src/room/room.model.ts b/backend/src/room/room.model.ts new file mode 100644 index 00000000..460fc27a --- /dev/null +++ b/backend/src/room/room.model.ts @@ -0,0 +1,6 @@ +export interface Room { + title: string; + createdAt: number; + members: string[]; + host: string; +} \ No newline at end of file diff --git a/backend/src/room/room.module.ts b/backend/src/room/room.module.ts new file mode 100644 index 00000000..2aa9a249 --- /dev/null +++ b/backend/src/room/room.module.ts @@ -0,0 +1,9 @@ +import { Module } from "@nestjs/common"; +import { RoomService } from "./room.service"; +import { RoomGateway } from "./room.gateway"; +import { RedisService } from "../redis/redis.service"; + +@Module({ + providers: [RoomService, RoomGateway, RedisService], +}) +export class RoomModule {} diff --git a/backend/src/room/room.service.spec.ts b/backend/src/room/room.service.spec.ts new file mode 100644 index 00000000..0ab7dff4 --- /dev/null +++ b/backend/src/room/room.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import { RoomService } from "./room.service"; + +describe("RoomService", () => { + let service: RoomService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [RoomService], + }).compile(); + + service = module.get(RoomService); + }); + + it("should be defined", () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/src/room/room.service.ts b/backend/src/room/room.service.ts new file mode 100644 index 00000000..b3e08401 --- /dev/null +++ b/backend/src/room/room.service.ts @@ -0,0 +1,25 @@ +import { Injectable } from "@nestjs/common"; +import { RedisService } from "../redis/redis.service"; +import { Room } from "./room.model"; +import { generateRoomId } from "../utils/generateRoomId"; +import { HOUR } from "../utils/time"; + +@Injectable() +export class RoomService { + constructor(private readonly redisService: RedisService) {} + + async createRoom(title: string, socketId: string) { + const client = this.redisService.getClient(); + const roomId = generateRoomId(); + + await client.hset(`room:${roomId}`, { + title: title, + createdAt: Date.now(), + members: [socketId], + host: socketId, + } as Room); + + await client.expire(`room:${roomId}`, 6 * HOUR); + return roomId; + } +} diff --git a/backend/src/signaling-server/socket.gateway.ts b/backend/src/signaling-server/socket.gateway.ts new file mode 100644 index 00000000..49d01d8f --- /dev/null +++ b/backend/src/signaling-server/socket.gateway.ts @@ -0,0 +1,74 @@ +import { + WebSocketGateway, + WebSocketServer, + OnGatewayConnection, + OnGatewayDisconnect, + SubscribeMessage, + MessageBody, +} from "@nestjs/websockets"; +import { Server } from "socket.io"; + +@WebSocketGateway({ + cors: { + origin: "*", // CORS 설정 + }, +}) +export class SocketGateway implements OnGatewayConnection, OnGatewayDisconnect { + @WebSocketServer() + server: Server; + + handleConnection(socket: any) { + console.log(`Client connected in signaling server: ${socket.id}`); + } + + handleDisconnect(socket: any) { + console.log(`Client disconnected signaling server: ${socket.id}`); + } + + @SubscribeMessage("offer") + handleOffer( + @MessageBody() + data: { + offerReceiveID: string; + sdp: any; + offerSendID: string; + offerSendNickname: string; + } + ) { + this.server.to(data.offerReceiveID).emit("getOffer", { + sdp: data.sdp, + offerSendID: data.offerSendID, + offerSendNickname: data.offerSendNickname, + }); + } + + @SubscribeMessage("answer") + handleAnswer( + @MessageBody() + data: { + answerReceiveID: string; + sdp: any; + answerSendID: string; + } + ) { + this.server.to(data.answerReceiveID).emit("getAnswer", { + sdp: data.sdp, + answerSendID: data.answerSendID, + }); + } + + @SubscribeMessage("candidate") + handleCandidate( + @MessageBody() + data: { + candidateReceiveID: string; + candidate: any; + candidateSendID: string; + } + ) { + this.server.to(data.candidateReceiveID).emit("getCandidate", { + candidate: data.candidate, + candidateSendID: data.candidateSendID, + }); + } +} diff --git a/backend/src/socket/socket.module.ts b/backend/src/signaling-server/socket.module.ts similarity index 100% rename from backend/src/socket/socket.module.ts rename to backend/src/signaling-server/socket.module.ts diff --git a/backend/src/socket/socket.gateway.ts b/backend/src/socket/socket.gateway.ts deleted file mode 100644 index 6e1f64d2..00000000 --- a/backend/src/socket/socket.gateway.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { - WebSocketGateway, - WebSocketServer, - OnGatewayConnection, - OnGatewayDisconnect, - SubscribeMessage, - MessageBody, -} from "@nestjs/websockets"; -import { Server } from "socket.io"; - -interface User { - id: string; - nickname: string; -} - -@WebSocketGateway({ - cors: { - origin: "*", // CORS 설정 - }, -}) -export class SocketGateway implements OnGatewayConnection, OnGatewayDisconnect { - @WebSocketServer() - server: Server; - - private users: { [key: string]: User[] } = {}; - private socketToRoom: { [key: string]: string } = {}; - private maximum = 5; - - handleConnection(socket: any) { - console.log(`Client connected: ${socket.id}`); - } - - handleDisconnect(socket: any) { - console.log(`Client disconnected: ${socket.id}`); - const roomID = this.socketToRoom[socket.id]; - if (roomID) { - const room = this.users[roomID]; - if (room) { - this.users[roomID] = room.filter( - (user) => user.id !== socket.id - ); - if (this.users[roomID].length === 0) { - delete this.users[roomID]; - } else { - this.server.to(roomID).emit("user_exit", { id: socket.id }); - } - } - } - } - - @SubscribeMessage("join_room") - handleJoinRoom(socket: any, data: { room: string; nickname: string }) { - if (this.users[data.room]) { - if (this.users[data.room].length === this.maximum) { - socket.emit("room_full"); - return; - } - this.users[data.room].push({ - id: socket.id, - nickname: data.nickname, - }); - } else { - this.users[data.room] = [ - { id: socket.id, nickname: data.nickname }, - ]; - } - - this.socketToRoom[socket.id] = data.room; - socket.join(data.room); - console.log(`[${data.room}]: ${socket.id} enter`); - - const usersInThisRoom = this.users[data.room].filter( - (user) => user.id !== socket.id - ); - socket.emit("all_users", usersInThisRoom); - } - - @SubscribeMessage("offer") - handleOffer( - @MessageBody() - data: { - offerReceiveID: string; - sdp: any; - offerSendID: string; - offerSendNickname: string; - } - ) { - this.server.to(data.offerReceiveID).emit("getOffer", { - sdp: data.sdp, - offerSendID: data.offerSendID, - offerSendNickname: data.offerSendNickname, - }); - } - - @SubscribeMessage("answer") - handleAnswer( - @MessageBody() - data: { - answerReceiveID: string; - sdp: any; - answerSendID: string; - } - ) { - this.server.to(data.answerReceiveID).emit("getAnswer", { - sdp: data.sdp, - answerSendID: data.answerSendID, - }); - } - - @SubscribeMessage("candidate") - handleCandidate( - @MessageBody() - data: { - candidateReceiveID: string; - candidate: any; - candidateSendID: string; - } - ) { - this.server.to(data.candidateReceiveID).emit("getCandidate", { - candidate: data.candidate, - candidateSendID: data.candidateSendID, - }); - } - - @SubscribeMessage("reaction") - handleReaction( - socket: any, - data: { - roomId: string; - reaction: string; - } - ) { - this.server - .to(data.roomId) - .emit("reaction", { senderId: socket.id, reaction: data.reaction }); - } -} diff --git a/backend/src/utils/generateRoomId.ts b/backend/src/utils/generateRoomId.ts new file mode 100644 index 00000000..c808594a --- /dev/null +++ b/backend/src/utils/generateRoomId.ts @@ -0,0 +1,9 @@ +import { createHash } from "node:crypto"; +import "dotenv/config"; + +let id = parseInt(process.env.SESSION_HASH_START); +export function generateRoomId() { + return createHash("sha256") + .update(id++ + process.env.SESSION_HASH) + .digest("hex"); +} \ No newline at end of file diff --git a/backend/src/utils/time.ts b/backend/src/utils/time.ts new file mode 100644 index 00000000..655f5325 --- /dev/null +++ b/backend/src/utils/time.ts @@ -0,0 +1,3 @@ +export const SEC = 1; +export const MIN = 60 * SEC; +export const HOUR = 60 * MIN; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0f89caff..1308ca81 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,12 @@ importers: '@nestjs/websockets': specifier: ^10.4.6 version: 10.4.7(@nestjs/common@10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(@nestjs/platform-socket.io@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1) + dotenv: + specifier: ^16.4.5 + version: 16.4.5 + ioredis: + specifier: ^5.4.1 + version: 5.4.1 reflect-metadata: specifier: ^0.2.0 version: 0.2.2 @@ -675,6 +681,9 @@ packages: resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} engines: {node: '>=18.18'} + '@ioredis/commands@1.2.0': + resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -1527,6 +1536,10 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + co@4.6.0: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} @@ -1710,6 +1723,10 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -1747,6 +1764,10 @@ packages: resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} engines: {node: '>=8'} + dotenv@16.4.5: + resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} + engines: {node: '>=12'} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -2252,6 +2273,10 @@ packages: resolution: {integrity: sha512-vI2w4zl/mDluHt9YEQ/543VTCwPKWiHzKtm9dM2V0NdFcqEexDAjUHzO1oA60HRNaVifGXXM1tRRNluLVHa0Kg==} engines: {node: '>=18'} + ioredis@5.4.1: + resolution: {integrity: sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==} + engines: {node: '>=12.22.0'} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -2584,6 +2609,12 @@ packages: lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + lodash.isplainobject@4.0.6: resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} @@ -3064,6 +3095,14 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} @@ -3248,6 +3287,9 @@ packages: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} @@ -4255,6 +4297,8 @@ snapshots: '@humanwhocodes/retry@0.3.1': {} + '@ioredis/commands@1.2.0': {} + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -5368,6 +5412,8 @@ snapshots: clone@1.0.4: {} + cluster-key-slot@1.1.2: {} + co@4.6.0: {} collect-v8-coverage@1.0.2: {} @@ -5530,6 +5576,8 @@ snapshots: delayed-stream@1.0.0: {} + denque@2.1.0: {} + depd@2.0.0: {} destroy@1.2.0: {} @@ -5557,6 +5605,8 @@ snapshots: dependencies: is-obj: 2.0.0 + dotenv@16.4.5: {} + eastasianwidth@0.2.0: {} ee-first@1.1.1: {} @@ -6208,6 +6258,20 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 6.2.0 + ioredis@5.4.1: + dependencies: + '@ioredis/commands': 1.2.0 + cluster-key-slot: 1.1.2 + debug: 4.3.7 + 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 + ipaddr.js@1.9.1: {} is-arrayish@0.2.1: {} @@ -6695,6 +6759,10 @@ snapshots: lodash.camelcase@4.3.0: {} + lodash.defaults@4.2.0: {} + + lodash.isarguments@3.1.0: {} + lodash.isplainobject@4.0.6: {} lodash.kebabcase@4.1.1: {} @@ -7111,6 +7179,12 @@ snapshots: dependencies: picomatch: 2.3.1 + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + reflect-metadata@0.2.2: {} repeat-string@1.6.1: {} @@ -7344,6 +7418,8 @@ snapshots: dependencies: escape-string-regexp: 2.0.0 + standard-as-callback@2.1.0: {} + statuses@2.0.1: {} streamsearch@1.1.0: {}