Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 20 additions & 8 deletions app/composables/useCachedFetch.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { CachedFetchResult } from '#shared/utils/fetch-cache-config'

/**
* Type for the cachedFetch function attached to event context.
*/
Expand All @@ -9,14 +11,19 @@ export type CachedFetchFunction = <T = unknown>(
headers?: Record<string, string>
},
ttl?: number,
) => Promise<T>
) => Promise<CachedFetchResult<T>>

/**
* Get the cachedFetch function from the current request context.
*
* IMPORTANT: This must be called in the composable setup context (outside of
* useAsyncData handlers). The returned function can then be used inside handlers.
*
* The returned function returns a wrapper object with staleness metadata:
* - `data`: The response data
* - `isStale`: Whether the data came from stale cache
* - `cachedAt`: Unix timestamp when cached, or null if fresh fetch
*
* @example
* ```ts
* export function usePackage(name: MaybeRefOrGetter<string>) {
Expand All @@ -25,15 +32,18 @@ export type CachedFetchFunction = <T = unknown>(
*
* return useLazyAsyncData(
* () => `package:${toValue(name)}`,
* // Use it inside the handler
* () => cachedFetch<Packument>(`https://registry.npmjs.org/${toValue(name)}`)
* // Use it inside the handler - destructure { data } or { data, isStale }
* async () => {
* const { data } = await cachedFetch<Packument>(`https://registry.npmjs.org/${toValue(name)}`)
* return data
* }
* )
* }
* ```
* @public
*/
export function useCachedFetch(): CachedFetchFunction {
// On client, return a function that just uses $fetch
// On client, return a function that just uses $fetch (no caching, not stale)
if (import.meta.client) {
return async <T = unknown>(
url: string,
Expand All @@ -43,8 +53,9 @@ export function useCachedFetch(): CachedFetchFunction {
headers?: Record<string, string>
} = {},
_ttl?: number,
): Promise<T> => {
return (await $fetch(url, options as Parameters<typeof $fetch>[1])) as T
): Promise<CachedFetchResult<T>> => {
const data = (await $fetch(url, options as Parameters<typeof $fetch>[1])) as T
return { data, isStale: false, cachedAt: null }
}
}

Expand All @@ -67,7 +78,8 @@ export function useCachedFetch(): CachedFetchFunction {
headers?: Record<string, string>
} = {},
_ttl?: number,
): Promise<T> => {
return (await $fetch(url, options as Parameters<typeof $fetch>[1])) as T
): Promise<CachedFetchResult<T>> => {
const data = (await $fetch(url, options as Parameters<typeof $fetch>[1])) as T
return { data, isStale: false, cachedAt: null }
}
}
59 changes: 45 additions & 14 deletions app/composables/useNpmRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,17 +183,25 @@ export function usePackage(
) {
const cachedFetch = useCachedFetch()

return useLazyAsyncData(
const asyncData = useLazyAsyncData(
() => `package:${toValue(name)}:${toValue(requestedVersion) ?? ''}`,
async () => {
const encodedName = encodePackageName(toValue(name))
const r = await cachedFetch<Packument>(`${NPM_REGISTRY}/${encodedName}`)
const { data: r, isStale } = await cachedFetch<Packument>(`${NPM_REGISTRY}/${encodedName}`)
const reqVer = toValue(requestedVersion)
const pkg = transformPackument(r, reqVer)
const resolvedVersion = getResolvedVersion(pkg, reqVer)
return { ...pkg, resolvedVersion }
return { ...pkg, resolvedVersion, isStale }
},
)

if (import.meta.client && asyncData.data.value?.isStale) {
onMounted(() => {
asyncData.refresh()
})
}

return asyncData
}

function getResolvedVersion(pkg: SlimPackument, reqVer?: string | null): string | null {
Expand Down Expand Up @@ -223,15 +231,24 @@ export function usePackageDownloads(
) {
const cachedFetch = useCachedFetch()

return useLazyAsyncData(
const asyncData = useLazyAsyncData(
() => `downloads:${toValue(name)}:${toValue(period)}`,
async () => {
const encodedName = encodePackageName(toValue(name))
return await cachedFetch<NpmDownloadCount>(
const { data, isStale } = await cachedFetch<NpmDownloadCount>(
`${NPM_API}/downloads/point/${toValue(period)}/${encodedName}`,
)
return { ...data, isStale }
},
)

if (import.meta.client && asyncData.data.value?.isStale) {
onMounted(() => {
asyncData.refresh()
})
}

return asyncData
}

type NpmDownloadsRangeResponse = {
Expand Down Expand Up @@ -260,6 +277,7 @@ export async function fetchNpmDownloadsRange(
const emptySearchResponse = {
objects: [],
total: 0,
isStale: false,
time: new Date().toISOString(),
} satisfies NpmSearchResponse

Expand Down Expand Up @@ -305,7 +323,7 @@ export function useNpmSearch(
// Use requested size for initial fetch
params.set('size', String(opts.size ?? 25))

const response = await cachedFetch<NpmSearchResponse>(
const { data: response, isStale } = await cachedFetch<NpmSearchResponse>(
`${NPM_REGISTRY}/-/v1/search?${params.toString()}`,
{},
60,
Expand All @@ -317,7 +335,7 @@ export function useNpmSearch(
total: response.total,
}

return response
return { ...response, isStale }
},
{ default: () => lastSearch || emptySearchResponse },
)
Expand Down Expand Up @@ -357,7 +375,7 @@ export function useNpmSearch(
params.set('size', String(size))
params.set('from', String(from))

const response = await cachedFetch<NpmSearchResponse>(
const { data: response } = await cachedFetch<NpmSearchResponse>(
`${NPM_REGISTRY}/-/v1/search?${params.toString()}`,
{},
60,
Expand Down Expand Up @@ -405,6 +423,7 @@ export function useNpmSearch(
const data = computed<NpmSearchResponse | null>(() => {
if (cache.value) {
return {
isStale: false,
objects: cache.value.objects,
total: cache.value.total,
time: new Date().toISOString(),
Expand All @@ -413,6 +432,12 @@ export function useNpmSearch(
return asyncData.data.value
})

if (import.meta.client && asyncData.data.value?.isStale) {
onMounted(() => {
asyncData.refresh()
})
}

// Whether there are more results available on the server (incremental mode only)
const hasMore = computed(() => {
if (!cache.value) return true
Expand Down Expand Up @@ -482,7 +507,7 @@ function packumentToSearchResult(pkg: MinimalPackument, weeklyDownloads?: number
export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
const cachedFetch = useCachedFetch()

return useLazyAsyncData(
const asyncData = useLazyAsyncData(
() => `org-packages:${toValue(orgName)}`,
async () => {
const org = toValue(orgName)
Expand All @@ -493,7 +518,7 @@ export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
// Get all package names in the org
let packageNames: string[]
try {
const data = await cachedFetch<Record<string, string>>(
const { data } = await cachedFetch<Record<string, string>>(
`${NPM_REGISTRY}/-/org/${encodeURIComponent(org)}/package`,
)
packageNames = Object.keys(data)
Expand Down Expand Up @@ -526,7 +551,10 @@ export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
batch.map(async name => {
try {
const encoded = encodePackageName(name)
return await cachedFetch<MinimalPackument>(`${NPM_REGISTRY}/${encoded}`)
const { data: pkg } = await cachedFetch<MinimalPackument>(
`${NPM_REGISTRY}/${encoded}`,
)
return pkg
} catch {
return null
}
Expand All @@ -551,13 +579,16 @@ export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
)

return {
isStale: false,
objects: results,
total: results.length,
time: new Date().toISOString(),
} satisfies NpmSearchResponse
},
{ default: () => emptySearchResponse },
)

return asyncData
}

// ============================================================================
Expand Down Expand Up @@ -665,9 +696,9 @@ async function checkDependencyOutdated(
if (cached) {
packument = await cached
} else {
const promise = cachedFetch<Packument>(
`${NPM_REGISTRY}/${encodePackageName(packageName)}`,
).catch(() => null)
const promise = cachedFetch<Packument>(`${NPM_REGISTRY}/${encodePackageName(packageName)}`)
.then(({ data }) => data)
.catch(() => null)
packumentCache.set(packageName, promise)
packument = await promise
}
Expand Down
28 changes: 18 additions & 10 deletions app/composables/useRepoMeta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,11 +145,12 @@ const githubAdapter: ProviderAdapter = {
// Using UNGH to avoid API limitations of the Github API
let res: UnghRepoResponse | null = null
try {
res = await cachedFetch<UnghRepoResponse>(
const { data } = await cachedFetch<UnghRepoResponse>(
`https://ungh.cc/repos/${ref.owner}/${ref.repo}`,
{ headers: { 'User-Agent': 'npmx' } },
REPO_META_TTL,
)
res = data
} catch {
return null
}
Expand Down Expand Up @@ -210,11 +211,12 @@ const gitlabAdapter: ProviderAdapter = {
const projectPath = encodeURIComponent(`${ref.owner}/${ref.repo}`)
let res: GitLabProjectResponse | null = null
try {
res = await cachedFetch<GitLabProjectResponse>(
const { data } = await cachedFetch<GitLabProjectResponse>(
`https://${baseHost}/api/v4/projects/${projectPath}`,
{ headers: { 'User-Agent': 'npmx' } },
REPO_META_TTL,
)
res = data
} catch {
return null
}
Expand Down Expand Up @@ -265,11 +267,12 @@ const bitbucketAdapter: ProviderAdapter = {
async fetchMeta(cachedFetch, ref, links) {
let res: BitbucketRepoResponse | null = null
try {
res = await cachedFetch<BitbucketRepoResponse>(
const { data } = await cachedFetch<BitbucketRepoResponse>(
`https://api.bitbucket.org/2.0/repositories/${ref.owner}/${ref.repo}`,
{ headers: { 'User-Agent': 'npmx' } },
REPO_META_TTL,
)
res = data
} catch {
return null
}
Expand Down Expand Up @@ -322,11 +325,12 @@ const codebergAdapter: ProviderAdapter = {
async fetchMeta(cachedFetch, ref, links) {
let res: GiteaRepoResponse | null = null
try {
res = await cachedFetch<GiteaRepoResponse>(
const { data } = await cachedFetch<GiteaRepoResponse>(
`https://codeberg.org/api/v1/repos/${ref.owner}/${ref.repo}`,
{ headers: { 'User-Agent': 'npmx' } },
REPO_META_TTL,
)
res = data
} catch {
return null
}
Expand Down Expand Up @@ -379,11 +383,12 @@ const giteeAdapter: ProviderAdapter = {
async fetchMeta(cachedFetch, ref, links) {
let res: GiteeRepoResponse | null = null
try {
res = await cachedFetch<GiteeRepoResponse>(
const { data } = await cachedFetch<GiteeRepoResponse>(
`https://gitee.com/api/v5/repos/${ref.owner}/${ref.repo}`,
{ headers: { 'User-Agent': 'npmx' } },
REPO_META_TTL,
)
res = data
} catch {
return null
}
Expand Down Expand Up @@ -469,11 +474,12 @@ const giteaAdapter: ProviderAdapter = {
// so caching may not apply for self-hosted instances
let res: GiteaRepoResponse | null = null
try {
res = await cachedFetch<GiteaRepoResponse>(
const { data } = await cachedFetch<GiteaRepoResponse>(
`https://${ref.host}/api/v1/repos/${ref.owner}/${ref.repo}`,
{ headers: { 'User-Agent': 'npmx' } },
REPO_META_TTL,
)
res = data
} catch {
return null
}
Expand Down Expand Up @@ -577,7 +583,7 @@ const tangledAdapter: ProviderAdapter = {
// Tangled doesn't have a public JSON API, but we can scrape the star count
// from the HTML page (it's in the hx-post URL as countHint=N)
try {
const html = await cachedFetch<string>(
const { data: html } = await cachedFetch<string>(
`https://tangled.org/${ref.owner}/${ref.repo}`,
{ headers: { 'User-Agent': 'npmx', 'Accept': 'text/html' } },
REPO_META_TTL,
Expand All @@ -594,7 +600,7 @@ const tangledAdapter: ProviderAdapter = {
if (atUriMatch) {
try {
//Get counts of records that reference this repo in the atmosphere using constellation
const allLinks = await cachedFetch<ConstellationAllLinksResponse>(
const { data: allLinks } = await cachedFetch<ConstellationAllLinksResponse>(
`https://constellation.microcosm.blue/links/all?target=${atUri}`,
{ headers: { 'User-Agent': 'npmx' } },
REPO_META_TTL,
Expand Down Expand Up @@ -655,11 +661,12 @@ const radicleAdapter: ProviderAdapter = {
async fetchMeta(cachedFetch, ref, links) {
let res: RadicleProjectResponse | null = null
try {
res = await cachedFetch<RadicleProjectResponse>(
const { data } = await cachedFetch<RadicleProjectResponse>(
`https://seed.radicle.at/api/v1/projects/${ref.repo}`,
{ headers: { 'User-Agent': 'npmx' } },
REPO_META_TTL,
)
res = data
} catch {
return null
}
Expand Down Expand Up @@ -720,11 +727,12 @@ const forgejoAdapter: ProviderAdapter = {

let res: GiteaRepoResponse | null = null
try {
res = await cachedFetch<GiteaRepoResponse>(
const { data } = await cachedFetch<GiteaRepoResponse>(
`https://${ref.host}/api/v1/repos/${ref.owner}/${ref.repo}`,
{ headers: { 'User-Agent': 'npmx' } },
REPO_META_TTL,
)
res = data
} catch {
return null
}
Expand Down
Loading
Loading