forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
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}