feat: Implement request cancellation for package fetching using AbortController in PackageBrowserV2 and update API client to support it.

This commit is contained in:
Yusuf İpek
2025-11-24 03:08:13 +03:00
parent 0e4716db54
commit d15e1c526e
3 changed files with 98 additions and 103 deletions
+40 -29
View File
@@ -42,6 +42,8 @@ export function PackageBrowserV2({
// Load packages when platform changes
useEffect(() => {
const controller = new AbortController()
const loadPackages = async () => {
if (!selectedPlatform) {
setPackages([])
@@ -73,7 +75,7 @@ export function PackageBrowserV2({
}
console.log('🔍 Frontend: API params:', params)
const result = await apiClient.getPackages(params)
const result = await apiClient.getPackages(params, controller.signal)
console.log('📦 Frontend: Received initial packages:', {
total: result.total,
packageCount: result.packages.length,
@@ -83,14 +85,24 @@ export function PackageBrowserV2({
setTotalCount(result.total)
setHasMore(result.packages.length < result.total)
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
console.log('Request aborted')
return
}
console.error('Failed to load packages:', error)
setPackages([])
} finally {
setLoading(false)
if (!controller.signal.aborted) {
setLoading(false)
}
}
}
loadPackages()
return () => {
controller.abort()
}
}, [selectedPlatform, typeFilter, repositoryFilter, isDebianUbuntu, isArch])
// Debounced search to prevent focus loss
@@ -256,30 +268,30 @@ export function PackageBrowserV2({
</div>
{/* Type Filter (Debian/Ubuntu only) */}
{isDebianUbuntu && (
<Select value={typeFilter || "all"} onValueChange={(value) => setTypeFilter(value === "all" ? "" : value)}>
<SelectTrigger className="w-full sm:w-40">
<SelectValue placeholder="Type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Types</SelectItem>
<SelectItem value="gui">GUI</SelectItem>
<SelectItem value="cli">CLI</SelectItem>
</SelectContent>
</Select>
)}
{isArch && (
<Select value={repositoryFilter || "all"} onValueChange={(value) => setRepositoryFilter(value === "all" ? "" : value)}>
<SelectTrigger className="w-full sm:w-40">
<SelectValue placeholder="Repository" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All</SelectItem>
<SelectItem value="official">Official</SelectItem>
<SelectItem value="aur">AUR</SelectItem>
</SelectContent>
</Select>
)}
{isDebianUbuntu && (
<Select value={typeFilter || "all"} onValueChange={(value) => setTypeFilter(value === "all" ? "" : value)}>
<SelectTrigger className="w-full sm:w-40">
<SelectValue placeholder="Type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Types</SelectItem>
<SelectItem value="gui">GUI</SelectItem>
<SelectItem value="cli">CLI</SelectItem>
</SelectContent>
</Select>
)}
{isArch && (
<Select value={repositoryFilter || "all"} onValueChange={(value) => setRepositoryFilter(value === "all" ? "" : value)}>
<SelectTrigger className="w-full sm:w-40">
<SelectValue placeholder="Repository" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All</SelectItem>
<SelectItem value="official">Official</SelectItem>
<SelectItem value="aur">AUR</SelectItem>
</SelectContent>
</Select>
)}
</div>
{/* Package List */}
@@ -303,11 +315,10 @@ export function PackageBrowserV2({
packages.map((pkg) => (
<div
key={pkg.id}
className={`flex items-start space-x-3 p-3 rounded-lg border transition-colors cursor-pointer ${
isPackageSelected(pkg)
className={`flex items-start space-x-3 p-3 rounded-lg border transition-colors cursor-pointer ${isPackageSelected(pkg)
? 'border-primary bg-primary/5'
: 'border-border hover:bg-secondary/50'
}`}
}`}
onClick={() => onPackageToggle(pkg)}
>
<Checkbox
+5 -3
View File
@@ -6,7 +6,7 @@ import {
const API_BASE_URL =
process.env.NEXT_PUBLIC_API_URL &&
process.env.NEXT_PUBLIC_API_URL.trim() !== ""
process.env.NEXT_PUBLIC_API_URL.trim() !== ""
? process.env.NEXT_PUBLIC_API_URL.replace(/\/$/, "")
: "/api";
@@ -47,7 +47,8 @@ class ApiClient {
// Package operations
async getPackages(
filters: FilterOptions = {}
filters: FilterOptions = {},
signal?: AbortSignal
): Promise<{ packages: Package[]; total: number }> {
const params = new URLSearchParams();
@@ -64,7 +65,8 @@ class ApiClient {
const query = params.toString() ? `?${params.toString()}` : "";
return this.request<{ packages: Package[]; total: number }>(
`/packages${query}`
`/packages${query}`,
{ signal }
);
}
+1 -19
View File
@@ -7,25 +7,7 @@ interface ArchPackage {
lastUpdated: string
}
// Simple HTML parser without external dependencies
function parseHTML(html: string) {
return {
querySelector: (selector: string) => {
const regex = new RegExp(`<${selector}[^>]*>([\\s\\S]*?)<\\/${selector}>`, 'gi')
const match = regex.exec(html)
return match ? { textContent: match[1].replace(/<[^>]*>/g, '').trim() } : null
},
querySelectorAll: (selector: string) => {
const results: any[] = []
const regex = new RegExp(`<${selector}[^>]*>([\\s\\S]*?)<\\/${selector}>`, 'gi')
let match
while ((match = regex.exec(html)) !== null) {
results.push({ innerHTML: match[1], textContent: match[1].replace(/<[^>]*>/g, '').trim() })
}
return results
}
}
}
export class ArchPackageFetcher {
private baseUrl = 'https://archlinux.org/packages'