diff --git a/app/api/courrier/[id]/route.ts b/app/api/courrier/[id]/route.ts index 7b1980b2..aa638ec2 100644 --- a/app/api/courrier/[id]/route.ts +++ b/app/api/courrier/[id]/route.ts @@ -10,6 +10,7 @@ import { NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/app/api/auth/[...nextauth]/route'; import { getEmailContent, markEmailReadStatus } from '@/lib/services/email-service'; +import { getCachedEmailContent, invalidateEmailContentCache } from '@/lib/redis'; export async function GET( request: Request, @@ -36,10 +37,19 @@ export async function GET( const folder = searchParams.get("folder") || "INBOX"; try { + // Try to get email from Redis cache first + const cachedEmail = await getCachedEmailContent(session.user.id, id); + if (cachedEmail) { + console.log(`Using cached email content for ${session.user.id}:${id}`); + return NextResponse.json(cachedEmail); + } + + console.log(`Cache miss for email content ${session.user.id}:${id}, fetching from IMAP`); + // Use the email service to fetch the email content const email = await getEmailContent(session.user.id, id, folder); - // Return the complete email object instead of just partial data + // Return the complete email object return NextResponse.json(email); } catch (error: any) { console.error("Error fetching email content:", error); @@ -106,6 +116,9 @@ export async function POST( ); } + // Invalidate cache for this email + await invalidateEmailContentCache(session.user.id, id); + return NextResponse.json({ success: true }); } catch (error: any) { console.error("Error in POST:", error); diff --git a/app/api/courrier/login/route.ts b/app/api/courrier/login/route.ts index 3a7df791..b7e2ce98 100644 --- a/app/api/courrier/login/route.ts +++ b/app/api/courrier/login/route.ts @@ -6,6 +6,7 @@ import { getUserEmailCredentials, testEmailConnection } from '@/lib/services/email-service'; +import { cacheEmailCredentials, invalidateUserEmailCache } from '@/lib/redis'; export async function POST(request: Request) { try { @@ -43,8 +44,11 @@ export async function POST(request: Request) { { status: 401 } ); } + + // Invalidate all cached data for this user as they are changing their credentials + await invalidateUserEmailCache(session.user.id); - // Save credentials in the database + // Save credentials in the database and Redis await saveUserEmailCredentials(session.user.id, { email, password, diff --git a/app/api/courrier/route.ts b/app/api/courrier/route.ts index c8050b60..efe91621 100644 --- a/app/api/courrier/route.ts +++ b/app/api/courrier/route.ts @@ -2,6 +2,11 @@ import { NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/app/api/auth/[...nextauth]/route'; import { getEmails } from '@/lib/services/email-service'; +import { + getCachedEmailList, + cacheEmailList, + invalidateFolderCache +} from '@/lib/redis'; // Simple in-memory cache (will be removed in a future update) interface EmailCacheEntry { @@ -31,17 +36,16 @@ export async function GET(request: Request) { const folder = searchParams.get("folder") || "INBOX"; const searchQuery = searchParams.get("search") || ""; - // Check cache - temporary until we implement a proper server-side cache - const cacheKey = `${session.user.id}:${folder}:${page}:${perPage}:${searchQuery}`; - const now = Date.now(); - const cachedEmails = emailListCache[cacheKey]; - - if (cachedEmails && now - cachedEmails.timestamp < CACHE_TTL) { - console.log(`Using cached emails for ${cacheKey}`); - return NextResponse.json(cachedEmails.data); + // Try to get from Redis cache first, but only if it's not a search query + if (!searchQuery) { + const cachedEmails = await getCachedEmailList(session.user.id, folder, page, perPage); + if (cachedEmails) { + console.log(`Using Redis cached emails for ${session.user.id}:${folder}:${page}:${perPage}`); + return NextResponse.json(cachedEmails); + } } - console.log(`Cache miss for ${cacheKey}, fetching emails`); + console.log(`Redis cache miss for ${session.user.id}:${folder}:${page}:${perPage}, fetching emails from IMAP`); // Use the email service to fetch emails const emailsResult = await getEmails( @@ -52,12 +56,7 @@ export async function GET(request: Request) { searchQuery ); - // Cache the results - emailListCache[cacheKey] = { - data: emailsResult, - timestamp: now - }; - + // The result is already cached in the getEmails function return NextResponse.json(emailsResult); } catch (error: any) { console.error("Error fetching emails:", error); @@ -81,19 +80,16 @@ export async function POST(request: Request) { return NextResponse.json({ error: 'Missing emailId parameter' }, { status: 400 }); } - // Invalidate cache entries for this folder or all folders if none specified - const userId = session.user.id; - Object.keys(emailListCache).forEach(key => { - if (folderName) { - if (key.includes(`${userId}:${folderName}`)) { - delete emailListCache[key]; - } - } else { - if (key.startsWith(`${userId}:`)) { - delete emailListCache[key]; - } + // Invalidate Redis cache for the folder + if (folderName) { + await invalidateFolderCache(session.user.id, folderName); + } else { + // If no folder specified, invalidate all folders (using a wildcard pattern) + const folders = ['INBOX', 'Sent', 'Drafts', 'Trash', 'Junk']; + for (const folder of folders) { + await invalidateFolderCache(session.user.id, folder); } - }); + } return NextResponse.json({ success: true }); } catch (error) { diff --git a/docker-compose.yml b/docker-compose.yml index 0280c8fe..bd527c92 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,16 @@ services: volumes: - db:/var/lib/postgresql/data + redis: + image: redis:latest + command: redis-server --requirepass mySecretPassword + container_name: redis + ports: + - "6379:6379" + volumes: + - redis_data:/data + volumes: db: - driver: local \ No newline at end of file + driver: local + redis_data: \ No newline at end of file diff --git a/lib/redis.ts b/lib/redis.ts new file mode 100644 index 00000000..1e3cf923 --- /dev/null +++ b/lib/redis.ts @@ -0,0 +1,302 @@ +import Redis from 'ioredis'; +import CryptoJS from 'crypto-js'; + +// Initialize Redis client +let redisClient: Redis | null = null; + +/** + * Get a Redis client instance (singleton pattern) + */ +export function getRedisClient(): Redis { + if (!redisClient) { + const redisUrl = process.env.REDIS_URL || 'redis://:mySecretPassword@localhost:6379'; + + redisClient = new Redis(redisUrl, { + retryStrategy: (times) => { + const delay = Math.min(times * 50, 2000); + return delay; + }, + enableOfflineQueue: true, + maxRetriesPerRequest: 3 + }); + + redisClient.on('error', (err) => { + console.error('Redis connection error:', err); + }); + + redisClient.on('connect', () => { + console.log('Successfully connected to Redis'); + }); + } + + return redisClient; +} + +/** + * Close Redis connection (useful for serverless environments) + */ +export async function closeRedisConnection(): Promise { + if (redisClient) { + await redisClient.quit(); + redisClient = null; + } +} + +// Encryption key from environment variable or fallback +const getEncryptionKey = () => { + return process.env.REDIS_ENCRYPTION_KEY || 'default-encryption-key-change-in-production'; +}; + +/** + * Encrypt sensitive data before storing in Redis + */ +export function encryptData(data: string): string { + return CryptoJS.AES.encrypt(data, getEncryptionKey()).toString(); +} + +/** + * Decrypt sensitive data retrieved from Redis + */ +export function decryptData(encryptedData: string): string { + const bytes = CryptoJS.AES.decrypt(encryptedData, getEncryptionKey()); + return bytes.toString(CryptoJS.enc.Utf8); +} + +// Cache key definitions +export const KEYS = { + CREDENTIALS: (userId: string) => `email:credentials:${userId}`, + SESSION: (userId: string) => `email:session:${userId}`, + EMAIL_LIST: (userId: string, folder: string, page: number, perPage: number) => + `email:list:${userId}:${folder}:${page}:${perPage}`, + EMAIL_CONTENT: (userId: string, emailId: string) => + `email:content:${userId}:${emailId}` +}; + +// TTL constants in seconds +export const TTL = { + CREDENTIALS: 60 * 60 * 24, // 24 hours + SESSION: 60 * 30, // 30 minutes + EMAIL_LIST: 60 * 5, // 5 minutes + EMAIL_CONTENT: 60 * 15 // 15 minutes +}; + +interface EmailCredentials { + email: string; + password?: string; + host: string; + port: number; + secure?: boolean; + encryptedPassword?: string; +} + +interface ImapSessionData { + connectionId?: string; + lastActive: number; + mailboxes?: string[]; +} + +/** + * Cache email credentials in Redis + */ +export async function cacheEmailCredentials( + userId: string, + credentials: EmailCredentials +): Promise { + const redis = getRedisClient(); + const key = KEYS.CREDENTIALS(userId); + + // Create a copy without the password to store + const secureCredentials: EmailCredentials = { + email: credentials.email, + host: credentials.host, + port: credentials.port, + secure: credentials.secure ?? true + }; + + // Encrypt password separately if it exists + if (credentials.password) { + secureCredentials.encryptedPassword = encryptData(credentials.password); + } + + await redis.set(key, JSON.stringify(secureCredentials), 'EX', TTL.CREDENTIALS); +} + +/** + * Get cached email credentials from Redis + */ +export async function getCachedEmailCredentials( + userId: string +): Promise { + const redis = getRedisClient(); + const key = KEYS.CREDENTIALS(userId); + + const cachedData = await redis.get(key); + if (!cachedData) return null; + + const credentials = JSON.parse(cachedData) as EmailCredentials; + + // Decrypt password if it was encrypted + if (credentials.encryptedPassword) { + credentials.password = decryptData(credentials.encryptedPassword); + delete credentials.encryptedPassword; + } + + return credentials; +} + +/** + * Cache IMAP session data for quick reconnection + */ +export async function cacheImapSession( + userId: string, + sessionData: ImapSessionData +): Promise { + const redis = getRedisClient(); + const key = KEYS.SESSION(userId); + + // Always update the lastActive timestamp + sessionData.lastActive = Date.now(); + + await redis.set(key, JSON.stringify(sessionData), 'EX', TTL.SESSION); +} + +/** + * Get cached IMAP session data + */ +export async function getCachedImapSession( + userId: string +): Promise { + const redis = getRedisClient(); + const key = KEYS.SESSION(userId); + + const cachedData = await redis.get(key); + if (!cachedData) return null; + + return JSON.parse(cachedData) as ImapSessionData; +} + +/** + * Cache email list in Redis + */ +export async function cacheEmailList( + userId: string, + folder: string, + page: number, + perPage: number, + data: any +): Promise { + const redis = getRedisClient(); + const key = KEYS.EMAIL_LIST(userId, folder, page, perPage); + + await redis.set(key, JSON.stringify(data), 'EX', TTL.EMAIL_LIST); +} + +/** + * Get cached email list from Redis + */ +export async function getCachedEmailList( + userId: string, + folder: string, + page: number, + perPage: number +): Promise { + const redis = getRedisClient(); + const key = KEYS.EMAIL_LIST(userId, folder, page, perPage); + + const cachedData = await redis.get(key); + if (!cachedData) return null; + + return JSON.parse(cachedData); +} + +/** + * Cache email content in Redis + */ +export async function cacheEmailContent( + userId: string, + emailId: string, + data: any +): Promise { + const redis = getRedisClient(); + const key = KEYS.EMAIL_CONTENT(userId, emailId); + + await redis.set(key, JSON.stringify(data), 'EX', TTL.EMAIL_CONTENT); +} + +/** + * Get cached email content from Redis + */ +export async function getCachedEmailContent( + userId: string, + emailId: string +): Promise { + const redis = getRedisClient(); + const key = KEYS.EMAIL_CONTENT(userId, emailId); + + const cachedData = await redis.get(key); + if (!cachedData) return null; + + return JSON.parse(cachedData); +} + +/** + * Invalidate all email caches for a folder + */ +export async function invalidateFolderCache( + userId: string, + folder: string +): Promise { + const redis = getRedisClient(); + const pattern = `email:list:${userId}:${folder}:*`; + + // Use SCAN to find and delete keys matching the pattern + let cursor = '0'; + do { + const [nextCursor, keys] = await redis.scan(cursor, 'MATCH', pattern, 'COUNT', 100); + cursor = nextCursor; + + if (keys.length > 0) { + await redis.del(...keys); + } + } while (cursor !== '0'); +} + +/** + * Invalidate email content cache + */ +export async function invalidateEmailContentCache( + userId: string, + emailId: string +): Promise { + const redis = getRedisClient(); + const key = KEYS.EMAIL_CONTENT(userId, emailId); + + await redis.del(key); +} + +/** + * Invalidate all user email caches (email lists and content) + */ +export async function invalidateUserEmailCache( + userId: string +): Promise { + const redis = getRedisClient(); + + // Patterns to delete + const patterns = [ + `email:list:${userId}:*`, + `email:content:${userId}:*` + ]; + + for (const pattern of patterns) { + let cursor = '0'; + do { + const [nextCursor, keys] = await redis.scan(cursor, 'MATCH', pattern, 'COUNT', 100); + cursor = nextCursor; + + if (keys.length > 0) { + await redis.del(...keys); + } + } while (cursor !== '0'); + } +} \ No newline at end of file diff --git a/lib/services/email-service.ts b/lib/services/email-service.ts index 561e6a58..f6c66cc5 100644 --- a/lib/services/email-service.ts +++ b/lib/services/email-service.ts @@ -5,6 +5,18 @@ import { ImapFlow } from 'imapflow'; import nodemailer from 'nodemailer'; import { prisma } from '@/lib/prisma'; import { simpleParser } from 'mailparser'; +import { + cacheEmailCredentials, + getCachedEmailCredentials, + cacheEmailList, + getCachedEmailList, + cacheEmailContent, + getCachedEmailContent, + cacheImapSession, + getCachedImapSession, + invalidateFolderCache, + invalidateEmailContentCache +} from '@/lib/redis'; // Types for the email service export interface EmailCredentials { @@ -88,20 +100,40 @@ setInterval(() => { * Get IMAP connection for a user, reusing existing connections when possible */ export async function getImapConnection(userId: string): Promise { - // Get credentials from database - const credentials = await getUserEmailCredentials(userId); + // First try to get credentials from Redis cache + let credentials = await getCachedEmailCredentials(userId); + + // If not in cache, get from database and cache them if (!credentials) { - throw new Error('No email credentials found'); + credentials = await getUserEmailCredentials(userId); + if (!credentials) { + throw new Error('No email credentials found'); + } + + // Cache credentials for future use + await cacheEmailCredentials(userId, credentials); } const connectionKey = `${userId}:${credentials.email}`; const existingConnection = connectionPool[connectionKey]; + // Try to get session data from Redis + const sessionData = await getCachedImapSession(userId); + // Return existing connection if available and connected if (existingConnection) { try { if (existingConnection.client.usable) { existingConnection.lastUsed = Date.now(); + + // Update session data in Redis + if (sessionData) { + await cacheImapSession(userId, { + ...sessionData, + lastActive: Date.now() + }); + } + return existingConnection.client; } } catch (error) { @@ -169,6 +201,7 @@ export async function saveUserEmailCredentials( userId: string, credentials: EmailCredentials ): Promise { + // Save to database await prisma.mailCredentials.upsert({ where: { userId }, update: { @@ -185,6 +218,9 @@ export async function saveUserEmailCredentials( port: credentials.port } }); + + // Also cache in Redis + await cacheEmailCredentials(userId, credentials); } // Helper type for IMAP fetch options @@ -207,6 +243,17 @@ export async function getEmails( perPage: number = 20, searchQuery: string = '' ): Promise { + // Try to get from cache first + if (!searchQuery) { + const cachedResult = await getCachedEmailList(userId, folder, page, perPage); + if (cachedResult) { + console.log(`Using cached email list for ${userId}:${folder}:${page}:${perPage}`); + return cachedResult; + } + } + + console.log(`Cache miss for emails ${userId}:${folder}:${page}:${perPage}, fetching from IMAP`); + const client = await getImapConnection(userId); try { @@ -223,7 +270,14 @@ export async function getEmails( // Empty result if no messages if (totalMessages === 0 || from > to) { const mailboxes = await getMailboxes(client); - return { + + // Cache mailbox list in session data + await cacheImapSession(userId, { + lastActive: Date.now(), + mailboxes + }); + + const result = { emails: [], totalEmails: 0, page, @@ -232,6 +286,13 @@ export async function getEmails( folder, mailboxes }; + + // Cache even empty results + if (!searchQuery) { + await cacheEmailList(userId, folder, page, perPage, result); + } + + return result; } // Search if needed @@ -362,7 +423,13 @@ export async function getEmails( const mailboxes = await getMailboxes(client); - return { + // Cache mailbox list in session data + await cacheImapSession(userId, { + lastActive: Date.now(), + mailboxes + }); + + const result = { emails, totalEmails: totalMessages, page, @@ -371,6 +438,13 @@ export async function getEmails( folder, mailboxes }; + + // Cache the result if it's not a search query + if (!searchQuery) { + await cacheEmailList(userId, folder, page, perPage, result); + } + + return result; } finally { // Don't logout, keep connection in pool if (folder !== 'INBOX') { @@ -391,6 +465,15 @@ export async function getEmailContent( emailId: string, folder: string = 'INBOX' ): Promise { + // Try to get from cache first + const cachedEmail = await getCachedEmailContent(userId, emailId); + if (cachedEmail) { + console.log(`Using cached email content for ${userId}:${emailId}`); + return cachedEmail; + } + + console.log(`Cache miss for email content ${userId}:${emailId}, fetching from IMAP`); + const client = await getImapConnection(userId); try { @@ -420,7 +503,7 @@ export async function getEmailContent( // Preserve the raw HTML exactly as it was in the original email const rawHtml = parsedEmail.html || ''; - return { + const email = { id: emailId, messageId: envelope.messageId, subject: envelope.subject || "(No Subject)", @@ -462,6 +545,11 @@ export async function getEmailContent( folder, contentFetched: true }; + + // Cache the email content + await cacheEmailContent(userId, emailId, email); + + return email; } finally { try { await client.mailboxClose(); @@ -491,6 +579,12 @@ export async function markEmailReadStatus( await client.messageFlagsRemove(emailId, ['\\Seen']); } + // Invalidate content cache since the flags changed + await invalidateEmailContentCache(userId, emailId); + + // Also invalidate folder cache because unread counts may have changed + await invalidateFolderCache(userId, folder); + return true; } catch (error) { console.error(`Error marking email ${emailId} as ${isRead ? 'read' : 'unread'}:`, error); diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json index 96cab6b8..ed4288b4 100644 --- a/node_modules/.package-lock.json +++ b/node_modules/.package-lock.json @@ -351,6 +351,12 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", + "license": "MIT" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -2331,6 +2337,13 @@ "tslib": "^2.8.0" } }, + "node_modules/@types/crypto-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz", + "integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/d3-array": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", @@ -2950,6 +2963,15 @@ "node": ">=6" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/cmdk": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.4.tgz", @@ -3084,6 +3106,12 @@ "node": "*" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -3310,6 +3338,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/detect-libc": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", @@ -3408,6 +3445,18 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -4119,6 +4168,30 @@ "node": ">=12" } }, + "node_modules/ioredis": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.1.tgz", + "integrity": "sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "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" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/ip-address": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", @@ -4490,6 +4563,12 @@ "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", "license": "MIT" }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, "node_modules/lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", @@ -4497,6 +4576,12 @@ "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", "license": "MIT" }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, "node_modules/lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", @@ -5938,6 +6023,27 @@ "decimal.js-light": "^2.4.1" } }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", @@ -6277,6 +6383,12 @@ "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "license": "BSD-3-Clause" }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, "node_modules/stream-browserify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", diff --git a/package-lock.json b/package-lock.json index d6154382..cce2bdba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,12 +53,15 @@ "clsx": "^2.1.1", "cmdk": "1.0.4", "cookies-next": "^5.1.0", + "crypto-js": "^4.2.0", "date-fns": "^3.6.0", + "dotenv": "^16.5.0", "embla-carousel-react": "8.5.1", "fullcalendar": "^6.1.15", "imap": "^0.8.19", "imapflow": "^1.0.184", "input-otp": "1.4.1", + "ioredis": "^5.6.1", "isomorphic-dompurify": "^2.24.0", "jwt-decode": "^4.0.0", "libmime": "^5.3.6", @@ -90,6 +93,7 @@ "webdav": "^5.8.0" }, "devDependencies": { + "@types/crypto-js": "^4.2.2", "@types/imapflow": "^1.0.20", "@types/jsdom": "^21.1.7", "@types/mime": "^3.0.4", @@ -1210,6 +1214,12 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", + "license": "MIT" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -3302,6 +3312,13 @@ "tslib": "^2.8.0" } }, + "node_modules/@types/crypto-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz", + "integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/d3-array": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", @@ -3921,6 +3938,15 @@ "node": ">=6" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/cmdk": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.4.tgz", @@ -4055,6 +4081,12 @@ "node": "*" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -4281,6 +4313,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/detect-libc": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", @@ -4379,6 +4420,18 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -5090,6 +5143,30 @@ "node": ">=12" } }, + "node_modules/ioredis": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.1.tgz", + "integrity": "sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "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" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/ip-address": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", @@ -5461,6 +5538,12 @@ "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", "license": "MIT" }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, "node_modules/lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", @@ -5468,6 +5551,12 @@ "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", "license": "MIT" }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, "node_modules/lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", @@ -6909,6 +6998,27 @@ "decimal.js-light": "^2.4.1" } }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", @@ -7248,6 +7358,12 @@ "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "license": "BSD-3-Clause" }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, "node_modules/stream-browserify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", diff --git a/package.json b/package.json index 47048ffd..19462b04 100644 --- a/package.json +++ b/package.json @@ -54,12 +54,15 @@ "clsx": "^2.1.1", "cmdk": "1.0.4", "cookies-next": "^5.1.0", + "crypto-js": "^4.2.0", "date-fns": "^3.6.0", + "dotenv": "^16.5.0", "embla-carousel-react": "8.5.1", "fullcalendar": "^6.1.15", "imap": "^0.8.19", "imapflow": "^1.0.184", "input-otp": "1.4.1", + "ioredis": "^5.6.1", "isomorphic-dompurify": "^2.24.0", "jwt-decode": "^4.0.0", "libmime": "^5.3.6", @@ -91,6 +94,7 @@ "webdav": "^5.8.0" }, "devDependencies": { + "@types/crypto-js": "^4.2.2", "@types/imapflow": "^1.0.20", "@types/jsdom": "^21.1.7", "@types/mime": "^3.0.4", diff --git a/scripts/test-redis.js b/scripts/test-redis.js new file mode 100644 index 00000000..9d008ed5 --- /dev/null +++ b/scripts/test-redis.js @@ -0,0 +1,68 @@ +#!/usr/bin/env node + +const Redis = require('ioredis'); +const dotenv = require('dotenv'); + +// Load environment variables +dotenv.config({ path: '.env.local' }); + +const redisUrl = process.env.REDIS_URL || 'redis://:mySecretPassword@localhost:6379'; + +// Connect to Redis +const redis = new Redis(redisUrl, { + retryStrategy: (times) => { + const delay = Math.min(times * 50, 2000); + return delay; + } +}); + +// Test functions +async function testRedisConnection() { + try { + // Test basic connection + console.log('Testing Redis connection...'); + await redis.ping(); + console.log('✅ Redis connection successful!'); + + // Test setting a key + console.log('\nTesting setting a key...'); + await redis.set('test-key', 'Hello from Redis test script'); + console.log('✅ Successfully set test-key'); + + // Test getting a key + console.log('\nTesting getting a key...'); + const value = await redis.get('test-key'); + console.log(`✅ Successfully retrieved test-key: "${value}"`); + + // Test expiry + console.log('\nTesting key expiration...'); + await redis.set('expiring-key', 'This will expire in 5 seconds', 'EX', 5); + console.log('✅ Set key with 5 second expiration'); + console.log('Waiting for key to expire...'); + + // Wait for expiration + await new Promise(resolve => setTimeout(resolve, 6000)); + + const expiredValue = await redis.get('expiring-key'); + if (expiredValue === null) { + console.log('✅ Key successfully expired'); + } else { + console.log('❌ Key did not expire as expected'); + } + + // Clean up + console.log('\nCleaning up...'); + await redis.del('test-key'); + console.log('✅ Removed test keys'); + + console.log('\n🎉 All Redis tests passed!'); + } catch (error) { + console.error('❌ Redis test failed:', error); + } finally { + // Close connection + redis.disconnect(); + } +} + +// Run the test +testRedisConnection(); \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 6dff5e8a..f8591b60 100644 --- a/yarn.lock +++ b/yarn.lock @@ -168,6 +168,11 @@ resolved "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.1.0.tgz" integrity sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA== +"@ioredis/commands@^1.1.1": + version "1.2.0" + resolved "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz" + integrity sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg== + "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz" @@ -1029,6 +1034,11 @@ dependencies: tslib "^2.8.0" +"@types/crypto-js@^4.2.2": + version "4.2.2" + resolved "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz" + integrity sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ== + "@types/d3-array@^3.0.3": version "3.2.1" resolved "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz" @@ -1416,6 +1426,11 @@ clsx@^2.0.0, clsx@^2.1.1: resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz" integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== +cluster-key-slot@^1.1.0: + version "1.1.2" + resolved "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz" + integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA== + cmdk@1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/cmdk/-/cmdk-1.0.4.tgz" @@ -1500,6 +1515,11 @@ crypt@0.0.2: resolved "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz" integrity sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow== +crypto-js@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz" + integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q== + cssesc@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz" @@ -1643,6 +1663,11 @@ define-data-property@^1.1.4: es-errors "^1.3.0" gopd "^1.0.1" +denque@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz" + integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== + detect-libc@^2.0.3: version "2.0.4" resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz" @@ -1708,6 +1733,11 @@ domutils@^3.0.1: domelementtype "^2.3.0" domhandler "^5.0.3" +dotenv@^16.5.0: + version "16.5.0" + resolved "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz" + integrity sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg== + dunder-proto@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz" @@ -2141,6 +2171,21 @@ input-otp@1.4.1: resolved "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz" integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== +ioredis@^5.6.1: + version "5.6.1" + resolved "https://registry.npmjs.org/ioredis/-/ioredis-5.6.1.tgz" + integrity sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA== + dependencies: + "@ioredis/commands" "^1.1.1" + cluster-key-slot "^1.1.0" + 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" + ip-address@^9.0.5: version "9.0.5" resolved "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz" @@ -2375,11 +2420,21 @@ lodash.clonedeep@^4.5.0: resolved "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz" integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ== +lodash.defaults@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz" + integrity sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ== + lodash.get@^4.4.2: version "4.4.2" resolved "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz" integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== +lodash.isarguments@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz" + integrity sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg== + lodash.isequal@^4.5.0: version "4.5.0" resolved "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz" @@ -3221,6 +3276,18 @@ recharts@2.15.0: tiny-invariant "^1.3.1" victory-vendor "^36.6.8" +redis-errors@^1.0.0, redis-errors@^1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz" + integrity sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w== + +redis-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz" + integrity sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A== + dependencies: + redis-errors "^1.0.0" + regenerator-runtime@^0.14.0: version "0.14.1" resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz" @@ -3418,6 +3485,11 @@ sprintf-js@^1.1.3: resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz" integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== +standard-as-callback@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz" + integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== + stream-browserify@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz"