// SPDX-License-Identifier: AGPL-3.0-or-later import { DELETED_USER_DISCRIMINATOR, DELETED_USER_GLOBAL_NAME, DELETED_USER_USERNAME, } from '@fluxer/constants/src/UserConstants'; import type {UserPartialResponse} from '@fluxer/schema/src/domains/user/UserResponseSchemas'; import type {UserID} from '../BrandedTypes'; import {Logger} from '../Logger'; import type {RequestCache} from '../middleware/RequestCacheMiddleware'; import type {User} from '../models/User'; import {mapUserToPartialResponse} from '../user/UserMappers'; import type {IUsersServiceClient} from './UsersServiceClient'; type UserPartialResponseTimingRecorder = (name: string, operation: () => Promise) => Promise; interface UserPartialResponseResolutionOptions { timeStep?: UserPartialResponseTimingRecorder; } export class UserCacheService { constructor(private usersServiceClient: IUsersServiceClient) {} async getUserPartialResponse(userId: UserID, requestCache: RequestCache): Promise { const cached = requestCache.userPartials.get(userId); if (cached) { return cached; } const partials = await this.getUserPartialResponses([userId], requestCache); const userPartialResponse = partials.get(userId) ?? this.createDeletedUserPartialFallback(userId); requestCache.userPartials.set(userId, userPartialResponse); return userPartialResponse; } async invalidateUserCache(userId: UserID): Promise { await this.usersServiceClient.invalidateUserCache(userId).catch((error) => { Logger.warn({userId: userId.toString(), error}, 'Failed to invalidate users service cache'); }); } async getUserPartialResponses( userIds: Array, requestCache: RequestCache, options: UserPartialResponseResolutionOptions = {}, ): Promise> { const timeStep: UserPartialResponseTimingRecorder = options.timeStep ?? ((_name, operation) => operation()); const results = new Map(); const missingFromRequestCache: Array = []; const seenMissing = new Set(); for (const userId of userIds) { const cached = requestCache.userPartials.get(userId); if (cached) { results.set(userId, cached); continue; } if (!seenMissing.has(userId)) { seenMissing.add(userId); missingFromRequestCache.push(userId); } } if (missingFromRequestCache.length === 0) { return results; } const servicePartials = await timeStep('users_service_request', async () => this.usersServiceClient.getUserPartialResponses(missingFromRequestCache), ); for (const [userId, partial] of servicePartials) { results.set(userId, partial); requestCache.userPartials.set(userId, partial); } for (const userId of missingFromRequestCache) { if (results.has(userId)) { continue; } const deleted = this.createDeletedUserPartialFallback(userId); results.set(userId, deleted); requestCache.userPartials.set(userId, deleted); } return results; } async setUserPartialResponseFromUser(user: User, requestCache?: RequestCache): Promise { const response = mapUserToPartialResponse(user); requestCache?.userPartials.set(user.id, response); await this.invalidateUserCache(user.id); return response; } setUserPartialResponseFromUserInBackground(user: User, requestCache?: RequestCache): UserPartialResponse { const response = mapUserToPartialResponse(user); requestCache?.userPartials.set(user.id, response); void this.invalidateUserCache(user.id); return response; } private createDeletedUserPartialFallback(userId: UserID): UserPartialResponse { return { id: userId.toString(), username: DELETED_USER_USERNAME, discriminator: DELETED_USER_DISCRIMINATOR.toString().padStart(4, '0'), global_name: DELETED_USER_GLOBAL_NAME, avatar: null, avatar_color: null, flags: 0, }; } }