/**
* 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;
}
Source