a fun bot for the hc slack

feat: add total takes count and optimize cache

dunkirk.sh d70d81df bc6daa01

verified
Changed files
+199 -152
src
features
libs
+84 -68
src/features/api/routes/projects.ts
···
import { db } from "../../../libs/db";
-
import { users as usersTable } from "../../../libs/schema";
import { handleApiError } from "../../../libs/apiError";
-
import { eq } from "drizzle-orm";
-
import { fetchUserData } from "../../../libs/cachet";
export type Project = {
projectName: string;
···
totalTakesTime: number;
userId: string;
userName?: string;
};
-
// Cache for user data from cachet
-
const userCache: Record<string, { name: string; timestamp: number }> = {};
-
const pendingRequests: Record<string, Promise<string>> = {};
-
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
-
-
// Function to get user name from cache or fetch it
-
async function getUserName(userId: string): Promise<string> {
-
const now = Date.now();
-
-
// Check if user data is in cache and still valid
-
if (userCache[userId] && now - userCache[userId].timestamp < CACHE_TTL) {
-
return userCache[userId].name;
-
}
-
-
// If there's already a pending request for this user, return that promise
-
// instead of creating a new request
-
if (pendingRequests[userId]) {
-
return pendingRequests[userId];
-
}
-
-
// Create a new promise for this user and store it
-
const fetchPromise = (async () => {
-
try {
-
const userData = await fetchUserData(userId);
-
const userName = userData?.displayName || "Unknown User";
-
-
userCache[userId] = {
-
name: userName,
-
timestamp: now,
-
};
-
-
return userName;
-
} catch (error) {
-
console.error("Error fetching user data:", error);
-
return "Unknown User";
-
} finally {
-
// Clean up the pending request when done
-
delete pendingRequests[userId];
-
}
-
})();
-
-
// Store the promise
-
pendingRequests[userId] = fetchPromise;
-
-
// Return the promise
-
return fetchPromise;
-
}
export async function projects(url: URL): Promise<Response> {
const user = url.searchParams.get("user");
try {
-
const projects = await db
-
.select({
-
projectName: usersTable.projectName,
-
projectDescription: usersTable.projectDescription,
-
projectBannerUrl: usersTable.projectBannerUrl,
-
totalTakesTime: usersTable.totalTakesTime,
-
userId: usersTable.id,
-
})
-
.from(usersTable)
-
.where(eq(usersTable.id, user ? user : usersTable.id));
-
if (projects.length === 0) {
return new Response(
JSON.stringify({
projects: [],
···
}
// Get unique user IDs
-
const userIds = [...new Set(projects.map((project) => project.userId))];
-
// Fetch all user names from cache or API
-
const userNamesPromises = userIds.map((id) => getUserName(id));
const userNames = await Promise.all(userNamesPromises);
// Create a map of user names
···
});
// Add user names to projects
-
const projectsWithUserNames = projects.map((project) => ({
...project,
userName: userNameMap[project.userId] || "Unknown User",
}));
return new Response(
JSON.stringify({
-
projects: user
-
? projectsWithUserNames[0]
-
: projectsWithUserNames,
}),
{
headers: {
···
import { db } from "../../../libs/db";
+
import { users as usersTable, takes as takesTable } from "../../../libs/schema";
import { handleApiError } from "../../../libs/apiError";
+
import { eq, count } from "drizzle-orm";
+
import { userService } from "../../../libs/cachet";
export type Project = {
projectName: string;
···
totalTakesTime: number;
userId: string;
userName?: string;
+
/** Total number of takes */
+
takesCount: number;
};
+
// Project cache to reduce database queries
+
const projectCache = new Map<
+
string,
+
{ data: Project | Project[]; timestamp: number }
+
>();
+
const PROJECT_CACHE_TTL = 60 * 1000; // 1 minute
export async function projects(url: URL): Promise<Response> {
const user = url.searchParams.get("user");
try {
+
// Check cache before database query
+
const cacheKey = user || "all_projects";
+
const cached = projectCache.get(cacheKey);
+
if (cached && Date.now() - cached.timestamp < PROJECT_CACHE_TTL) {
+
return new Response(
+
JSON.stringify({
+
projects: cached.data,
+
}),
+
{
+
headers: {
+
"Content-Type": "application/json",
+
},
+
},
+
);
+
}
+
// Use a JOIN query to get projects and takes count in a single database operation
+
let projectsWithCounts: {
+
projectName: string;
+
projectDescription: string;
+
projectBannerUrl: string;
+
totalTakesTime: number;
+
userId: string;
+
takesCount: number;
+
}[];
+
+
if (user) {
+
// For a single user, get their project data and takes count directly
+
projectsWithCounts = await db
+
.select({
+
projectName: usersTable.projectName,
+
projectDescription: usersTable.projectDescription,
+
projectBannerUrl: usersTable.projectBannerUrl,
+
totalTakesTime: usersTable.totalTakesTime,
+
userId: usersTable.id,
+
takesCount: count(takesTable.id).as("takes_count"),
+
})
+
.from(usersTable)
+
.leftJoin(takesTable, eq(usersTable.id, takesTable.userId))
+
.where(eq(usersTable.id, user))
+
.groupBy(usersTable.id);
+
} else {
+
// For all users, get project data and takes count
+
projectsWithCounts = await db
+
.select({
+
projectName: usersTable.projectName,
+
projectDescription: usersTable.projectDescription,
+
projectBannerUrl: usersTable.projectBannerUrl,
+
totalTakesTime: usersTable.totalTakesTime,
+
userId: usersTable.id,
+
takesCount: count(takesTable.id).as("takes_count"),
+
})
+
.from(usersTable)
+
.leftJoin(takesTable, eq(usersTable.id, takesTable.userId))
+
.groupBy(usersTable.id);
+
}
+
+
if (projectsWithCounts.length === 0) {
return new Response(
JSON.stringify({
projects: [],
···
}
// Get unique user IDs
+
const userIds = [
+
...new Set(projectsWithCounts.map((project) => project.userId)),
+
];
+
// Fetch all user names from shared user service
+
const userNamesPromises = userIds.map((id) =>
+
userService.getUserName(id),
+
);
const userNames = await Promise.all(userNamesPromises);
// Create a map of user names
···
});
// Add user names to projects
+
const projectsWithUserNames = projectsWithCounts.map((project) => ({
...project,
userName: userNameMap[project.userId] || "Unknown User",
}));
+
// Store in cache
+
const result = user ? projectsWithUserNames[0] : projectsWithUserNames;
+
projectCache.set(cacheKey, {
+
data: result as Project | Project[],
+
timestamp: Date.now(),
+
});
+
return new Response(
JSON.stringify({
+
projects: result,
}),
{
headers: {
+64 -84
src/features/api/routes/recentTakes.ts
···
-
import { eq, desc, or } from "drizzle-orm";
import { db } from "../../../libs/db";
import { takes as takesTable, users as usersTable } from "../../../libs/schema";
import { handleApiError } from "../../../libs/apiError";
-
import { fetchUserData } from "../../../libs/cachet";
export type RecentTake = {
id: string;
···
userName?: string; // Add userName field
};
-
// Cache for user data from cachet
-
const userCache: Record<string, { name: string; timestamp: number }> = {};
-
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
-
// Track pending requests to avoid duplicate API calls
-
const pendingRequests: Record<string, Promise<string>> = {};
-
-
// Function to get user name from cache or fetch it
-
async function getUserName(userId: string): Promise<string> {
-
const now = Date.now();
-
-
// Check if user data is in cache and still valid
-
if (userCache[userId] && now - userCache[userId].timestamp < CACHE_TTL) {
-
return userCache[userId].name;
-
}
-
-
// If there's already a pending request for this user, return that promise
-
// instead of creating a new request
-
if (pendingRequests[userId]) {
-
return pendingRequests[userId];
-
}
-
-
// Create a new promise for this user and store it
-
const fetchPromise = (async () => {
-
try {
-
const userData = await fetchUserData(userId);
-
const userName = userData?.displayName || "Unknown User";
-
-
userCache[userId] = {
-
name: userName,
-
timestamp: now,
-
};
-
-
return userName;
-
} catch (error) {
-
console.error("Error fetching user data:", error);
-
return "Unknown User";
-
} finally {
-
// Clean up the pending request when done
-
delete pendingRequests[userId];
-
}
-
})();
-
-
// Store the promise
-
pendingRequests[userId] = fetchPromise;
-
-
// Return the promise
-
return fetchPromise;
-
}
export async function recentTakes(url: URL): Promise<Response> {
try {
const userId = url.searchParams.get("user");
if (userId) {
// Verify user exists if userId provided
···
}
}
-
const recentTakes = await db
-
.select()
.from(takesTable)
.orderBy(desc(takesTable.createdAt))
-
.where(eq(takesTable.userId, userId ? userId : takesTable.userId))
.limit(40);
-
if (recentTakes.length === 0) {
return new Response(
JSON.stringify({
takes: [],
···
}
// Get unique user IDs
-
const userIds = [...new Set(recentTakes.map((take) => take.userId))];
-
-
// Query users from takes table
-
const users = await db
-
.select()
-
.from(usersTable)
-
.where(or(...userIds.map((id) => eq(usersTable.id, id))));
-
// Create map of user data by ID
-
const userMap = users.reduce(
-
(acc, user) => {
-
acc[user.id] = user;
-
return acc;
-
},
-
{} as Record<string, (typeof users)[number]>,
);
-
-
// Fetch all user names from cache or API
-
const userNamesPromises = userIds.map((id) => getUserName(id));
const userNames = await Promise.all(userNamesPromises);
// Create a map of user names
···
userNameMap[id] = userNames[index] || "unknown";
});
-
const takes: RecentTake[] =
-
recentTakes.map((take) => ({
-
id: take.id,
-
userId: take.userId,
-
notes: take.notes,
-
createdAt: new Date(take.createdAt),
-
mediaUrls: take.media ? JSON.parse(take.media) : [],
-
elapsedTime: take.elapsedTime,
-
project: userMap[take.userId]?.projectName || "unknown project",
-
totalTakesTime:
-
userMap[take.userId]?.totalTakesTime || take.elapsedTime,
-
userName: userNameMap[take.userId] || "Unknown User",
-
})) || [];
return new Response(
JSON.stringify({
···
+
import { eq, desc } from "drizzle-orm";
import { db } from "../../../libs/db";
import { takes as takesTable, users as usersTable } from "../../../libs/schema";
import { handleApiError } from "../../../libs/apiError";
+
import { userService } from "../../../libs/cachet";
export type RecentTake = {
id: string;
···
userName?: string; // Add userName field
};
+
// Recent takes cache to reduce database queries
+
const takesCache = new Map<string, { data: RecentTake[]; timestamp: number }>();
+
const TAKES_CACHE_TTL = 30 * 1000; // 30 seconds cache TTL - shorter since takes change frequently
export async function recentTakes(url: URL): Promise<Response> {
try {
const userId = url.searchParams.get("user");
+
+
// Check cache before querying database
+
const cacheKey = userId || "all_takes";
+
const cached = takesCache.get(cacheKey);
+
if (cached && Date.now() - cached.timestamp < TAKES_CACHE_TTL) {
+
return new Response(
+
JSON.stringify({
+
takes: cached.data,
+
}),
+
{
+
headers: {
+
"Content-Type": "application/json",
+
},
+
},
+
);
+
}
if (userId) {
// Verify user exists if userId provided
···
}
}
+
// Use a JOIN query to get takes and user data in a single operation
+
const takesWithUserData = await db
+
.select({
+
take: {
+
id: takesTable.id,
+
userId: takesTable.userId,
+
notes: takesTable.notes,
+
createdAt: takesTable.createdAt,
+
media: takesTable.media,
+
elapsedTime: takesTable.elapsedTime,
+
},
+
user: {
+
projectName: usersTable.projectName,
+
totalTakesTime: usersTable.totalTakesTime,
+
},
+
})
.from(takesTable)
+
.leftJoin(usersTable, eq(takesTable.userId, usersTable.id))
+
.where(userId ? eq(takesTable.userId, userId) : undefined)
.orderBy(desc(takesTable.createdAt))
.limit(40);
+
if (takesWithUserData.length === 0) {
return new Response(
JSON.stringify({
takes: [],
···
}
// Get unique user IDs
+
const userIds = [
+
...new Set(takesWithUserData.map((item) => item.take.userId)),
+
];
+
// Fetch all user names from shared user service
+
const userNamesPromises = userIds.map((id) =>
+
userService.getUserName(id),
);
const userNames = await Promise.all(userNamesPromises);
// Create a map of user names
···
userNameMap[id] = userNames[index] || "unknown";
});
+
// Map the joined results to the expected format
+
const takes: RecentTake[] = takesWithUserData.map((item) => ({
+
id: item.take.id,
+
userId: item.take.userId,
+
notes: item.take.notes,
+
createdAt: new Date(item.take.createdAt),
+
mediaUrls: item.take.media ? JSON.parse(item.take.media) : [],
+
elapsedTime: item.take.elapsedTime,
+
project: item.user?.projectName || "unknown project",
+
totalTakesTime: item.user?.totalTakesTime || item.take.elapsedTime,
+
userName: userNameMap[item.take.userId] || "Unknown User",
+
}));
+
+
// Store results in cache
+
takesCache.set(cacheKey, {
+
data: takes,
+
timestamp: Date.now(),
+
});
return new Response(
JSON.stringify({
+51
src/libs/cachet.ts
···
image: json.image,
};
}
···
image: json.image,
};
}
+
+
export const userService = {
+
cache: {} as Record<string, { name: string; timestamp: number }>,
+
pendingRequests: {} as Record<string, Promise<string>>,
+
CACHE_TTL: 5 * 60 * 1000,
+
+
async getUserName(userId: string): Promise<string> {
+
const now = Date.now();
+
+
// Check if user data is in cache and still valid
+
if (
+
this.cache[userId] &&
+
now - this.cache[userId].timestamp < this.CACHE_TTL
+
) {
+
return this.cache[userId].name;
+
}
+
+
// If there's already a pending request for this user, return that promise
+
// instead of creating a new request
+
if (this.pendingRequests[userId]) {
+
return this.pendingRequests[userId];
+
}
+
+
// Create a new promise for this user and store it
+
const fetchPromise = (async () => {
+
try {
+
const userData = await fetchUserData(userId);
+
const userName = userData?.displayName || "Unknown User";
+
+
this.cache[userId] = {
+
name: userName,
+
timestamp: now,
+
};
+
+
return userName;
+
} catch (error) {
+
console.error("Error fetching user data:", error);
+
return "Unknown User";
+
} finally {
+
// Clean up the pending request when done
+
delete this.pendingRequests[userId];
+
}
+
})();
+
+
// Store the promise
+
this.pendingRequests[userId] = fetchPromise;
+
+
// Return the promise
+
return fetchPromise;
+
},
+
};