Skip to content

Commit 2317d7c

Browse files
authored
perf(ui): abort claim modal check request on deduplication (#557)
1 parent 31cb504 commit 2317d7c

File tree

3 files changed

+52
-31
lines changed

3 files changed

+52
-31
lines changed

app/components/ClaimPackageModal.vue

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
<script setup lang="ts">
2-
import type { CheckNameResult } from '~/utils/package-name'
32
import { checkPackageName } from '~/utils/package-name'
43
54
const props = defineProps<{
@@ -16,25 +15,34 @@ const {
1615
refreshState,
1716
} = useConnector()
1817
19-
// Fetch name availability when modal opens
20-
const checkResult = shallowRef<CheckNameResult | null>(null)
21-
22-
const isChecking = shallowRef(false)
2318
const isPublishing = shallowRef(false)
24-
const publishError = shallowRef<string | null>(null)
2519
const publishSuccess = shallowRef(false)
20+
const publishError = shallowRef<string | null>(null)
2621
27-
async function checkAvailability() {
28-
isChecking.value = true
29-
publishError.value = null
30-
try {
31-
checkResult.value = await checkPackageName(props.packageName)
32-
} catch (err) {
33-
publishError.value = err instanceof Error ? err.message : $t('claim.modal.failed_to_check')
34-
} finally {
35-
isChecking.value = false
36-
}
37-
}
22+
const {
23+
data: checkResult,
24+
refresh: checkAvailability,
25+
status,
26+
error: checkError,
27+
} = useAsyncData(
28+
(_nuxtApp, { signal }) => {
29+
return checkPackageName(props.packageName, { signal })
30+
},
31+
{ default: () => null, immediate: false },
32+
)
33+
34+
const isChecking = computed(() => {
35+
return status.value === 'pending'
36+
})
37+
38+
const mergedError = computed(() => {
39+
return (
40+
publishError.value ??
41+
(checkError.value instanceof Error
42+
? checkError.value.message
43+
: $t('claim.modal.failed_to_check'))
44+
)
45+
})
3846
3947
const connectorModal = useModal('connector-modal')
4048
@@ -92,7 +100,6 @@ const dialogRef = ref<HTMLDialogElement>()
92100
93101
function open() {
94102
// Reset state and check availability each time modal is opened
95-
checkResult.value = null
96103
publishError.value = null
97104
publishSuccess.value = false
98105
checkAvailability()
@@ -287,11 +294,11 @@ const previewPackageJson = computed(() => {
287294

288295
<!-- Error message -->
289296
<div
290-
v-if="publishError"
297+
v-if="mergedError"
291298
role="alert"
292299
class="p-3 text-sm text-red-400 bg-red-500/10 border border-red-500/20 rounded-md"
293300
>
294-
{{ publishError }}
301+
{{ mergedError }}
295302
</div>
296303

297304
<!-- Actions -->
@@ -369,17 +376,17 @@ const previewPackageJson = computed(() => {
369376
</div>
370377

371378
<!-- Error state -->
372-
<div v-else-if="publishError" class="space-y-4">
379+
<div v-else-if="mergedError" class="space-y-4">
373380
<div
374381
role="alert"
375382
class="p-3 text-sm text-red-400 bg-red-500/10 border border-red-500/20 rounded-md"
376383
>
377-
{{ publishError }}
384+
{{ mergedError }}
378385
</div>
379386
<button
380387
type="button"
381388
class="w-full px-4 py-2 font-mono text-sm text-fg-muted bg-bg-subtle border border-border rounded-md transition-colors duration-200 hover:text-fg hover:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
382-
@click="checkAvailability"
389+
@click="() => checkAvailability()"
383390
>
384391
{{ $t('common.retry') }}
385392
</button>

app/composables/useNpmRegistry.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ const packumentCache = new Map<string, Promise<Packument | null>>()
2626
* Uses bulk API for unscoped packages, parallel individual requests for scoped.
2727
* Note: npm bulk downloads API does not support scoped packages.
2828
*/
29-
async function fetchBulkDownloads(packageNames: string[]): Promise<Map<string, number>> {
29+
async function fetchBulkDownloads(
30+
packageNames: string[],
31+
options: Parameters<typeof $fetch>[1] = {},
32+
): Promise<Map<string, number>> {
3033
const downloads = new Map<string, number>()
3134
if (packageNames.length === 0) return downloads
3235

@@ -44,6 +47,7 @@ async function fetchBulkDownloads(packageNames: string[]): Promise<Map<string, n
4447
try {
4548
const response = await $fetch<Record<string, { downloads: number } | null>>(
4649
`${NPM_API}/downloads/point/last-week/${chunk.join(',')}`,
50+
options,
4751
)
4852
for (const [name, data] of Object.entries(response)) {
4953
if (data?.downloads !== undefined) {
@@ -572,7 +576,7 @@ export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
572576
return results
573577
})(),
574578
// Fetch downloads in bulk
575-
fetchBulkDownloads(packageNames),
579+
fetchBulkDownloads(packageNames, { signal }),
576580
])
577581

578582
// Convert to search results with download data

app/utils/package-name.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,17 @@ export interface CheckNameResult {
7272

7373
const NPM_REGISTRY = 'https://registry.npmjs.org'
7474

75-
export async function checkPackageExists(name: string): Promise<boolean> {
75+
export async function checkPackageExists(
76+
name: string,
77+
options: Parameters<typeof $fetch>[1] = {},
78+
): Promise<boolean> {
7679
try {
7780
const encodedName = name.startsWith('@')
7881
? `@${encodeURIComponent(name.slice(1))}`
7982
: encodeURIComponent(name)
8083

8184
await $fetch(`${NPM_REGISTRY}/${encodedName}`, {
85+
...options,
8286
method: 'HEAD',
8387
})
8488
return true
@@ -87,7 +91,10 @@ export async function checkPackageExists(name: string): Promise<boolean> {
8791
}
8892
}
8993

90-
export async function findSimilarPackages(name: string): Promise<SimilarPackage[]> {
94+
export async function findSimilarPackages(
95+
name: string,
96+
options: Parameters<typeof $fetch>[1] = {},
97+
): Promise<SimilarPackage[]> {
9198
const normalized = normalizePackageName(name)
9299
const similar: SimilarPackage[] = []
93100

@@ -99,7 +106,7 @@ export async function findSimilarPackages(name: string): Promise<SimilarPackage[
99106
description?: string
100107
}
101108
}>
102-
}>(`${NPM_REGISTRY}/-/v1/search?text=${encodeURIComponent(name)}&size=20`)
109+
}>(`${NPM_REGISTRY}/-/v1/search?text=${encodeURIComponent(name)}&size=20`, options)
103110

104111
for (const obj of searchResponse.objects) {
105112
const pkgName = obj.package.name
@@ -153,7 +160,10 @@ export async function findSimilarPackages(name: string): Promise<SimilarPackage[
153160
}
154161
}
155162

156-
export async function checkPackageName(name: string): Promise<CheckNameResult> {
163+
export async function checkPackageName(
164+
name: string,
165+
options: Parameters<typeof $fetch>[1] = {},
166+
): Promise<CheckNameResult> {
157167
const validation = validatePackageName(name)
158168
const valid = validation.validForNewPackages === true
159169

@@ -177,8 +187,8 @@ export async function checkPackageName(name: string): Promise<CheckNameResult> {
177187

178188
// Check if package exists and find similar packages in parallel
179189
const [exists, similarPackages] = await Promise.all([
180-
checkPackageExists(name),
181-
findSimilarPackages(name),
190+
checkPackageExists(name, options),
191+
findSimilarPackages(name, options),
182192
])
183193

184194
result.available = !exists

0 commit comments

Comments
 (0)