Skip to content

Commit 8900390

Browse files
committed
update
1 parent 908082c commit 8900390

1 file changed

Lines changed: 113 additions & 74 deletions

File tree

packages/app/server/api/repo/search.get.ts

Lines changed: 113 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,119 +1,115 @@
11
import stringSimilarity from "string-similarity";
22
import { z } from "zod";
3+
import { useBucket } from "../../utils/bucket";
34
import { useOctokitApp } from "../../utils/octokit";
45

56
const querySchema = z.object({
67
text: z.string(),
78
});
89

9-
const INSTALLATION_CACHE_TTL_MS = 5 * 60 * 1000;
10+
const REPO_INDEX_CACHE_KEY = "repo-search:index";
11+
const REPO_INDEX_CACHE_TTL_MS = 5 * 60 * 1000;
1012

11-
interface CachedRepos {
12-
fetchedAt: number;
13-
repos: Array<{
14-
id: number;
15-
name: string;
16-
private: boolean;
17-
owner: { login: string; avatar_url: string };
18-
stargazers_count?: number;
19-
}>;
13+
interface RepoSearchIndexItem {
14+
id: number;
15+
name: string;
16+
ownerLogin: string;
17+
ownerAvatarUrl: string;
18+
stars: number;
2019
}
2120

22-
const installationRepoCache = new Map<number, CachedRepos>();
21+
interface RepoSearchIndexCache {
22+
fetchedAt: number;
23+
repos: RepoSearchIndexItem[];
24+
}
2325

2426
export default defineEventHandler(async (event) => {
2527
const query = await getValidatedQuery(event, (data) =>
2628
querySchema.parse(data),
2729
);
28-
2930
if (!query.text) {
3031
return { nodes: [] };
3132
}
3233

3334
const { signal } = toWebRequest(event);
34-
35-
setResponseHeaders(event, {
36-
"Content-Type": "text/event-stream",
37-
"Cache-Control": "no-cache",
38-
Connection: "keep-alive",
39-
});
40-
41-
const app = useOctokitApp(event);
4235
const searchText = query.text.toLowerCase();
36+
const app = useOctokitApp(event);
37+
const bucket = useBucket(event);
4338

44-
async function getInstallationRepos(installationId: number) {
45-
const cached = installationRepoCache.get(installationId);
46-
const now = Date.now();
47-
48-
if (cached && now - cached.fetchedAt < INSTALLATION_CACHE_TTL_MS) {
49-
return cached.repos;
50-
}
51-
52-
try {
53-
const octokit = await app.getInstallationOctokit(installationId);
54-
const { data } = await octokit.request("GET /installation/repositories", {
55-
per_page: 100,
56-
});
57-
installationRepoCache.set(installationId, {
58-
fetchedAt: now,
59-
repos: data.repositories,
60-
});
61-
return data.repositories;
62-
} catch {
63-
if (cached) {
64-
return cached.repos;
65-
}
66-
throw new Error("Unable to load repositories");
67-
}
68-
}
69-
70-
async function* iterateMatches() {
39+
const fetchInstalledRepos = async () => {
7140
const seen = new Set<number>();
41+
const repos: RepoSearchIndexItem[] = [];
7242

7343
for await (const { installation } of app.eachInstallation.iterator()) {
7444
if (signal.aborted) {
75-
return;
45+
break;
7646
}
7747

7848
try {
79-
const repos = await getInstallationRepos(installation.id);
80-
81-
for (const repo of repos) {
82-
if (repo.private || seen.has(repo.id)) {
83-
continue;
84-
}
85-
86-
const name = repo.name.toLowerCase();
87-
const owner = repo.owner.login.toLowerCase();
88-
const score = Math.max(
89-
stringSimilarity.compareTwoStrings(name, searchText),
90-
stringSimilarity.compareTwoStrings(owner, searchText),
49+
const octokit = await app.getInstallationOctokit(installation.id);
50+
let page = 1;
51+
52+
while (true) {
53+
const { data } = await octokit.request(
54+
"GET /installation/repositories",
55+
{
56+
page,
57+
per_page: 100,
58+
},
9159
);
9260

93-
if (
94-
score > 0.3 ||
95-
name.includes(searchText) ||
96-
owner.includes(searchText)
97-
) {
61+
for (const repo of data.repositories) {
62+
if (repo.private || seen.has(repo.id)) {
63+
continue;
64+
}
9865
seen.add(repo.id);
99-
yield JSON.stringify({
66+
repos.push({
10067
id: repo.id,
10168
name: repo.name,
102-
owner: {
103-
login: repo.owner.login,
104-
avatarUrl: repo.owner.avatar_url,
105-
},
69+
ownerLogin: repo.owner.login,
70+
ownerAvatarUrl: repo.owner.avatar_url,
10671
stars: repo.stargazers_count || 0,
10772
});
10873
}
74+
75+
if (data.repositories.length < 100) {
76+
break;
77+
}
78+
page += 1;
10979
}
11080
} catch {
11181
// Skip suspended installations
11282
}
11383
}
11484

115-
yield "[DONE]";
116-
}
85+
return repos;
86+
};
87+
88+
const getIndexedRepos = async () => {
89+
const now = Date.now();
90+
const cached =
91+
await bucket.getItem<RepoSearchIndexCache>(REPO_INDEX_CACHE_KEY);
92+
93+
if (cached && now - cached.fetchedAt < REPO_INDEX_CACHE_TTL_MS) {
94+
return cached.repos;
95+
}
96+
97+
const repos = await fetchInstalledRepos();
98+
if (!signal.aborted) {
99+
await bucket.setItem(REPO_INDEX_CACHE_KEY, {
100+
fetchedAt: now,
101+
repos,
102+
});
103+
}
104+
105+
return repos;
106+
};
107+
108+
setResponseHeaders(event, {
109+
"Content-Type": "text/event-stream",
110+
"Cache-Control": "no-cache",
111+
Connection: "keep-alive",
112+
});
117113

118114
const stream = new ReadableStream<string>({
119115
async start(controller) {
@@ -122,9 +118,52 @@ export default defineEventHandler(async (event) => {
122118
};
123119

124120
try {
125-
for await (const match of iterateMatches()) {
126-
send(match);
121+
const repos = await getIndexedRepos(event, signal);
122+
const matches = repos
123+
.map((repo) => {
124+
const name = repo.name.toLowerCase();
125+
const owner = repo.ownerLogin.toLowerCase();
126+
const score = Math.max(
127+
stringSimilarity.compareTwoStrings(name, searchText),
128+
stringSimilarity.compareTwoStrings(owner, searchText),
129+
);
130+
131+
if (
132+
score <= 0.3 &&
133+
!name.includes(searchText) &&
134+
!owner.includes(searchText)
135+
) {
136+
return null;
137+
}
138+
139+
return {
140+
...repo,
141+
score,
142+
};
143+
})
144+
.filter(
145+
(repo): repo is RepoSearchIndexItem & { score: number } => !!repo,
146+
)
147+
.sort((a, b) => b.score - a.score || b.stars - a.stars);
148+
149+
for (const repo of matches) {
150+
if (signal.aborted) {
151+
break;
152+
}
153+
send(
154+
JSON.stringify({
155+
id: repo.id,
156+
name: repo.name,
157+
owner: {
158+
login: repo.ownerLogin,
159+
avatarUrl: repo.ownerAvatarUrl,
160+
},
161+
stars: repo.stars,
162+
}),
163+
);
127164
}
165+
166+
send("[DONE]");
128167
} catch (err) {
129168
send(JSON.stringify({ error: (err as Error).message }));
130169
} finally {

0 commit comments

Comments
 (0)