From 8386ad54c5559fa632926ee6eae117620cd35cc7 Mon Sep 17 00:00:00 2001 From: Bailey Townsend Date: Wed, 4 Feb 2026 21:15:18 -0600 Subject: [PATCH 1/3] fix: getting tangled stats from our backend --- app/composables/useRepoMeta.ts | 35 ++-------- .../tangled-stats/[owner]/[...repo].ts | 64 +++++++++++++++++++ 2 files changed, 69 insertions(+), 30 deletions(-) create mode 100644 server/api/atproto/tangled-stats/[owner]/[...repo].ts diff --git a/app/composables/useRepoMeta.ts b/app/composables/useRepoMeta.ts index ea004a21a..7cfe3846f 100644 --- a/app/composables/useRepoMeta.ts +++ b/app/composables/useRepoMeta.ts @@ -566,43 +566,18 @@ const tangledAdapter: ProviderAdapter = { }, async fetchMeta(cachedFetch, ref, links, options = {}) { - // 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 { data: html } = await cachedFetch( - `https://tangled.org/${ref.owner}/${ref.repo}`, - { - headers: { 'User-Agent': 'npmx', 'Accept': 'text/html', ...options.headers }, - ...options, - }, + const { data } = await cachedFetch<{ stars: number; forks: number }>( + `/api/atproto/tangled-stats/${ref.owner}/${ref.repo}`, + options, REPO_META_TTL, ) - // Extracts the at-uri used in atproto - const atUriMatch = html.match(/data-star-subject-at="([^"]+)"/) - // Extract star count from: hx-post="/star?subject=...&countHint=23" - const starMatch = html.match(/countHint=(\d+)/) - //We'll set the stars from tangled's repo page and may override it with constellation if successful - let stars = starMatch?.[1] ? parseInt(starMatch[1], 10) : 0 - let forks = 0 - const atUri = atUriMatch?.[1] - - if (atUri) { - try { - const constellation = new Constellation(cachedFetch) - //Get counts of records that reference this repo in the atmosphere using constellation - const { data: allLinks } = await constellation.getAllLinks(atUri) - stars = allLinks.links['sh.tangled.feed.star']?.['.subject']?.distinct_dids ?? stars - forks = allLinks.links['sh.tangled.repo']?.['.source']?.distinct_dids ?? stars - } catch { - //failing silently since this is just an enhancement to the information already showing - } - } return { provider: 'tangled', url: links.repo, - stars, - forks, + stars: data.stars, + forks: data.forks, links, } } catch { diff --git a/server/api/atproto/tangled-stats/[owner]/[...repo].ts b/server/api/atproto/tangled-stats/[owner]/[...repo].ts new file mode 100644 index 000000000..34c0b4341 --- /dev/null +++ b/server/api/atproto/tangled-stats/[owner]/[...repo].ts @@ -0,0 +1,64 @@ +import type { CachedFetchFunction } from '#shared/utils/fetch-cache-config' + +export default defineEventHandler(async event => { + let owner = getRouterParam(event, 'owner') + let repo = getRouterParam(event, 'repo') + + let cachedFetch: CachedFetchFunction + if (event.context.cachedFetch) { + cachedFetch = event.context.cachedFetch + } else { + // Fallback: return a function that uses regular $fetch + // (shouldn't happen in normal operation) + cachedFetch = async ( + url: string, + options: Parameters[1] = {}, + _ttl?: number, + ): Promise> => { + const data = (await $fetch(url, options)) as T + return { data, isStale: false, cachedAt: null } + } + } + + try { + // 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) + const { data: html } = await cachedFetch( + `https://tangled.org/${owner}/${repo}`, + { + headers: { 'User-Agent': 'npmx', 'Accept': 'text/html' }, + }, + 60 * 10, + ) + // Extracts the at-uri used in atproto + const atUriMatch = html.match(/data-star-subject-at="([^"]+)"/) + // Extract star count from: hx-post="/star?subject=...&countHint=23" + const starMatch = html.match(/countHint=(\d+)/) + //We'll set the stars from tangled's repo page and may override it with constellation if successful + let stars = starMatch?.[1] ? parseInt(starMatch[1], 10) : 0 + let forks = 0 + const atUri = atUriMatch?.[1] + + if (atUri) { + try { + const constellation = new Constellation(cachedFetch) + //Get counts of records that reference this repo in the atmosphere using constellation + const { data: allLinks } = await constellation.getAllLinks(atUri) + stars = allLinks.links['sh.tangled.feed.star']?.['.subject']?.distinct_dids ?? stars + forks = allLinks.links['sh.tangled.repo']?.['.source']?.distinct_dids ?? stars + } catch { + //failing silently since this is just an enhancement to the information already showing + } + } + + return { + stars, + forks, + } + } catch { + return { + stars: 0, + forks: 0, + } + } +}) From 21760758d514d88d45394af5423dd72997ffc649 Mon Sep 17 00:00:00 2001 From: Bailey Townsend Date: Wed, 4 Feb 2026 21:27:59 -0600 Subject: [PATCH 2/3] rabbit fix --- server/api/atproto/tangled-stats/[owner]/[...repo].ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/api/atproto/tangled-stats/[owner]/[...repo].ts b/server/api/atproto/tangled-stats/[owner]/[...repo].ts index 34c0b4341..53fd4984b 100644 --- a/server/api/atproto/tangled-stats/[owner]/[...repo].ts +++ b/server/api/atproto/tangled-stats/[owner]/[...repo].ts @@ -45,7 +45,7 @@ export default defineEventHandler(async event => { //Get counts of records that reference this repo in the atmosphere using constellation const { data: allLinks } = await constellation.getAllLinks(atUri) stars = allLinks.links['sh.tangled.feed.star']?.['.subject']?.distinct_dids ?? stars - forks = allLinks.links['sh.tangled.repo']?.['.source']?.distinct_dids ?? stars + forks = allLinks.links['sh.tangled.repo']?.['.source']?.distinct_dids ?? 0 } catch { //failing silently since this is just an enhancement to the information already showing } From d3e96983148f79833929d870d378a21b51faba38 Mon Sep 17 00:00:00 2001 From: Bailey Townsend Date: Thu, 5 Feb 2026 07:04:39 -0600 Subject: [PATCH 3/3] found a cache event handler --- .../tangled-stats/[owner]/[...repo].ts | 115 +++++++++--------- 1 file changed, 60 insertions(+), 55 deletions(-) diff --git a/server/api/atproto/tangled-stats/[owner]/[...repo].ts b/server/api/atproto/tangled-stats/[owner]/[...repo].ts index 53fd4984b..0b8e698c6 100644 --- a/server/api/atproto/tangled-stats/[owner]/[...repo].ts +++ b/server/api/atproto/tangled-stats/[owner]/[...repo].ts @@ -1,64 +1,69 @@ import type { CachedFetchFunction } from '#shared/utils/fetch-cache-config' -export default defineEventHandler(async event => { - let owner = getRouterParam(event, 'owner') - let repo = getRouterParam(event, 'repo') +export default defineCachedEventHandler( + async event => { + let owner = getRouterParam(event, 'owner') + let repo = getRouterParam(event, 'repo') - let cachedFetch: CachedFetchFunction - if (event.context.cachedFetch) { - cachedFetch = event.context.cachedFetch - } else { - // Fallback: return a function that uses regular $fetch - // (shouldn't happen in normal operation) - cachedFetch = async ( - url: string, - options: Parameters[1] = {}, - _ttl?: number, - ): Promise> => { - const data = (await $fetch(url, options)) as T - return { data, isStale: false, cachedAt: null } + let cachedFetch: CachedFetchFunction + if (event.context.cachedFetch) { + cachedFetch = event.context.cachedFetch + } else { + // Fallback: return a function that uses regular $fetch + // (shouldn't happen in normal operation) + cachedFetch = async ( + url: string, + options: Parameters[1] = {}, + _ttl?: number, + ): Promise> => { + const data = (await $fetch(url, options)) as T + return { data, isStale: false, cachedAt: null } + } } - } - try { - // 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) - const { data: html } = await cachedFetch( - `https://tangled.org/${owner}/${repo}`, - { - headers: { 'User-Agent': 'npmx', 'Accept': 'text/html' }, - }, - 60 * 10, - ) - // Extracts the at-uri used in atproto - const atUriMatch = html.match(/data-star-subject-at="([^"]+)"/) - // Extract star count from: hx-post="/star?subject=...&countHint=23" - const starMatch = html.match(/countHint=(\d+)/) - //We'll set the stars from tangled's repo page and may override it with constellation if successful - let stars = starMatch?.[1] ? parseInt(starMatch[1], 10) : 0 - let forks = 0 - const atUri = atUriMatch?.[1] + try { + // 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) + const { data: html } = await cachedFetch( + `https://tangled.org/${owner}/${repo}`, + { + headers: { 'User-Agent': 'npmx', 'Accept': 'text/html' }, + }, + CACHE_MAX_AGE_ONE_MINUTE * 10, + ) + // Extracts the at-uri used in atproto + const atUriMatch = html.match(/data-star-subject-at="([^"]+)"/) + // Extract star count from: hx-post="/star?subject=...&countHint=23" + const starMatch = html.match(/countHint=(\d+)/) + //We'll set the stars from tangled's repo page and may override it with constellation if successful + let stars = starMatch?.[1] ? parseInt(starMatch[1], 10) : 0 + let forks = 0 + const atUri = atUriMatch?.[1] - if (atUri) { - try { - const constellation = new Constellation(cachedFetch) - //Get counts of records that reference this repo in the atmosphere using constellation - const { data: allLinks } = await constellation.getAllLinks(atUri) - stars = allLinks.links['sh.tangled.feed.star']?.['.subject']?.distinct_dids ?? stars - forks = allLinks.links['sh.tangled.repo']?.['.source']?.distinct_dids ?? 0 - } catch { - //failing silently since this is just an enhancement to the information already showing + if (atUri) { + try { + const constellation = new Constellation(cachedFetch) + //Get counts of records that reference this repo in the atmosphere using constellation + const { data: allLinks } = await constellation.getAllLinks(atUri) + stars = allLinks.links['sh.tangled.feed.star']?.['.subject']?.distinct_dids ?? stars + forks = allLinks.links['sh.tangled.repo']?.['.source']?.distinct_dids ?? 0 + } catch { + //failing silently since this is just an enhancement to the information already showing + } } - } - return { - stars, - forks, - } - } catch { - return { - stars: 0, - forks: 0, + return { + stars, + forks, + } + } catch { + return { + stars: 0, + forks: 0, + } } - } -}) + }, + { + maxAge: CACHE_MAX_AGE_ONE_MINUTE * 10, + }, +)