-
-
Notifications
You must be signed in to change notification settings - Fork 424
Expand file tree
/
Copy pathuseUserPackages.ts
More file actions
249 lines (213 loc) · 7.78 KB
/
useUserPackages.ts
File metadata and controls
249 lines (213 loc) · 7.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
import { bridgeSearchSSRPayload } from './search-utils'
/** Default page size for incremental loading (npm registry path) */
const PAGE_SIZE = 50 as const
/** npm search API practical limit for maintainer queries */
const MAX_RESULTS = 250
/**
* Fetch packages for a given npm user/maintainer.
*
* The composable handles all loading strategy internally based on the active
* search provider. Consumers get a uniform interface regardless of provider:
*
* - **Algolia**: Fetches all packages at once via `owner.name` filter (fast).
* - **npm**: Incrementally paginates through `maintainer:` search results.
*
* @example
* ```ts
* const { data, status, hasMore, isLoadingMore, loadMore } = useUserPackages(username)
* ```
*/
export function useUserPackages(username: MaybeRefOrGetter<string>) {
const { searchProvider } = useSearchProvider()
// this is only used in npm path, but we need to extract it when the composable runs
const { $npmRegistry } = useNuxtApp()
const { searchByOwner } = useAlgoliaSearch()
// --- Incremental loading state (npm path) ---
const currentPage = shallowRef(1)
/** Tracks which provider actually served the current data (may differ from
* searchProvider when Algolia returns empty and we fall through to npm) */
const activeProvider = shallowRef<'npm' | 'algolia'>(searchProvider.value)
bridgeSearchSSRPayload('user-packages', username, searchProvider)
const cache = shallowRef<{
username: string
objects: NpmSearchResult[]
total: number
} | null>(null)
const isLoadingMore = shallowRef(false)
const asyncData = useLazyAsyncData(
() => `user-packages:${searchProvider.value}:${toValue(username)}`,
async (_nuxtApp, { signal }) => {
const user = toValue(username)
if (!user) {
return emptySearchResponse()
}
const provider = searchProvider.value
// --- Algolia: fetch all at once ---
if (provider === 'algolia') {
try {
const response = await searchByOwner(user)
// Guard against stale response (user/provider changed during await)
if (user !== toValue(username) || provider !== searchProvider.value) {
return emptySearchResponse()
}
// If Algolia returns results, use them. If empty, fall through to npm
// registry which uses `maintainer:` search (matches all maintainers,
// not just the primary owner that Algolia's owner.name indexes).
if (response.objects.length > 0) {
activeProvider.value = 'algolia'
cache.value = {
username: user,
objects: response.objects,
total: response.total,
}
return response
}
} catch {
// Fall through to npm registry path on Algolia failure
}
}
// --- npm registry: initial page (or Algolia fallback) ---
activeProvider.value = 'npm'
cache.value = null
currentPage.value = 1
const params = new URLSearchParams()
params.set('text', `maintainer:${user}`)
params.set('size', String(PAGE_SIZE))
const { data: response, isStale } = await $npmRegistry<NpmSearchResponse>(
`/-/v1/search?${params.toString()}`,
{ signal },
60,
)
// Guard against stale response (user/provider changed during await)
if (user !== toValue(username) || provider !== searchProvider.value) {
return emptySearchResponse()
}
cache.value = {
username: user,
objects: response.objects,
total: response.total,
}
return { ...response, isStale }
},
{ default: emptySearchResponse },
)
// --- Fetch more (npm path only) ---
/**
* Fetch the next page of results from npm registry.
* @param manageLoadingState - When false, caller manages isLoadingMore (used by loadAll to prevent flicker)
*/
async function fetchMore(manageLoadingState = true): Promise<void> {
const user = toValue(username)
// Use activeProvider: if Algolia fell through to npm, we still need pagination
if (!user || activeProvider.value !== 'npm') return
if (cache.value && cache.value.username !== user) {
cache.value = null
await asyncData.refresh()
return
}
const currentCount = cache.value?.objects.length ?? 0
const total = Math.min(cache.value?.total ?? Infinity, MAX_RESULTS)
if (currentCount >= total) return
if (manageLoadingState) isLoadingMore.value = true
try {
const from = currentCount
const size = Math.min(PAGE_SIZE, total - currentCount)
const params = new URLSearchParams()
params.set('text', `maintainer:${user}`)
params.set('size', String(size))
params.set('from', String(from))
const { data: response } = await $npmRegistry<NpmSearchResponse>(
`/-/v1/search?${params.toString()}`,
{},
60,
)
// Guard against stale response
if (user !== toValue(username) || activeProvider.value !== 'npm') return
if (cache.value && cache.value.username === user) {
const existingNames = new Set(cache.value.objects.map(obj => obj.package.name))
const newObjects = response.objects.filter(obj => !existingNames.has(obj.package.name))
cache.value = {
username: user,
objects: [...cache.value.objects, ...newObjects],
total: response.total,
}
} else {
cache.value = {
username: user,
objects: response.objects,
total: response.total,
}
}
} finally {
if (manageLoadingState) isLoadingMore.value = false
}
}
/** Load the next page of results (no-op if all loaded or using Algolia) */
async function loadMore(): Promise<void> {
if (isLoadingMore.value || !hasMore.value) return
currentPage.value++
await fetchMore()
}
/** Load all remaining results at once (e.g. when user starts filtering) */
async function loadAll(): Promise<void> {
if (!hasMore.value) return
isLoadingMore.value = true
try {
while (hasMore.value) {
await fetchMore(false)
}
} finally {
isLoadingMore.value = false
}
}
// asyncdata will automatically rerun due to key, but we need to reset cache/page
// when provider changes
watch(
() => searchProvider.value,
newProvider => {
cache.value = null
currentPage.value = 1
activeProvider.value = newProvider
},
)
// Computed data that uses cache (only if it belongs to the current username)
const data = computed<NpmSearchResponse | null>(() => {
const user = toValue(username)
if (cache.value && cache.value.username === user) {
return {
isStale: false,
objects: cache.value.objects,
total: cache.value.total,
time: new Date().toISOString(),
}
}
return asyncData.data.value
})
/** Whether there are more results available to load (npm path only) */
const hasMore = computed(() => {
if (!toValue(username)) return false
// Algolia fetches everything in one request; only npm needs pagination
if (activeProvider.value !== 'npm') return false
if (!cache.value) return true
// npm path: more available if we haven't hit the server total or our cap
const fetched = cache.value.objects.length
const available = cache.value.total
return fetched < available && fetched < MAX_RESULTS
})
const { data: _data, ...rest } = asyncData
return {
...rest,
/** Reactive package results */
data,
/** Whether currently loading more results */
isLoadingMore,
/** Whether there are more results available */
hasMore,
/** Load next page of results */
loadMore,
/** Load all remaining results (for filter/sort) */
loadAll,
/** Default page size (for display) */
pageSize: PAGE_SIZE,
}
}