Source

utils/user-profile.utils.js

/**
 * Utilities for fetching and caching user profile information
 */
import { getLogger } from "@utils/asyncLocalStorage";
import { redisUtils } from "../database/redis.client";
/**
 * Fetches a user profile using a multi-layered caching strategy.
 *
 * The function attempts to retrieve the profile in the following order:
 * 1. **Memoization cache** (if provided) to prevent duplicate fetches in-flight.
 * 2. **Redis cache** for fast retrieval.
 * 3. **gRPC Auth service** if cache misses occur.
 * 4. Falls back to null fields if gRPC fails (best-effort approach).
 *
 * If a `profileCache` map is provided, this function will store both in-flight promises
 * and resolved profiles to handle concurrent requests safely.
 * @category Utils
 * @param {string} userId - The ID of the user whose profile is being fetched.
 * @param {Map<string, IUserProfile | Promise<IUserProfile>>} profileCache - Optional memoization cache to prevent duplicate fetches.
 * @param {AuthGrpcService} grpcClient - The gRPC client used to fetch the user if cache misses.
 * @returns {Promise<IUserProfile>} - The resolved user profile with `name`, `avatar`, and `email` fields (nullable if unavailable).
 */
export async function fetchUserProfile(userId, profileCache, grpcClient) {
    const logger = getLogger();
    // Step 1: Check memoization cache if provided
    if (profileCache?.has(userId)) {
        const cached = profileCache.get(userId);
        // If it's a Promise (race condition handling), await it
        if (cached instanceof Promise) {
            logger.info({ userId }, "[SOURCE: Memoization] Awaiting in-flight fetch promise");
            const result = await cached;
            logger.info({ userId, profile: result }, "[SOURCE: Memoization] Got result from awaited promise");
            return result;
        }
        // Otherwise, it's the actual profile data
        logger.info({ userId, profile: cached }, "[SOURCE: Memoization] Cache hit - returning cached profile");
        return cached;
    }
    // Create a promise for this fetch operation to handle race conditions
    const fetchPromise = (async () => {
        // Step 2: Check Redis cache
        const redisKey = `user:${userId}`;
        try {
            const cached = await redisUtils.get(redisKey);
            if (cached) {
                const userData = JSON.parse(cached);
                const profile = {
                    name: userData.name ?? null,
                    avatar: userData.avatar ?? null,
                    email: userData.email ?? null,
                };
                logger.info({ userId, profile }, "[SOURCE: Redis] Profile fetched from Redis cache");
                // Store actual value in memoization cache if provided
                if (profileCache) {
                    profileCache.set(userId, profile);
                }
                return profile;
            }
        }
        catch (error) {
            logger.error({ error, userId }, "Failed to read user from Redis");
        }
        // Step 3: If Redis miss, try gRPC
        try {
            const user = (await grpcClient.getUserById(userId));
            const profile = {
                name: user?.name ?? null,
                avatar: user?.avatar ?? null,
                email: user?.email ?? null,
            };
            logger.info({ userId, profile }, "[SOURCE: gRPC] Profile fetched from gRPC service");
            // Store actual value in memoization cache if provided
            if (profileCache) {
                profileCache.set(userId, profile);
            }
            return profile;
        }
        catch (error) {
            logger.error({ error, userId }, "Failed to fetch user from gRPC");
            // If gRPC fails, fallback to null values (user profile enrichment is best-effort)
            const profile = { name: null, avatar: null, email: null };
            logger.info({ userId, profile }, "[SOURCE: Fallback] gRPC failed - returning null profile");
            if (profileCache) {
                profileCache.set(userId, profile);
            }
            return profile;
        }
    })();
    // Store the promise in cache immediately to handle parallel requests
    if (profileCache) {
        profileCache.set(userId, fetchPromise);
    }
    return await fetchPromise;
}