const { GET_BODYSHOP_BY_ID } = require("../graphql-client/queries"); const devDebugLogger = require("./devDebugLogger"); const client = require("../graphql-client/graphql-client").client; const BODYSHOP_CACHE_TTL = 3600; // 1 hour /** * Generate a cache key for a bodyshop * @param bodyshopId * @returns {`bodyshop-cache:${string}`} */ const getBodyshopCacheKey = (bodyshopId) => `bodyshop-cache:${bodyshopId}`; /** * Generate a cache key for a user socket mapping * @param email * @returns {`user:${string}:${string}:socketMapping`} */ const getUserSocketMappingKey = (email) => `user:${process.env?.NODE_ENV === "production" ? "prod" : "dev"}:${email}:socketMapping`; /** * Fetch bodyshop data from the database * @param bodyshopId * @param logger * @returns {Promise<*>} */ const fetchBodyshopFromDB = async (bodyshopId, logger) => { try { const response = await client.request(GET_BODYSHOP_BY_ID, { id: bodyshopId }); const bodyshop = response.bodyshops_by_pk; if (!bodyshop) { throw new Error(`Bodyshop with ID ${bodyshopId} not found`); } return bodyshop; // Return the full object as-is } catch (error) { logger.log("fetch-bodyshop-from-db", "ERROR", "redis", null, { bodyshopId, error: error?.message, stack: error?.stack }); throw error; } }; /** * Apply Redis helper functions * @param pubClient * @param app * @param logger */ const applyRedisHelpers = ({ pubClient, app, logger }) => { // Store session data in Redis const setSessionData = async (socketId, key, value) => { try { await pubClient.hset(`socket:${socketId}`, key, JSON.stringify(value)); // Use Redis pubClient } catch (error) { logger.log(`Error Setting Session Data for socket ${socketId}: ${error}`, "ERROR", "redis"); } }; // Retrieve session data from Redis const getSessionData = async (socketId, key) => { try { const data = await pubClient.hget(`socket:${socketId}`, key); return data ? JSON.parse(data) : null; } catch (error) { logger.log(`Error Getting Session Data for socket ${socketId}: ${error}`, "ERROR", "redis"); } }; // Clear session data from Redis const clearSessionData = async (socketId) => { try { await pubClient.del(`socket:${socketId}`); } catch (error) { logger.log(`Error Clearing Session Data for socket ${socketId}: ${error}`, "ERROR", "redis"); } }; /** * Add a socket mapping for a user * @param email * @param socketId * @param bodyshopId * @returns {Promise} */ const addUserSocketMapping = async (email, socketId, bodyshopId) => { const socketMappingKey = getUserSocketMappingKey(email); try { devDebugLogger(`Adding socket ${socketId} to user ${email} for bodyshop ${bodyshopId}`); // Save the mapping: socketId -> bodyshopId await pubClient.hset(socketMappingKey, socketId, bodyshopId); // Set TTL (24 hours) for the mapping hash await pubClient.expire(socketMappingKey, 86400); } catch (error) { logger.log(`Error adding socket mapping for ${email} (bodyshop ${bodyshopId}): ${error}`, "ERROR", "redis"); } }; /** * Refresh the TTL for a user's socket mapping * @param email * @returns {Promise} */ const refreshUserSocketTTL = async (email) => { const socketMappingKey = getUserSocketMappingKey(email); try { const exists = await pubClient.exists(socketMappingKey); if (exists) { await pubClient.expire(socketMappingKey, 86400); devDebugLogger(`Refreshed TTL for ${email} socket mapping`); } } catch (error) { logger.log(`Error refreshing TTL for ${email}: ${error}`, "ERROR", "redis"); } }; /** * Remove a socket mapping for a user * @param email * @param socketId * @returns {Promise} */ const removeUserSocketMapping = async (email, socketId) => { const socketMappingKey = getUserSocketMappingKey(email); try { devDebugLogger(`Removing socket ${socketId} mapping for user ${email}`); // Look up the bodyshopId associated with this socket const bodyshopId = await pubClient.hget(socketMappingKey, socketId); if (!bodyshopId) { devDebugLogger(`Socket ${socketId} not found for user ${email}`); return; } // Remove the socket mapping await pubClient.hdel(socketMappingKey, socketId); devDebugLogger(`Removed socket ${socketId} (associated with bodyshop ${bodyshopId}) for user ${email}`); // Refresh TTL if any socket mappings remain const remainingSockets = await pubClient.hlen(socketMappingKey); if (remainingSockets > 0) { await pubClient.expire(socketMappingKey, 86400); } } catch (error) { logger.log(`Error removing socket mapping for ${email}: ${error}`, "ERROR", "redis"); } }; /** * Get all socket mappings for a user * @param email * @returns {Promise<{}>} */ const getUserSocketMapping = async (email) => { const socketMappingKey = getUserSocketMappingKey(email); try { // Retrieve all socket mappings for the user const mapping = await pubClient.hgetall(socketMappingKey); const ttl = await pubClient.ttl(socketMappingKey); // Group socket IDs by bodyshopId const result = {}; for (const [socketId, bodyshopId] of Object.entries(mapping)) { if (!result[bodyshopId]) { result[bodyshopId] = { socketIds: [], ttl }; } result[bodyshopId].socketIds.push(socketId); } return result; } catch (error) { console.error(`Error retrieving socket mappings for ${email}:`, error); throw error; } }; /** * Get socket IDs for a user by bodyshopId * @param email * @param bodyshopId * @returns {Promise<{socketIds: [string, string], ttl: *}>} */ const getUserSocketMappingByBodyshop = async (email, bodyshopId) => { const socketMappingKey = getUserSocketMappingKey(email); try { // Retrieve all socket mappings for the user const mapping = await pubClient.hgetall(socketMappingKey); const ttl = await pubClient.ttl(socketMappingKey); // Filter socket IDs for the provided bodyshopId const socketIds = Object.entries(mapping).reduce((acc, [socketId, bId]) => { if (bId === bodyshopId) { acc.push(socketId); } return acc; }, []); return { socketIds, ttl }; } catch (error) { logger.log(`Error retrieving socket mappings for ${email} by bodyshop ${bodyshopId}: ${error}`, "ERROR", "redis"); throw error; } }; /** * Get bodyshop data from Redis * @param bodyshopId * @returns {Promise<*>} */ const getBodyshopFromRedis = async (bodyshopId) => { const key = getBodyshopCacheKey(bodyshopId); try { // Check if data exists in Redis const cachedData = await pubClient.get(key); if (cachedData) { return JSON.parse(cachedData); // Parse and return the full object } // Cache miss: fetch from DB const bodyshopData = await fetchBodyshopFromDB(bodyshopId, logger); // Store in Redis as a single JSON string const jsonData = JSON.stringify(bodyshopData); await pubClient.set(key, jsonData); await pubClient.expire(key, BODYSHOP_CACHE_TTL); devDebugLogger("bodyshop-cache-miss", { bodyshopId, action: "Fetched from DB and cached" }); return bodyshopData; // Return the full object } catch (error) { logger.log("get-bodyshop-from-redis", "ERROR", "redis", null, { bodyshopId, error: error.message }); throw error; } }; /** * Update or invalidate bodyshop data in Redis * @param bodyshopId * @param values * @returns {Promise} */ const updateOrInvalidateBodyshopFromRedis = async (bodyshopId, values = null) => { const key = getBodyshopCacheKey(bodyshopId); try { if (!values) { // Invalidate cache by deleting the key await pubClient.del(key); devDebugLogger("bodyshop-cache-invalidate", { bodyshopId, action: "Cache invalidated" }); } else { // Update cache with the full provided values const jsonData = JSON.stringify(values); await pubClient.set(key, jsonData); await pubClient.expire(key, BODYSHOP_CACHE_TTL); devDebugLogger("bodyshop-cache-update", { bodyshopId, action: "Cache updated", values }); } } catch (error) { logger.log("update-or-invalidate-bodyshop-from-redis", "ERROR", "api", "redis", { bodyshopId, values, error: error.message }); throw error; } }; // NOTE: The following code was written for an abandoned branch and things have changes since the, // Leaving it here for demonstration purposes, commenting it out so it does not get used // Store multiple session data in Redis // const setMultipleSessionData = async (socketId, keyValues) => { // try { // // keyValues is expected to be an object { key1: value1, key2: value2, ... } // const entries = Object.entries(keyValues).map(([key, value]) => [key, JSON.stringify(value)]); // await pubClient.hset(`socket:${socketId}`, ...entries.flat()); // } catch (error) { // logger.log(`Error Setting Multiple Session Data for socket ${socketId}: ${error}`, "ERROR", "redis"); // } // }; // Retrieve multiple session data from Redis // const getMultipleSessionData = async (socketId, keys) => { // try { // const data = await pubClient.hmget(`socket:${socketId}`, keys); // // Redis returns an object with null values for missing keys, so we parse the non-null ones // return Object.fromEntries(keys.map((key, index) => [key, data[index] ? JSON.parse(data[index]) : null])); // } catch (error) { // logger.log(`Error Getting Multiple Session Data for socket ${socketId}: ${error}`, "ERROR", "redis"); // } // }; // const setMultipleFromArraySessionData = async (socketId, keyValueArray) => { // try { // // Use Redis multi/pipeline to batch the commands // const multi = pubClient.multi(); // keyValueArray.forEach(([key, value]) => { // multi.hset(`socket:${socketId}`, key, JSON.stringify(value)); // }); // await multi.exec(); // Execute all queued commands // } catch (error) { // logger.log(`Error Setting Multiple Session Data for socket ${socketId}: ${error}`, "ERROR", "redis"); // } // }; // Helper function to add an item to the end of the Redis list // const addItemToEndOfList = async (socketId, key, newItem) => { // try { // await pubClient.rpush(`socket:${socketId}:${key}`, JSON.stringify(newItem)); // } catch (error) { // let userEmail = "unknown"; // let socketMappings = {}; // try { // const userData = await getSessionData(socketId, "user"); // if (userData && userData.email) { // userEmail = userData.email; // socketMappings = await getUserSocketMapping(userEmail); // } // } catch (sessionError) { // logger.log(`Failed to fetch session data for socket ${socketId}: ${sessionError}`, "ERROR", "redis"); // } // const mappingString = JSON.stringify(socketMappings, null, 2); // const errorMessage = `Error adding item to the end of the list for socket ${socketId}: ${error}. User: ${userEmail}, Socket Mappings: ${mappingString}`; // logger.log(errorMessage, "ERROR", "redis"); // } // }; // Helper function to add an item to the beginning of the Redis list // const addItemToBeginningOfList = async (socketId, key, newItem) => { // try { // await pubClient.lpush(`socket:${socketId}:${key}`, JSON.stringify(newItem)); // } catch (error) { // logger.log(`Error adding item to the beginning of the list for socket ${socketId}: ${error}`, "ERROR", "redis"); // } // }; // Helper function to clear a list in Redis // const clearList = async (socketId, key) => { // try { // await pubClient.del(`socket:${socketId}:${key}`); // } catch (error) { // logger.log(`Error clearing list for socket ${socketId}: ${error}`, "ERROR", "redis"); // } // }; // Add methods to manage room users // const addUserToRoom = async (room, user) => { // try { // await pubClient.sadd(room, JSON.stringify(user)); // } catch (error) { // logger.log(`Error adding user to room ${room}: ${error}`, "ERROR", "redis"); // } // }; // Remove users from room // const removeUserFromRoom = async (room, user) => { // try { // await pubClient.srem(room, JSON.stringify(user)); // } catch (error) { // logger.log(`Error removing user to room ${room}: ${error}`, "ERROR", "redis"); // } // }; // Get Users in room // const getUsersInRoom = async (room) => { // try { // const users = await pubClient.smembers(room); // return users.map((user) => JSON.parse(user)); // } catch (error) { // logger.log(`Error getting users in room ${room}: ${error}`, "ERROR", "redis"); // } // }; const api = { getUserSocketMappingKey, getBodyshopCacheKey, setSessionData, getSessionData, clearSessionData, addUserSocketMapping, removeUserSocketMapping, getUserSocketMappingByBodyshop, getUserSocketMapping, refreshUserSocketTTL, getBodyshopFromRedis, updateOrInvalidateBodyshopFromRedis // setMultipleSessionData, // getMultipleSessionData, // setMultipleFromArraySessionData, // addItemToEndOfList, // addItemToBeginningOfList, // clearList, // addUserToRoom, // removeUserFromRoom, // getUsersInRoom, }; Object.assign(module.exports, api); app.use((req, res, next) => { req.sessionUtils = api; next(); }); return api; }; module.exports = { applyRedisHelpers };