[READ-ONLY] a fast, modern browser for the npm registry
at main 175 lines 4.9 kB view raw
1import type { MaybeRefOrGetter } from 'vue' 2import { toValue } from 'vue' 3import type { 4 VersionDistributionResponse, 5 VersionGroupDownloads, 6 VersionGroupingMode, 7} from '#shared/types/version-downloads' 8 9interface ChartDataItem { 10 name: string 11 downloads: number 12} 13 14/** 15 * Composable for managing version download distribution data and state. 16 * 17 * Fetches version download statistics from the API, manages grouping/filtering state, 18 * and formats data for chart visualization. 19 * 20 * @param packageName - The package name to fetch version downloads for 21 * @returns Reactive state and computed chart data 22 */ 23export function useVersionDistribution(packageName: MaybeRefOrGetter<string>) { 24 const groupingMode = ref<VersionGroupingMode>('major') 25 const showRecentOnly = ref(false) 26 const showLowUsageVersions = ref(false) 27 const pending = ref(false) 28 const error = ref<Error | null>(null) 29 const data = ref<VersionDistributionResponse | null>(null) 30 31 /** 32 * Fetches version download distribution from the API 33 */ 34 async function fetchDistribution() { 35 const pkgName = toValue(packageName) 36 if (!pkgName) { 37 data.value = null 38 return 39 } 40 41 pending.value = true 42 error.value = null 43 44 try { 45 const mode = groupingMode.value 46 const response = await $fetch<VersionDistributionResponse>( 47 `/api/registry/downloads/${encodeURIComponent(pkgName)}/versions`, 48 { 49 query: { 50 mode, 51 filterOldVersions: showRecentOnly.value ? 'true' : 'false', 52 filterThreshold: showLowUsageVersions.value ? '0' : '1', 53 }, 54 cache: 'default', // Don't force-cache since query params change frequently 55 }, 56 ) 57 58 data.value = response 59 } catch (err) { 60 error.value = err instanceof Error ? err : new Error('Failed to fetch version distribution') 61 data.value = null 62 } finally { 63 pending.value = false 64 } 65 } 66 67 /** 68 * Applies filtering to version groups based on current filter settings 69 * Sorts groups from oldest to newest version 70 */ 71 const filteredGroups = computed<VersionGroupDownloads[]>(() => { 72 if (!data.value) return [] 73 74 let groups = data.value.groups 75 76 // Filter using server-provided recent versions list 77 if (showRecentOnly.value && data.value.recentVersions) { 78 const recentVersionsSet = new Set(data.value.recentVersions) 79 80 groups = groups.filter(group => { 81 return group.versions.some(v => { 82 // Check exact version match 83 if (recentVersionsSet.has(v.version)) return true 84 85 // Also check base version (strip prerelease suffix) 86 if (v.version.includes('-')) { 87 const baseVersion = v.version.split('-')[0] 88 if (baseVersion && recentVersionsSet.has(baseVersion)) return true 89 } 90 91 return false 92 }) 93 }) 94 } 95 96 // Sort groups from oldest to newest by parsing version numbers 97 return groups.slice().sort((a, b) => { 98 // Extract version numbers from groupKey (e.g., "1.x" or "1.2.x") 99 const aParts = a.groupKey.replace(/\.x$/, '').split('.').map(Number) 100 const bParts = b.groupKey.replace(/\.x$/, '').split('.').map(Number) 101 102 for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) { 103 const aPart = aParts[i] ?? 0 104 const bPart = bParts[i] ?? 0 105 if (aPart !== bPart) { 106 return aPart - bPart 107 } 108 } 109 return 0 110 }) 111 }) 112 113 const chartDataset = computed<ChartDataItem[]>(() => { 114 const groups = filteredGroups.value 115 if (!groups.length) return [] 116 117 return groups.map(group => ({ 118 name: group.label, 119 downloads: group.downloads, 120 })) 121 }) 122 123 const totalDownloads = computed(() => { 124 const groups = filteredGroups.value 125 if (!groups || !groups.length) return 0 126 return groups.reduce((sum, group) => sum + group.downloads, 0) 127 }) 128 129 const hasData = computed(() => { 130 return data.value !== null && filteredGroups.value.length > 0 131 }) 132 133 // Refetch when filter changes - no immediate since we already have data 134 watch(showRecentOnly, () => { 135 fetchDistribution() 136 }) 137 138 watch(showLowUsageVersions, () => { 139 fetchDistribution() 140 }) 141 142 // Refetch when grouping mode changes - immediate to load initial data 143 watch( 144 groupingMode, 145 () => { 146 fetchDistribution() 147 }, 148 { immediate: true }, 149 ) 150 151 // Refetch when package name changes - not immediate since parent component controls initialization 152 watch( 153 () => toValue(packageName), 154 () => { 155 fetchDistribution() 156 }, 157 { immediate: false }, 158 ) 159 160 return { 161 // State 162 groupingMode, 163 showRecentOnly, 164 showLowUsageVersions, 165 pending, 166 error, 167 // Computed 168 filteredGroups, 169 chartDataset, 170 totalDownloads, 171 hasData, 172 // Methods 173 fetchDistribution, 174 } 175}