diff --git a/.github/workflows/e2e-api-v2.yml b/.github/workflows/e2e-api-v2.yml index b1df17691f9a14..7a1c87c99fa6ae 100644 --- a/.github/workflows/e2e-api-v2.yml +++ b/.github/workflows/e2e-api-v2.yml @@ -22,6 +22,7 @@ env: STRIPE_API_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }} STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }} STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }} + SLOTS_CACHE_TTL: ${{secret.CI_SLOTS_CACHE_TTL}} jobs: e2e: timeout-minutes: 20 diff --git a/apps/api/v2/package.json b/apps/api/v2/package.json index dff4f3de175b8f..33d311b9c2e334 100644 --- a/apps/api/v2/package.json +++ b/apps/api/v2/package.json @@ -38,7 +38,7 @@ "@axiomhq/winston": "^1.2.0", "@calcom/platform-constants": "*", "@calcom/platform-enums": "*", - "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.283", + "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.285", "@calcom/platform-types": "*", "@calcom/platform-utils": "*", "@calcom/prisma": "*", diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts index ee3f33464c30e6..604f7281661750 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts @@ -104,10 +104,11 @@ export class OutputBookingsService_2024_08_13 { const rescheduledToInfo = databaseBooking.rescheduled ? await this.getRescheduledToInfo(databaseBooking.uid) : undefined; - - const rescheduledToUid = rescheduledToInfo?.uid; - const rescheduledByEmail = databaseBooking.rescheduled ? rescheduledToInfo?.rescheduledBy : databaseBooking.rescheduledBy; + const rescheduledToUid = rescheduledToInfo?.uid; + const rescheduledByEmail = databaseBooking.rescheduled + ? rescheduledToInfo?.rescheduledBy + : databaseBooking.rescheduledBy; const booking = { id: databaseBooking.id, @@ -157,13 +158,12 @@ export class OutputBookingsService_2024_08_13 { async getRescheduledToInfo(bookingUid: string): Promise<{ uid?: string; rescheduledBy?: string | null }> { const rescheduledTo = await this.bookingsRepository.getByFromReschedule(bookingUid); - return { - uid: rescheduledTo?.uid, - rescheduledBy: rescheduledTo?.rescheduledBy + return { + uid: rescheduledTo?.uid, + rescheduledBy: rescheduledTo?.rescheduledBy, }; } - getUserDefinedMetadata(databaseMetadata: DatabaseMetadata) { if (databaseMetadata === null) return {}; @@ -279,9 +279,11 @@ export class OutputBookingsService_2024_08_13 { const rescheduledToInfo = databaseBooking.rescheduled ? await this.getRescheduledToInfo(databaseBooking.uid) : undefined; - + const rescheduledToUid = rescheduledToInfo?.uid; - const rescheduledByEmail = databaseBooking.rescheduled ? rescheduledToInfo?.rescheduledBy : databaseBooking.rescheduledBy; + const rescheduledByEmail = databaseBooking.rescheduled + ? rescheduledToInfo?.rescheduledBy + : databaseBooking.rescheduledBy; const booking = { id: databaseBooking.id, diff --git a/apps/api/v2/src/lib/modules/available-slots.module.ts b/apps/api/v2/src/lib/modules/available-slots.module.ts index 33c6b42797ddf7..d2cc48376d1eaa 100644 --- a/apps/api/v2/src/lib/modules/available-slots.module.ts +++ b/apps/api/v2/src/lib/modules/available-slots.module.ts @@ -11,6 +11,7 @@ import { AvailableSlotsService } from "@/lib/services/available-slots.service"; import { CacheService } from "@/lib/services/cache.service"; import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service"; import { PrismaModule } from "@/modules/prisma/prisma.module"; +import { RedisService } from "@/modules/redis/redis.service"; import { Module } from "@nestjs/common"; import { UserAvailabilityService } from "@/lib/services/user-availability.service"; @@ -25,6 +26,7 @@ import { UserAvailabilityService } from "@/lib/services/user-availability.servic PrismaEventTypeRepository, PrismaRoutingFormResponseRepository, PrismaTeamRepository, + RedisService, PrismaFeaturesRepository, CheckBookingLimitsService, CacheService, diff --git a/apps/api/v2/src/lib/services/available-slots.service.ts b/apps/api/v2/src/lib/services/available-slots.service.ts index 27582906f2966a..47ed0c4d4b32e5 100644 --- a/apps/api/v2/src/lib/services/available-slots.service.ts +++ b/apps/api/v2/src/lib/services/available-slots.service.ts @@ -9,6 +9,7 @@ import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository" import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository"; import { CacheService } from "@/lib/services/cache.service"; import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service"; +import { RedisService } from "@/modules/redis/redis.service"; import { Injectable } from "@nestjs/common"; import { AvailableSlotsService as BaseAvailableSlotsService } from "@calcom/platform-libraries/slots"; @@ -25,6 +26,7 @@ export class AvailableSlotsService extends BaseAvailableSlotsService { selectedSlotRepository: PrismaSelectedSlotRepository, eventTypeRepository: PrismaEventTypeRepository, userRepository: PrismaUserRepository, + redisService: RedisService, featuresRepository: PrismaFeaturesRepository ) { super({ @@ -36,7 +38,8 @@ export class AvailableSlotsService extends BaseAvailableSlotsService { selectedSlotRepo: selectedSlotRepository, eventTypeRepo: eventTypeRepository, userRepo: userRepository, - checkBookingLimitsService: new CheckBookingLimitsService(bookingRepository) as any, + redisClient: redisService, + checkBookingLimitsService: new CheckBookingLimitsService(bookingRepository), cacheService: new CacheService(featuresRepository), userAvailabilityService: new UserAvailabilityService(oooRepoDependency, bookingRepository, eventTypeRepository) }); diff --git a/apps/api/v2/src/modules/redis/redis.service.ts b/apps/api/v2/src/modules/redis/redis.service.ts index d683c50a359c81..952ae149184418 100644 --- a/apps/api/v2/src/modules/redis/redis.service.ts +++ b/apps/api/v2/src/modules/redis/redis.service.ts @@ -7,12 +7,32 @@ import { Redis } from "ioredis"; export class RedisService implements OnModuleDestroy { public redis: Redis; private readonly logger = new Logger("RedisService"); + private isReady = false; // Track connection status constructor(readonly configService: ConfigService) { const dbUrl = configService.get("db.redisUrl", { infer: true }); if (!dbUrl) throw new Error("Misconfigured Redis, halting."); this.redis = new Redis(dbUrl); + + this.redis.on("error", (err) => { + this.logger.error(`IoRedis connection error: ${err.message}`); + this.isReady = false; + }); + + this.redis.on("connect", () => { + this.logger.log("IoRedis connected!"); + this.isReady = true; + }); + + this.redis.on("reconnecting", (delay: string) => { + this.logger.warn(`IoRedis reconnecting... next retry in ${delay}ms`); + }); + + this.redis.on("end", () => { + this.logger.warn("IoRedis connection ended."); + this.isReady = false; + }); } async onModuleDestroy() { @@ -22,4 +42,99 @@ export class RedisService implements OnModuleDestroy { this.logger.error(err); } } + + async get(key: string): Promise { + let data = null; + if (!this.isReady) { + return null; + } + + try { + data = await this.redis.get(key); + } catch (err) { + if (err instanceof Error) this.logger.error(`IoRedis get failed: ${err.message}`); + } + + if (data === null) { + return null; + } + + try { + return JSON.parse(data) as TData; + } catch (e) { + return data as TData; + } + } + + async del(key: string): Promise { + if (!this.isReady) { + return 0; + } + try { + return this.redis.del(key); + } catch (err) { + if (err instanceof Error) this.logger.error(`IoRedis del failed: ${err.message}`); + return 0; + } + } + + async set(key: string, value: TData, opts?: { ttl?: number }): Promise<"OK" | TData | null> { + if (!this.isReady) { + return null; + } + + try { + const stringifiedValue = typeof value === "object" ? JSON.stringify(value) : String(value); + if (opts?.ttl) { + await this.redis.set(key, stringifiedValue, "PX", opts.ttl); + } else { + await this.redis.set(key, stringifiedValue); + } + } catch (err) { + if (err instanceof Error) this.logger.error(`IoRedis set failed: ${err.message}`); + return null; + } + + return "OK"; + } + + async expire(key: string, seconds: number): Promise<0 | 1> { + if (!this.isReady) { + return 0; + } + try { + return this.redis.expire(key, seconds) as Promise<0 | 1>; + } catch (err) { + if (err instanceof Error) this.logger.error(`IoRedis expire failed: ${err.message}`); + return 0; + } + } + + async lrange(key: string, start: number, end: number): Promise { + if (!this.isReady) { + return []; + } + try { + const results = await this.redis.lrange(key, start, end); + return results.map((item) => JSON.parse(item) as TResult); + } catch (err) { + if (err instanceof Error) this.logger.error(`IoRedis lrange failed: ${err.message}`); + return []; + } + } + + async lpush(key: string, ...elements: TData[]): Promise { + if (!this.isReady) { + return 0; + } + try { + const stringifiedElements = elements.map((element) => + typeof element === "object" ? JSON.stringify(element) : String(element) + ); + return this.redis.lpush(key, ...stringifiedElements); + } catch (err) { + if (err instanceof Error) this.logger.error(`IoRedis lpush failed: ${err.message}`); + return 0; + } + } } diff --git a/apps/api/v2/test/setEnvVars.ts b/apps/api/v2/test/setEnvVars.ts index 432517d04cd839..aac391b80cea34 100644 --- a/apps/api/v2/test/setEnvVars.ts +++ b/apps/api/v2/test/setEnvVars.ts @@ -36,4 +36,5 @@ process.env = { CALENDSO_ENCRYPTION_KEY: "22gfxhWUlcKliUeXcu8xNah2+HP/29ZX", INTEGRATION_TEST_MODE: "true", e2e: "true", + SLOTS_CACHE_TTL: "1" }; diff --git a/apps/web/package.json b/apps/web/package.json index 3ef4aabb8d4121..5ebf8198cd3d88 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -78,7 +78,7 @@ "@team-plain/typescript-sdk": "^5.9.0", "@types/turndown": "^5.0.1", "@unkey/ratelimit": "^0.1.1", - "@upstash/redis": "^1.21.0", + "@upstash/redis": "^1.35.2", "@vercel/edge-config": "^0.1.1", "@vercel/edge-functions-ui": "^0.2.1", "@vercel/og": "^0.6.3", diff --git a/packages/features/redis/IRedisService.d.ts b/packages/features/redis/IRedisService.d.ts index ebb4f22a23e93b..bfc97c2cba612e 100644 --- a/packages/features/redis/IRedisService.d.ts +++ b/packages/features/redis/IRedisService.d.ts @@ -1,7 +1,7 @@ export interface IRedisService { get: (key: string) => Promise; - set: (key: string, value: TData) => Promise<"OK" | TData | null>; + set: (key: string, value: TData, opts?: { ttl?: number }) => Promise<"OK" | TData | null>; expire: (key: string, seconds: number) => Promise<0 | 1>; diff --git a/packages/features/redis/NoopRedisService.ts b/packages/features/redis/NoopRedisService.ts new file mode 100644 index 00000000000000..e53e7875364f4e --- /dev/null +++ b/packages/features/redis/NoopRedisService.ts @@ -0,0 +1,32 @@ +import type { IRedisService } from "./IRedisService"; + +/** + * Noop implementation of IRedisService for testing or fallback scenarios. + */ + +export class NoopRedisService implements IRedisService { + async get(_key: string): Promise { + return null; + } + + async del(_key: string): Promise { + return 0; + } + + async set(_key: string, _value: TData, _opts?: { ttl?: number }): Promise<"OK" | TData | null> { + return "OK"; + } + + async expire(_key: string, _seconds: number): Promise<0 | 1> { + // Implementation for setting expiration time for key in Redis + return 0; + } + + async lrange(_key: string, _start: number, _end: number): Promise { + return []; + } + + async lpush(_key: string, ..._elements: TData[]): Promise { + return 0; + } +} diff --git a/packages/features/redis/RedisService.ts b/packages/features/redis/RedisService.ts index 6c7f8ab1ec0c01..d07261050b9a8c 100644 --- a/packages/features/redis/RedisService.ts +++ b/packages/features/redis/RedisService.ts @@ -6,7 +6,13 @@ export class RedisService implements IRedisService { private redis: Redis; constructor() { - this.redis = Redis.fromEnv(); + // Ensure we throw an Error to mimick old behavior + if (!process.env.UPSTASH_REDIS_REST_URL || !process.env.UPSTASH_REDIS_REST_TOKEN) { + throw new Error("Attempted to initialize Upstash Redis client without url or token."); + } + this.redis = Redis.fromEnv({ + signal: () => AbortSignal.timeout(2000), + }); } async get(key: string): Promise { @@ -17,9 +23,16 @@ export class RedisService implements IRedisService { return this.redis.del(key); } - async set(key: string, value: TData): Promise<"OK" | TData | null> { - // Implementation for setting value in Redis - return this.redis.set(key, value); + async set(key: string, value: TData, opts?: { ttl?: number }): Promise<"OK" | TData | null> { + return this.redis.set( + key, + value, + opts?.ttl + ? { + px: opts.ttl, + } + : undefined + ); } async expire(key: string, seconds: number): Promise<0 | 1> { diff --git a/packages/features/redis/di/redisModule.ts b/packages/features/redis/di/redisModule.ts new file mode 100644 index 00000000000000..32545b0c192428 --- /dev/null +++ b/packages/features/redis/di/redisModule.ts @@ -0,0 +1,17 @@ +import { createModule } from "@evyweb/ioctopus"; + +import { DI_TOKENS } from "@calcom/lib/di/tokens"; + +import { NoopRedisService } from "../NoopRedisService"; +import { RedisService } from "../RedisService"; + +const redisModule = createModule(); + +redisModule.bind(DI_TOKENS.REDIS_CLIENT).toFactory(() => { + if (process.env.UPSTASH_REDIS_REST_URL && process.env.UPSTASH_REDIS_REST_TOKEN) { + return new RedisService(); + } + return new NoopRedisService(); +}, "singleton"); + +export { redisModule }; diff --git a/packages/lib/di/containers/available-slots.ts b/packages/lib/di/containers/available-slots.ts index 30dd7bab646d90..6dc67cb8748414 100644 --- a/packages/lib/di/containers/available-slots.ts +++ b/packages/lib/di/containers/available-slots.ts @@ -1,5 +1,6 @@ import { createContainer } from "@evyweb/ioctopus"; +import { redisModule } from "@calcom/features/redis/di/redisModule"; import { DI_TOKENS } from "@calcom/lib/di/tokens"; import { prismaModule } from "@calcom/prisma/prisma.module"; import type { AvailableSlotsService } from "@calcom/trpc/server/routers/viewer/slots/util"; @@ -19,6 +20,7 @@ import { userRepositoryModule } from "../modules/user"; import { getUserAvailabilityModule } from "../modules/get-user-availability"; const container = createContainer(); +container.load(DI_TOKENS.REDIS_CLIENT, redisModule); container.load(DI_TOKENS.PRISMA_MODULE, prismaModule); container.load(DI_TOKENS.OOO_REPOSITORY_MODULE, oooRepositoryModule); container.load(DI_TOKENS.SCHEDULE_REPOSITORY_MODULE, scheduleRepositoryModule); diff --git a/packages/lib/di/modules/available-slots.ts b/packages/lib/di/modules/available-slots.ts index 15bd79a17b671b..3c4a42a1cadcfb 100644 --- a/packages/lib/di/modules/available-slots.ts +++ b/packages/lib/di/modules/available-slots.ts @@ -15,6 +15,7 @@ availableSlotsModule.bind(DI_TOKENS.AVAILABLE_SLOTS_SERVICE).toClass(AvailableSl bookingRepo: DI_TOKENS.BOOKING_REPOSITORY, eventTypeRepo: DI_TOKENS.EVENT_TYPE_REPOSITORY, routingFormResponseRepo: DI_TOKENS.ROUTING_FORM_RESPONSE_REPOSITORY, + redisClient: DI_TOKENS.REDIS_CLIENT, cacheService: DI_TOKENS.CACHE_SERVICE, checkBookingLimitsService: DI_TOKENS.CHECK_BOOKING_LIMITS_SERVICE, userAvailabilityService: DI_TOKENS.GET_USER_AVAILABILITY_SERVICE diff --git a/packages/lib/di/tokens.ts b/packages/lib/di/tokens.ts index d124b162199b74..373aa3a4c7619c 100644 --- a/packages/lib/di/tokens.ts +++ b/packages/lib/di/tokens.ts @@ -2,6 +2,7 @@ export const DI_TOKENS = { PRISMA_CLIENT: Symbol("PrismaClient"), READ_ONLY_PRISMA_CLIENT: Symbol("ReadOnlyPrismaClient"), PRISMA_MODULE: Symbol("PrismaModule"), + REDIS_CLIENT: Symbol("RedisClient"), OOO_REPOSITORY: Symbol("OOORepository"), OOO_REPOSITORY_MODULE: Symbol("OOORepositoryModule"), SCHEDULE_REPOSITORY: Symbol("ScheduleRepository"), diff --git a/packages/lib/getUserAvailability.ts b/packages/lib/getUserAvailability.ts index 107fcd70ad6997..39a181d8c19354 100644 --- a/packages/lib/getUserAvailability.ts +++ b/packages/lib/getUserAvailability.ts @@ -154,10 +154,9 @@ type GetUsersAvailabilityProps = { }; export interface IUserAvailabilityService { - eventTypeRepo: EventTypeRepository; + eventTypeRepo: EventTypeRepository; oooRepo: PrismaOOORepository; bookingRepo: BookingRepository; - } export class UserAvailabilityService { @@ -611,4 +610,4 @@ export class UserAvailabilityService { } getUsersAvailability = withReporting(this._getUsersAvailability.bind(this), "getUsersAvailability"); -} +} \ No newline at end of file diff --git a/packages/trpc/server/routers/viewer/slots/util.ts b/packages/trpc/server/routers/viewer/slots/util.ts index a1a997d6d37867..93c8107d06e846 100644 --- a/packages/trpc/server/routers/viewer/slots/util.ts +++ b/packages/trpc/server/routers/viewer/slots/util.ts @@ -8,6 +8,7 @@ import { orgDomainConfig } from "@calcom/ee/organizations/lib/orgDomains"; import { checkForConflicts } from "@calcom/features/bookings/lib/conflictChecker/checkForConflicts"; import { isEventTypeLoggingEnabled } from "@calcom/features/bookings/lib/isEventTypeLoggingEnabled"; import type { CacheService } from "@calcom/features/calendar-cache/lib/getShouldServeCache"; +import type { IRedisService } from "@calcom/features/redis/IRedisService"; import { findQualifiedHostsWithDelegationCredentials } from "@calcom/lib/bookings/findQualifiedHostsWithDelegationCredentials"; import { shouldIgnoreContactOwner } from "@calcom/lib/bookings/routing/utils"; import { RESERVED_SUBDOMAINS } from "@calcom/lib/constants"; @@ -64,6 +65,7 @@ import { handleNotificationWhenNoSlots } from "./handleNotificationWhenNoSlots"; import type { GetScheduleOptions } from "./types"; const log = logger.getSubLogger({ prefix: ["[slots/util]"] }); +const DEFAULT_SLOTS_CACHE_TTL = 2000; type GetAvailabilityUserWithDelegationCredentials = Omit & { credentials: CredentialForCalendarService[]; @@ -101,7 +103,47 @@ export interface IAvailableSlotsService { routingFormResponseRepo: RoutingFormResponseRepository; cacheService: CacheService; checkBookingLimitsService: CheckBookingLimitsService; - userAvailabilityService: UserAvailabilityService + userAvailabilityService: UserAvailabilityService; + redisClient: IRedisService; +} + +function withSlotsCache( + redisClient: IRedisService, + func: (args: GetScheduleOptions) => Promise +) { + return async (args: GetScheduleOptions): Promise => { + const cacheKey = `${JSON.stringify(args.input)}`; + let success = false; + let cachedResult: IGetAvailableSlots | null = null; + const startTime = process.hrtime(); + try { + cachedResult = await redisClient.get(cacheKey); + success = true; + } catch (err: unknown) { + if (err instanceof Error && err.name === "TimeoutError") { + const endTime = process.hrtime(startTime); + log.error(`Redis request timed out after ${endTime[0]}${endTime[1] / 1e6}ms`); + } else { + throw err; + } + } + + if (!success) { + // If the cache request fails, we proceed to call the function directly + return await func(args); + } + if (cachedResult) { + log.info("[CACHE HIT] Available slots", { cacheKey }); + return cachedResult; + } + const result = await func(args); + const ttl = parseInt(process.env.SLOTS_CACHE_TTL ?? "", 10) || DEFAULT_SLOTS_CACHE_TTL; + // we do not wait for the cache to complete setting; we fire and forget, and hope it'll finish. + // this is to already start responding to the client. + redisClient.set(cacheKey, result, { ttl }); + log.info("[CACHE MISS] Available slots", { cacheKey, ttl }); + return result; + }; } export class AvailableSlotsService { @@ -884,7 +926,10 @@ export class AvailableSlotsService { "getRegularOrDynamicEventType" ); - getAvailableSlots = withReporting(this._getAvailableSlots.bind(this), "getAvailableSlots"); + getAvailableSlots = withReporting( + withSlotsCache(this.dependencies.redisClient, this._getAvailableSlots.bind(this)), + "getAvailableSlots" + ); async _getAvailableSlots({ input, ctx }: GetScheduleOptions): Promise { const { diff --git a/turbo.json b/turbo.json index 9dc1c9b24873e5..1ace3cc7b84245 100644 --- a/turbo.json +++ b/turbo.json @@ -490,6 +490,7 @@ "NEXT_PUBLIC_SINGLE_ORG_MODE_ENABLED", "CALCOM_SERVICE_ACCOUNT_ENCRYPTION_KEY", "CAL_VIDEO_RECORDING_TOKEN_SECRET", - "ORGANIZER_EMAIL_EXEMPT_DOMAINS" + "ORGANIZER_EMAIL_EXEMPT_DOMAINS", + "SLOTS_CACHE_TTL" ] } diff --git a/yarn.lock b/yarn.lock index 62bd2d9cc761dd..d33aaf467458ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2500,7 +2500,7 @@ __metadata: "@axiomhq/winston": ^1.2.0 "@calcom/platform-constants": "*" "@calcom/platform-enums": "*" - "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.283" + "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.285" "@calcom/platform-types": "*" "@calcom/platform-utils": "*" "@calcom/prisma": "*" @@ -3556,13 +3556,13 @@ __metadata: languageName: unknown linkType: soft -"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.283": - version: 0.0.283 - resolution: "@calcom/platform-libraries@npm:0.0.283" +"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.285": + version: 0.0.285 + resolution: "@calcom/platform-libraries@npm:0.0.285" dependencies: "@calcom/features": "*" "@calcom/lib": "*" - checksum: 7d741bd13ac050f8552855658b2348cba0999529c12ca6741e5f802a7bc9ce4e29c9ac69a17306c42beb392f17acb7b5f8a0554b8d2ea9b5fc6d963950d0fca8 + checksum: 2faebf08e2565a1eb75e70a1bf68af3a7593b8a2c35f4d05af97fdfba7fda6fba962984934a47c3a7fbf0a1a5b4f1ca4dd89029d710b6684b0f6fafa96526e11 languageName: node linkType: hard @@ -4076,7 +4076,7 @@ __metadata: "@types/turndown": ^5.0.1 "@types/uuid": 8.3.1 "@unkey/ratelimit": ^0.1.1 - "@upstash/redis": ^1.21.0 + "@upstash/redis": ^1.35.2 "@vercel/edge-config": ^0.1.1 "@vercel/edge-functions-ui": ^0.2.1 "@vercel/og": ^0.6.3 @@ -18784,12 +18784,12 @@ __metadata: languageName: node linkType: hard -"@upstash/redis@npm:^1.21.0": - version: 1.30.1 - resolution: "@upstash/redis@npm:1.30.1" +"@upstash/redis@npm:^1.35.2": + version: 1.35.2 + resolution: "@upstash/redis@npm:1.35.2" dependencies: - crypto-js: ^4.2.0 - checksum: 1d8083650fbc1760fe4c58d263c62f9c658d08035480927316932df2757f4e987799f7ce752579e7dd871155e96996bb4caf8ad1034af3ab18965d7ca76421cf + uncrypto: ^0.1.3 + checksum: 1d54217589418873702f08bdc27d9455ea44df0fce30ace612b5637a1c4c261b762a80fb85d7b3c72d3213e82b8938bb57c413ae44f9f1e713eeab3f31d5539a languageName: node linkType: hard @@ -23319,13 +23319,6 @@ __metadata: languageName: node linkType: hard -"crypto-js@npm:^4.2.0": - version: 4.2.0 - resolution: "crypto-js@npm:4.2.0" - checksum: f051666dbc077c8324777f44fbd3aaea2986f198fe85092535130d17026c7c2ccf2d23ee5b29b36f7a4a07312db2fae23c9094b644cc35f7858b1b4fcaf27774 - languageName: node - linkType: hard - "crypto-random-string@npm:^2.0.0": version: 2.0.0 resolution: "crypto-random-string@npm:2.0.0" @@ -45439,6 +45432,13 @@ __metadata: languageName: node linkType: hard +"uncrypto@npm:^0.1.3": + version: 0.1.3 + resolution: "uncrypto@npm:0.1.3" + checksum: 07160e08806dd6cea16bb96c3fd54cd70fc801e02fc3c6f86980144d15c9ebbd1c55587f7280a207b3af6cd34901c0d0b77ada5a02c2f7081a033a05acf409e2 + languageName: node + linkType: hard + "undici-types@npm:~6.19.2": version: 6.19.8 resolution: "undici-types@npm:6.19.8"