Replace fetchReputationByUsername's full /users table scan with a scoped username lookup
メンテナーはふだん 1 日以内に返信
@Ayom04 がすでに取り組んでいます。
2026年8月16日 から。
評価
この issue はまだ評価されていません。
説明
Overview
fetchReputationByUsername, the function powering every single visit to /reputation/[handle] (every public contributor profile page in the app), fetches the entire user table from the backend just to find one row by username:
export async function fetchReputationByUsername(
username: string,
fallback: ReputationProfile | null,
): Promise<ReputationProfile | null> {
try {
const users = await request<(RawUserProfile & { id: string })[]>("/users");
const user = users.find((u) => u.username === username);
if (!user) return fallback;
const snapshot = await request<RawReputationSnapshot | null>(
`/reputation/${user.id}`,
);
return adaptReputation(user, snapshot);
} catch {
return fallback;
}
}
GET /users with no query parameters, no pagination, no filter — the entire platform's user list, downloaded and linear-scanned client-side (well, Server-Component-side) via .find(), purely to translate a username string into an id so a second request can fetch that user's reputation snapshot. This function is also called from a notFound()-triggering path (src/app/reputation/[handle]/page.tsx:14-20), meaning even a mistyped/nonexistent handle — the kind of request a crawler, a broken link, or a typo generates constantly — still pays the cost of downloading the full user table before concluding there's no match.
This doesn't scale. Per the landing page's own marketing copy, the platform already claims "341 contributors" — at that count this is a real but modest overhead; it becomes a serious, compounding cost as the user base grows, given /reputation/[handle] is: (a) a public, unauthenticated, presumably crawlable/shareable page (every contributor has an incentive to link their own profile), (b) rendered fresh on every request (cache: "no-store" is set in the underlying request() helper — see src/lib/api.ts:27-37 — so there's no HTTP-level caching softening this at all), and (c) architecturally guaranteed to keep getting slower over time as the user table grows, independent of how many people are actually viewing any given profile.
Requirements
- Replace the "fetch everyone, filter client-side" pattern with a scoped lookup — either a query-parameterized
/users?username=Xrequest, a dedicated/users/by-username/:usernameendpoint, or whatever shapemergefi-backendalready exposes (or would need to add) for looking up exactly one user by username. This will likely require backend coordination/a corresponding change inmergefi-backend— document that dependency explicitly in the PR rather than silently blocking on it, and consider whether a frontend-side interim mitigation (e.g. caching the full user list in-memory across requests within some short TTL, though this raises its own staleness concerns for a page meant to reflect a contributor's live reputation) is worth doing in the meantime. - Once the lookup is scoped, evaluate whether the subsequent
/reputation/${user.id}fetch can be combined into a single request (e.g. a backend endpoint that resolves username → reputation snapshot directly, avoiding two sequential round-trips for one page render) — this is a secondary, smaller optimization on top of the primary fix, worth doing in the same pass if backend coordination is already happening. - Confirm the
notFound()path for a nonexistent handle no longer requires downloading the full user table before concluding there's no match.
Acceptance Criteria
- Loading
/reputation/[handle]no longer issues a request for the entire, unfiltered/userscollection. - A nonexistent handle still correctly triggers
notFound(), without needing to have fetched more than the minimum data required to determine the user doesn't exist. - The existing behavior — successful profile load, live-vs-fallback mock behavior on fetch failure — is unchanged from the caller's perspective (
ReputationPageshouldn't need to change beyond whatever prop/shape adjustments the new lookup requires). - The PR documents whether this required a corresponding
mergefi-backendchange, and links to or describes that coordination.
Additional Notes
Precise references:
src/lib/api.ts:140-155— the fullfetchReputationByUsernamefunction; the unscopedrequest<...>("/users")call at line 145, the linear.find()at line 146.src/lib/api.ts:27-37— the underlyingrequest()helper, confirmingcache: "no-store"is always set (no HTTP caching layer softens the repeated full-table fetch across requests).src/app/reputation/[handle]/page.tsx:9-20— the only call site, anasyncServer Component that callsfetchReputationByUsername(handle, mockReputationProfiles[handle] ?? null)and immediatelynotFound()s on anullresult — confirming the full-table fetch happens on every single page view, including ones that end in a 404.- Landing page context:
src/lib/mock-data.ts:161-166(platformStats.activeContributors: 341) — the platform's own stated current scale, for context on how large "the entire user table" already claims to be even before further growth.
Edge cases: if the eventual scoped-lookup endpoint is case-sensitive or has different normalization behavior than the current .find((u) => u.username === username) exact-match comparison, verify handle casing/URL-decoding behavior is preserved (GitHub usernames are case-insensitive for uniqueness purposes but are typically stored/displayed with their original casing — confirm the new endpoint's matching semantics don't silently 404 a handle that currently resolves via the exact-match .find()).
Test/reproduction plan: with a mocked request(), assert that resolving /reputation/somehandle issues a request scoped to somehandle (e.g. asserting the mocked fetch was called with a URL containing the username as a query/path parameter) rather than a bare /users call with no parameters; assert a mocked "not found" response from the new scoped endpoint still results in notFound() being triggered by the page.
- 主要言語
- TypeScript
- スター
- 1
- フォーク
- 94
- 平均マージ
- 1時間 2分
- マージ済み PR(30日)
- 25
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
MergeFi/frontend のほかの issue
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
メンテナーはふだん 1 日以内に返信
-
bug testing
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
メンテナーはふだん 1 日以内に返信
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
メンテナーはふだん 1 日以内に返信
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 84/100
メンテナーはふだん 1 日以内に返信
-
accessibility bug
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
MergeFi/frontend#835 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
MergeFi/frontend の issue をすべて見る
似ている issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
zapcooking/frontend#946 ·
メンテナーはふだん 1 日以内に返信
-
enhancement good first issue
難易度 2/5 1〜3時間 初心者へのやさしさ 88/100
WiliamRosa/cosmos-semantic-platform#40 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 77/100
liberation-data/drivine#733 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 64/100
makecindy/cindy#10062 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
メンテナーはふだん 1 日以内に返信