Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
5579280
search
AmirSa12 May 18, 2025
3062d20
update
AmirSa12 May 18, 2025
da97d56
update
AmirSa12 May 18, 2025
ca5d51e
update
AmirSa12 May 18, 2025
effc3e7
update stream
AmirSa12 May 22, 2025
f5a2472
better streams
AmirSa12 May 22, 2025
2631484
update streams
AmirSa12 May 22, 2025
fb822d1
request cancellation
AmirSa12 May 22, 2025
c0e9bab
cleanup
AmirSa12 May 22, 2025
160a070
revisions
AmirSa12 May 22, 2025
ab9f193
test - not sure
AmirSa12 May 22, 2025
da70d46
prettier
AmirSa12 May 22, 2025
148eb35
another test
AmirSa12 May 23, 2025
826d25b
cleanup - another test
AmirSa12 May 23, 2025
ac15156
update - test
AmirSa12 May 24, 2025
d264736
update - Jaccard algo
AmirSa12 May 24, 2025
a8cd641
handle console errs
AmirSa12 May 24, 2025
5e3294e
trigger ci
AmirSa12 May 24, 2025
c6b49aa
track time
AmirSa12 May 24, 2025
0808df6
cf headers
AmirSa12 May 24, 2025
54a022d
headers
AmirSa12 May 24, 2025
30598d9
fix time log
AmirSa12 May 24, 2025
7b095ae
test
AmirSa12 May 24, 2025
2ba90f4
benchmark
AmirSa12 May 24, 2025
a4ccd1f
update workflow
AmirSa12 May 24, 2025
0bc4ada
update workflow
AmirSa12 May 24, 2025
f907ba5
update workflow
AmirSa12 May 24, 2025
8e93cdd
revert
AmirSa12 May 24, 2025
999fc04
remove streams
AmirSa12 May 24, 2025
2b99b56
empty - trigger ci
AmirSa12 May 24, 2025
ddcca9c
use package
AmirSa12 May 25, 2025
a3ed3cc
revisions
AmirSa12 May 26, 2025
e795c7d
no score
AmirSa12 May 26, 2025
a4929f8
update
AmirSa12 May 26, 2025
f8759a6
update
AmirSa12 May 26, 2025
9902758
config
AmirSa12 May 26, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 46 additions & 15 deletions packages/app/app/components/RepoSearch.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,47 @@
<script lang="ts" setup>
import type { RepoNode } from "../../server/utils/types";
const search = useSessionStorage("search", "");
const searchResults = ref<RepoNode[]>([]);
const isLoading = ref(false);

let activeController: AbortController | null = null;
const throttledSearch = useThrottle(search, 500, true, false);

const { data, status } = useFetch("/api/repo/search", {
query: computed(() => ({ text: throttledSearch.value })),
immediate: !!throttledSearch.value,
});
watch(
throttledSearch,
async (newValue) => {
activeController?.abort();
searchResults.value = [];
if (!newValue) {
isLoading.value = false;
return;
}

const controller = new AbortController();
activeController = controller;

isLoading.value = true;
try {
const response = await fetch(
`/api/repo/search?text=${encodeURIComponent(newValue)}`,
{ signal: activeController.signal },
);
const data = (await response.json()) as { nodes: RepoNode[] };
if (activeController === controller) {
searchResults.value = data.nodes ?? [];
}
} catch (err: any) {
if (err.name !== "AbortError") {
console.error(err);

Check warning on line 35 in packages/app/app/components/RepoSearch.vue

View workflow job for this annotation

GitHub Actions / Run Linting

Unexpected console statement
}
} finally {
if (activeController === controller) {
isLoading.value = false;
}
}
},
{ immediate: false },
);

