From 5fa299cc9a6927e082b933947b3a1c71cffc8586 Mon Sep 17 00:00:00 2001 From: Ahmet Kilinc Date: Sat, 20 Dec 2025 11:19:18 +0000 Subject: [PATCH] speed improvements? --- actions/repositories.ts | 142 +++++++++++++++++++++----- app/(main)/[username]/[repo]/page.tsx | 34 ++---- lib/r2-fs.ts | 44 ++++++-- 3 files changed, 158 insertions(+), 62 deletions(-) diff --git a/actions/repositories.ts b/actions/repositories.ts index 0b590b9..9b15ad8 100644 --- a/actions/repositories.ts +++ b/actions/repositories.ts @@ -346,8 +346,7 @@ export async function getRepositoryWithStars(owner: string, name: string) { const repo = await getRepository(owner, name); if (!repo) return null; - const starCount = await getStarCount(repo.id); - const starred = await isStarredByUser(repo.id); + const [starCount, starred] = await Promise.all([getStarCount(repo.id), isStarredByUser(repo.id)]); return { ...repo, starCount, starred }; } @@ -365,10 +364,7 @@ export async function getUserRepositoriesWithStars(username: string) { return reposWithStars; } -export async function updateRepository( - repoId: string, - data: { name?: string; description?: string; visibility?: "public" | "private" } -) { +export async function updateRepository(repoId: string, data: { name?: string; description?: string; visibility?: "public" | "private" }) { const session = await getSession(); if (!session?.user) { throw new Error("Unauthorized"); @@ -445,13 +441,7 @@ export async function getRepoBranches(owner: string, repoName: string) { } } -export async function getRepoCommits( - owner: string, - repoName: string, - branch: string, - limit: number = 30, - skip: number = 0 -) { +export async function getRepoCommits(owner: string, repoName: string, branch: string, limit: number = 30, skip: number = 0) { const user = await db.query.users.findFirst({ where: eq(users.username, owner), }); @@ -515,11 +505,7 @@ export async function getRepoCommitCount(owner: string, repoName: string, branch } } -export async function getPublicRepositories( - sortBy: "stars" | "updated" | "created" = "updated", - limit: number = 20, - offset: number = 0 -) { +export async function getPublicRepositories(sortBy: "stars" | "updated" | "created" = "updated", limit: number = 20, offset: number = 0) { const allRepos = await db .select({ id: repositories.id, @@ -538,13 +524,7 @@ export async function getPublicRepositories( .from(repositories) .innerJoin(users, eq(repositories.ownerId, users.id)) .where(eq(repositories.visibility, "public")) - .orderBy( - sortBy === "stars" - ? desc(sql`star_count`) - : sortBy === "created" - ? desc(repositories.createdAt) - : desc(repositories.updatedAt) - ) + .orderBy(sortBy === "stars" ? desc(sql`star_count`) : sortBy === "created" ? desc(repositories.createdAt) : desc(repositories.updatedAt)) .limit(limit + 1) .offset(offset); @@ -571,3 +551,115 @@ export async function getPublicRepositories( hasMore, }; } + +export type FileEntry = { + name: string; + type: "blob" | "tree"; + oid: string; + path: string; + lastCommit: { message: string; timestamp: number } | null; +}; + +export async function getRepoPageData(owner: string, repoName: string) { + const [user, session] = await Promise.all([db.query.users.findFirst({ where: eq(users.username, owner) }), getSession()]); + + if (!user) return null; + + const repo = await db.query.repositories.findFirst({ + where: and(eq(repositories.ownerId, user.id), eq(repositories.name, repoName)), + }); + + if (!repo) return null; + + if (repo.visibility === "private" && (!session?.user || session.user.id !== repo.ownerId)) { + return null; + } + + const [starCountResult, starredResult] = await Promise.all([ + db.select({ count: count() }).from(stars).where(eq(stars.repositoryId, repo.id)), + session?.user ? db.query.stars.findFirst({ where: and(eq(stars.userId, session.user.id), eq(stars.repositoryId, repo.id)) }) : Promise.resolve(null), + ]); + + const starCount = starCountResult[0]?.count ?? 0; + const starred = !!starredResult; + const isOwner = session?.user?.id === repo.ownerId; + + const repoPrefix = getRepoPrefix(user.id, `${repoName}.git`); + const fs = createR2Fs(repoPrefix); + + let files: FileEntry[] = []; + let isEmpty = true; + let readmeContent: string | null = null; + let branches: string[] = []; + let commitCount = 0; + + try { + const [branchList, commits] = await Promise.all([git.listBranches({ fs, gitdir: "/" }), git.log({ fs, gitdir: "/", ref: repo.defaultBranch })]); + + branches = branchList; + commitCount = commits.length; + + if (commits.length > 0) { + isEmpty = false; + const commitOid = commits[0].oid; + + const { tree } = await git.readTree({ fs, gitdir: "/", oid: commitOid }); + + const fileEntries = await Promise.all( + tree.map(async (entry) => { + let lastCommit: { message: string; timestamp: number } | null = null; + try { + const fileCommits = await git.log({ fs, gitdir: "/", ref: repo.defaultBranch, filepath: entry.path, depth: 1 }); + if (fileCommits.length > 0) { + lastCommit = { + message: fileCommits[0].commit.message.split("\n")[0], + timestamp: fileCommits[0].commit.committer.timestamp * 1000, + }; + } + } catch {} + return { + name: entry.path, + type: entry.type as "blob" | "tree", + oid: entry.oid, + path: entry.path, + lastCommit, + }; + }) + ); + + fileEntries.sort((a, b) => { + if (a.type === "tree" && b.type !== "tree") return -1; + if (a.type !== "tree" && b.type === "tree") return 1; + return a.name.localeCompare(b.name); + }); + + files = fileEntries; + + const readmeEntry = tree.find((e) => e.path.toLowerCase() === "readme.md" && e.type === "blob"); + if (readmeEntry) { + const { blob } = await git.readBlob({ fs, gitdir: "/", oid: readmeEntry.oid }); + readmeContent = new TextDecoder("utf-8").decode(blob); + } + } + } catch (err: unknown) { + const error = err as { code?: string }; + if (error.code !== "NotFoundError") { + console.error("getRepoPageData error:", err); + } + } + + return { + repo: { + ...repo, + owner: { id: user.id, username: user.username, name: user.name, image: user.image }, + starCount, + starred, + }, + files, + isEmpty, + readmeContent, + branches, + commitCount, + isOwner, + }; +} diff --git a/app/(main)/[username]/[repo]/page.tsx b/app/(main)/[username]/[repo]/page.tsx index d437735..78e1b88 100644 --- a/app/(main)/[username]/[repo]/page.tsx +++ b/app/(main)/[username]/[repo]/page.tsx @@ -1,6 +1,5 @@ import { notFound } from "next/navigation"; -import { getRepositoryWithStars, getRepoFileTree, getRepoFile, getRepoBranches, getRepoCommitCount } from "@/actions/repositories"; -import { getSession } from "@/lib/session"; +import { getRepoPageData } from "@/actions/repositories"; import { FileTree } from "@/components/file-tree"; import { CodeViewer } from "@/components/code-viewer"; import { CloneUrl } from "@/components/clone-url"; @@ -15,27 +14,13 @@ import { getPublicServerUrl } from "@/lib/utils"; export default async function RepoPage({ params }: { params: Promise<{ username: string; repo: string }> }) { const { username, repo: repoName } = await params; - const repo = await getRepositoryWithStars(username, repoName); + const data = await getRepoPageData(username, repoName); - if (!repo) { + if (!data) { notFound(); } - const session = await getSession(); - const isOwner = session?.user?.id === repo.ownerId; - - const [fileTree, branches, commitCount] = await Promise.all([ - getRepoFileTree(username, repoName, repo.defaultBranch), - getRepoBranches(username, repoName), - getRepoCommitCount(username, repoName, repo.defaultBranch), - ]); - const readmeFile = fileTree?.files.find((f) => f.name.toLowerCase() === "readme.md" && f.type === "blob"); - - let readmeContent = null; - if (readmeFile) { - const file = await getRepoFile(username, repoName, repo.defaultBranch, readmeFile.name); - readmeContent = file?.content; - } + const { repo, files, isEmpty, readmeContent, branches, commitCount, isOwner } = data; return (
@@ -81,12 +66,7 @@ export default async function RepoPage({ params }: { params: Promise<{ username:
- + {commitCount > 0 && ( - {fileTree?.isEmpty ? ( + {isEmpty ? ( ) : ( - + )}
diff --git a/lib/r2-fs.ts b/lib/r2-fs.ts index f9beff2..0a50668 100644 --- a/lib/r2-fs.ts +++ b/lib/r2-fs.ts @@ -1,4 +1,6 @@ -import { r2Get, r2Put, r2Delete, r2Exists, r2List, r2DeletePrefix } from "./r2"; +import { r2Get, r2Put, r2Delete, r2List, r2DeletePrefix } from "./r2"; + +const NOT_FOUND = Symbol("NOT_FOUND"); function normalizePath(path: string): string { return path.replace(/\/+/g, "/").replace(/^\//, "").replace(/\/$/, ""); @@ -24,6 +26,8 @@ function createStatResult(type: "file" | "dir", size: number) { export function createR2Fs(repoPrefix: string) { const dirMarkerCache = new Set(); + const fileCache = new Map(); + const listCache = new Map(); const getKey = (filepath: string) => { const normalized = normalizePath(filepath); @@ -36,9 +40,28 @@ export function createR2Fs(repoPrefix: string) { return `${repoPrefix}/${normalized}`.replace(/\/+/g, "/"); }; + const cachedR2Get = async (key: string): Promise => { + if (fileCache.has(key)) { + const cached = fileCache.get(key); + return cached === NOT_FOUND ? null : cached!; + } + const data = await r2Get(key); + fileCache.set(key, data ?? NOT_FOUND); + return data; + }; + + const cachedR2List = async (prefix: string): Promise => { + if (listCache.has(prefix)) { + return listCache.get(prefix)!; + } + const keys = await r2List(prefix); + listCache.set(prefix, keys); + return keys; + }; + const readFile = async (filepath: string, options?: { encoding?: string }): Promise => { const key = getKey(filepath); - const data = await r2Get(key); + const data = await cachedR2Get(key); if (!data) { const err = new Error(`ENOENT: no such file or directory, open '${filepath}'`) as NodeJS.ErrnoException; err.code = "ENOENT"; @@ -52,19 +75,21 @@ export function createR2Fs(repoPrefix: string) { const writeFile = async (filepath: string, data: Buffer | string): Promise => { const key = getKey(filepath); - console.log("[R2FS] writeFile:", key, "size:", typeof data === "string" ? data.length : data.length); - await r2Put(key, typeof data === "string" ? Buffer.from(data) : data); + const buffer = typeof data === "string" ? Buffer.from(data) : data; + await r2Put(key, buffer); + fileCache.set(key, buffer); }; const unlink = async (filepath: string): Promise => { const key = getKey(filepath); await r2Delete(key); + fileCache.set(key, NOT_FOUND); }; const readdir = async (filepath: string): Promise => { const prefix = getKey(filepath); const fullPrefix = prefix.endsWith("/") ? prefix : `${prefix}/`; - const keys = await r2List(fullPrefix); + const keys = await cachedR2List(fullPrefix); const entries = new Set(); for (const key of keys) { @@ -98,14 +123,13 @@ export function createR2Fs(repoPrefix: string) { return createStatResult("dir", 0); } - const exists = await r2Exists(key); - if (exists) { - const data = await r2Get(key); - return createStatResult("file", data?.length || 0); + const data = await cachedR2Get(key); + if (data) { + return createStatResult("file", data.length); } const prefix = key.endsWith("/") ? key : `${key}/`; - const children = await r2List(prefix); + const children = await cachedR2List(prefix); if (children.length > 0) { return createStatResult("dir", 0); }