const examples = [
{
Expand Down Expand Up @@ -37,16 +72,12 @@
];

const router = useRouter();

function openFirstResult() {
if (data.value?.nodes[0]) {
const { owner, name } = data.value.nodes[0];
const [first] = searchResults.value;
if (first) {
router.push({
name: "repo:details",
params: {
owner: owner.login,
repo: name,
},
params: { owner: first.owner.login, repo: first.name },
});
}
}
Expand All @@ -64,13 +95,13 @@
@keydown.enter="openFirstResult()"
/>

<div v-if="status === 'pending'" class="-mb-2 relative">
<div v-if="isLoading" class="-mb-2 relative">
<UProgress size="xs" class="absolute inset-x-0 top-0" />
</div>

<div v-if="data?.nodes.length">
<div v-if="searchResults.length">
<RepoButton
v-for="repo in data.nodes"
v-for="repo in searchResults"
:key="repo.id"
:owner="repo.owner.login"
:name="repo.name"
Expand All @@ -79,7 +110,7 @@
</div>

<div
v-else-if="search && status !== 'pending'"
v-else-if="search && !isLoading"
class="text-gray-500 p-12 text-center"
>
No repositories found
Expand Down
1 change: 1 addition & 0 deletions packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"nuxt-shiki": "0.3.0",
"octokit": "^4.0.2",
"query-registry": "^3.0.1",
"string-similarity": "^4.0.4",
"unstorage": "^1.16.0",
"vue": "^3.5.13",
"vue-router": "^4.4.3",
Expand Down
102 changes: 41 additions & 61 deletions packages/app/server/api/repo/search.get.ts
Original file line number Diff line number Diff line change
@@ -1,86 +1,66 @@
import { z } from "zod";
import { useOctokitApp } from "../../utils/octokit";
import stringSimilarity from "string-similarity";

const querySchema = z.object({
text: z.string(),
});

export default defineEventHandler(async (event) => {
const r2Binding = useBinding(event);
const request = toWebRequest(event);
const signal = request.signal;

try {
const query = await getValidatedQuery(event, (data) =>
querySchema.parse(data),
);
if (!query.text) return { nodes: [] };

if (!query.text) {
return { nodes: [] };
}

const app = useOctokitApp(event);
const searchText = query.text.toLowerCase();
const matches: RepoNode[] = [];

// Internal pagination: iterate until uniqueNodes is filled or no more objects
let cursor: string | undefined;
const seen = new Set<string>();
const uniqueNodes = [];
const maxNodes = 10;
let keepGoing = true;
await app.eachRepository(async ({ repository }) => {
if (signal.aborted) return;
if (repository.private) return;

while (uniqueNodes.length < maxNodes && keepGoing && !signal.aborted) {
const listResult = await r2Binding.list({
prefix: usePackagesBucket.base,
limit: 1000,
cursor,
});
const { objects, truncated } = listResult;
cursor = truncated ? listResult.cursor : undefined;
const repoName = repository.name.toLowerCase();
const ownerLogin = repository.owner.login.toLowerCase();

const nameScore = stringSimilarity.compareTwoStrings(
repoName,
searchText,
);
const ownerScore = stringSimilarity.compareTwoStrings(
ownerLogin,
searchText,
);

for (const obj of objects) {
const parts = parseKey(obj.key);
const orgRepo = `${parts.org}/${parts.repo}`.toLowerCase();
const applies =
parts.org.toLowerCase().includes(searchText) ||
parts.repo.toLowerCase().includes(searchText) ||
orgRepo.includes(searchText);
if (!applies) continue;
matches.push({
id: repository.id,
name: repository.name,
owner: {
login: repository.owner.login,
avatarUrl: repository.owner.avatar_url,
},
stars: repository.stargazers_count || 0,
score: Math.max(nameScore, ownerScore),
});
});

const key = `${parts.org}/${parts.repo}`;
if (!seen.has(key)) {
seen.add(key);
uniqueNodes.push({
name: parts.repo,
owner: {
login: parts.org,
avatarUrl: `https://github.com/${parts.org}.png`,
},
});
if (uniqueNodes.length >= maxNodes) break;
}
}
matches.sort((a, b) =>
b.score !== a.score ? b.score - a.score : b.stars - a.stars,
);

if (!truncated || uniqueNodes.length >= maxNodes) {
keepGoing = false;
}
}
const top = matches.slice(0, 10).map((node) => ({
id: node.id,
name: node.name,
owner: node.owner,
stars: node.stars,
}));

return {
nodes: uniqueNodes,
};
return { nodes: top };
} catch (error) {
console.error("Error in repository search:", error);
return {
nodes: [],
error: true,
message: (error as Error).message,
};
return { nodes: [], error: true, message: (error as Error).message };
}
});

function parseKey(key: string) {
const parts = key.split(":");
return {
org: parts[2],
repo: parts[3],
};
}
8 changes: 8 additions & 0 deletions packages/app/server/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,11 @@ export interface Cursor {
timestamp: number;
sha: string;
}

export type RepoNode = {
id: number;
name: string;
owner: { login: string; avatarUrl: string };
stars: number;
score: number;
};
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading