[READ-ONLY] a fast, modern browser for the npm registry

feat: constellation ๐ŸŒŒ client (#474)

Co-authored-by: Daniel Roe <daniel@roe.dev>

authored by baileytownsend.dev

Daniel Roe and committed by
GitHub
79371925 2dc73cf1

+164 -52
-13
app/composables/useCachedFetch.ts
··· 1 1 import type { CachedFetchResult } from '#shared/utils/fetch-cache-config' 2 2 3 3 /** 4 - * Type for the cachedFetch function attached to event context. 5 - */ 6 - export type CachedFetchFunction = <T = unknown>( 7 - url: string, 8 - options?: { 9 - method?: string 10 - body?: unknown 11 - headers?: Record<string, string> 12 - }, 13 - ttl?: number, 14 - ) => Promise<CachedFetchResult<T>> 15 - 16 - /** 17 4 * Get the cachedFetch function from the current request context. 18 5 * 19 6 * IMPORTANT: This must be called in the composable setup context (outside of
+1 -1
app/composables/useNpmRegistry.ts
··· 12 12 import { maxSatisfying, prerelease, major, minor, diff, gt, compare } from 'semver' 13 13 import { isExactVersion } from '~/utils/versions' 14 14 import { extractInstallScriptsInfo } from '~/utils/install-scripts' 15 - import type { CachedFetchFunction } from '~/composables/useCachedFetch' 15 + import type { CachedFetchFunction } from '#shared/utils/fetch-cache-config' 16 16 17 17 const NPM_REGISTRY = 'https://registry.npmjs.org' 18 18 const NPM_API = 'https://api.npmjs.org'
+3 -21
app/composables/useRepoMeta.ts
··· 1 1 import type { ProviderId, RepoRef } from '#shared/utils/git-providers' 2 2 import { parseRepoUrl, GITLAB_HOSTS } from '#shared/utils/git-providers' 3 - import type { CachedFetchFunction } from '~/composables/useCachedFetch' 4 3 5 4 // TTL for git repo metadata (10 minutes - repo stats don't change frequently) 6 5 const REPO_META_TTL = 60 * 10 ··· 84 83 delegates?: Array<{ id: string; alias?: string }> 85 84 patches?: { open: number; draft: number; archived: number; merged: number } 86 85 issues?: { open: number; closed: number } 87 - } 88 - 89 - /** microcosm's constellation API response for /links/all to get tangled.org stats */ 90 - type ConstellationAllLinksResponse = { 91 - links: Record< 92 - string, 93 - Record< 94 - string, 95 - { 96 - records: number 97 - distinct_dids: number 98 - } 99 - > 100 - > 101 86 } 102 87 103 88 type ProviderAdapter = { ··· 597 582 let forks = 0 598 583 const atUri = atUriMatch?.[1] 599 584 600 - if (atUriMatch) { 585 + if (atUri) { 601 586 try { 587 + const constellation = new Constellation(cachedFetch) 602 588 //Get counts of records that reference this repo in the atmosphere using constellation 603 - const { data: allLinks } = await cachedFetch<ConstellationAllLinksResponse>( 604 - `https://constellation.microcosm.blue/links/all?target=${atUri}`, 605 - { headers: { 'User-Agent': 'npmx' } }, 606 - REPO_META_TTL, 607 - ) 589 + const { data: allLinks } = await constellation.getAllLinks(atUri) 608 590 stars = allLinks.links['sh.tangled.feed.star']?.['.subject']?.distinct_dids ?? stars 609 591 forks = allLinks.links['sh.tangled.repo']?.['.source']?.distinct_dids ?? stars 610 592 } catch {
+31
lexicons/dev/npmx/feed/like.json
··· 1 + { 2 + "defs": { 3 + "main": { 4 + "description": "A like of a package on npmx", 5 + "key": "tid", 6 + "record": { 7 + "properties": { 8 + "createdAt": { 9 + "format": "datetime", 10 + "type": "string" 11 + }, 12 + "subject": { 13 + "description": "A strong reference to the dev.npmx.package record. If the package does not have a record in an atproto repo, this is not included.", 14 + "type": "ref", 15 + "ref": "com.atproto.repo.strongRef" 16 + }, 17 + "subjectRef": { 18 + "description": "The npmx URL to the package to allow for counting of packages that do not have a record in an atproto repo.", 19 + "type": "string", 20 + "format": "uri" 21 + } 22 + }, 23 + "required": ["createdAt", "subjectRef"], 24 + "type": "object" 25 + }, 26 + "type": "record" 27 + } 28 + }, 29 + "id": "dev.npmx.feed.like", 30 + "lexicon": 1 31 + }
+2 -2
server/api/auth/atproto.get.ts
··· 2 2 import { NodeOAuthClient } from '@atproto/oauth-client-node' 3 3 import { createError, getQuery, sendRedirect } from 'h3' 4 4 import { useOAuthStorage } from '#server/utils/atproto/storage' 5 - import { SLINGSHOT_ENDPOINT } from '#shared/utils/constants' 5 + import { SLINGSHOT_HOST } from '#shared/utils/constants' 6 6 import type { UserSession } from '#shared/schemas/userSession' 7 7 8 8 export default defineEventHandler(async event => { ··· 53 53 }) 54 54 55 55 const response = await fetch( 56 - `${SLINGSHOT_ENDPOINT}/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${agent.did}`, 56 + `https://${SLINGSHOT_HOST}/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${agent.did}`, 57 57 { headers: { 'User-Agent': 'npmx' } }, 58 58 ) 59 59 const miniDoc = (await response.json()) as UserSession
-10
server/plugins/fetch-cache.ts
··· 41 41 return parts.join(':') 42 42 } 43 43 44 - export type CachedFetchFunction = <T = unknown>( 45 - url: string, 46 - options?: { 47 - method?: string 48 - body?: unknown 49 - headers?: Record<string, string> 50 - }, 51 - ttl?: number, 52 - ) => Promise<CachedFetchResult<T>> 53 - 54 44 /** 55 45 * Server plugin that attaches a cachedFetch function to the event context. 56 46 * This allows app composables to access the cached fetch via useRequestEvent().
+2 -2
shared/utils/constants.ts
··· 21 21 export const ERROR_SUGGESTIONS_FETCH_FAILED = 'Failed to fetch suggestions.' 22 22 23 23 // microcosm services 24 - export const CONSTELLATION_ENDPOINT = 'https://constellation.microcosm.blue' 25 - export const SLINGSHOT_ENDPOINT = 'https://slingshot.microcosm.blue' 24 + export const CONSTELLATION_HOST = 'constellation.microcosm.blue' 25 + export const SLINGSHOT_HOST = 'slingshot.microcosm.blue' 26 26 27 27 // Theming 28 28 export const ACCENT_COLORS = {
+109
shared/utils/constellation.ts
··· 1 + import { CONSTELLATION_HOST } from '#shared/utils/constants' 2 + import type { CachedFetchFunction } from './fetch-cache-config' 3 + 4 + export type Backlink = { 5 + did: string 6 + collection: string 7 + rkey: string 8 + } 9 + 10 + export type BacklinksResponse = { 11 + total: number 12 + records: Backlink[] 13 + cursor: string | undefined 14 + } 15 + 16 + export type LinksDistinctDidsResponse = { 17 + total: number 18 + linking_dids: string[] 19 + cursor: string | undefined 20 + } 21 + 22 + export type AllLinksResponse = { 23 + links: Record< 24 + string, 25 + Record< 26 + string, 27 + { 28 + records: number 29 + distinct_dids: number 30 + } 31 + > 32 + > 33 + } 34 + 35 + const HEADERS = { 'User-Agent': 'npmx' } 36 + 37 + /** @public */ 38 + export class Constellation { 39 + private readonly cachedFetch: CachedFetchFunction 40 + constructor(fetch: CachedFetchFunction) { 41 + this.cachedFetch = fetch 42 + } 43 + 44 + /** 45 + * Gets backlinks from constellation 46 + * https://constellation.microcosm.blue/xrpc/blue.microcosm.links.getBacklinks?subject=at%3A%2F%2Fdid%3Aplc%3Aa4pqq234yw7fqbddawjo7y35%2Fapp.bsky.feed.post%2F3m237ilwc372e&source=app.bsky.feed.like%3Asubject.uri&limit=16 47 + * @param subject - A uri encoded link. did, url, or at-uri 48 + * @param collection - The lexicon collection to check like dev.npmx.feed.like 49 + * @param recordPath - Where in the record to check for the subject 50 + * @param limit - The number of backlinks to return 51 + * @param cursor - The cursor to use for pagination 52 + * @param reverse - Whether to reverse the order of the results 53 + * @param filterByDids - An array of dids to filter by in the results 54 + * @param ttl - The ttl to use for the cache 55 + */ 56 + async getBackLinks( 57 + subject: string, 58 + collection: string, 59 + recordPath: string, 60 + limit = 16, 61 + cursor?: string, 62 + reverse = false, 63 + filterByDids: [string][] = [], 64 + ttl: number | undefined = undefined, 65 + ) { 66 + const source = encodeURIComponent(`${collection}:${recordPath}`) 67 + let urlToCall = `https://${CONSTELLATION_HOST}/xrpc/blue.microcosm.links.getBacklinks?subject=${encodeURIComponent(subject)}&source=${source}&limit=${limit}` 68 + if (cursor) urlToCall += `&cursor=${cursor}` 69 + if (reverse) urlToCall += '&reverse=true' 70 + filterByDids.forEach(did => (urlToCall += `&did=${did}`)) 71 + 72 + return await this.cachedFetch<BacklinksResponse>(urlToCall, { headers: HEADERS }, ttl) 73 + } 74 + 75 + /** 76 + * Gets the distinct dids that link to a target record 77 + * @param target - A uri encoded link. did, url, or at-uri 78 + * @param collection - The lexicon collection to check like dev.npmx.feed.like 79 + * @param recordPath - Where in the record to check for the subject 80 + * @param limit - The number of distinct dids to return 81 + * @param cursor - The cursor to use for pagination 82 + * @param ttl - The ttl to use for the cache 83 + */ 84 + async getLinksDistinctDids( 85 + target: string, 86 + collection: string, 87 + recordPath: string, 88 + limit: number = 16, 89 + cursor?: string, 90 + ttl: number | undefined = undefined, 91 + ) { 92 + let urlToCall = `https://${CONSTELLATION_HOST}/links/distinct-dids?target=${encodeURIComponent(target)}&collection=${collection}&path=${recordPath}&limit=${limit}` 93 + if (cursor) urlToCall += `&cursor=${cursor}` 94 + return await this.cachedFetch<LinksDistinctDidsResponse>(urlToCall, { headers: HEADERS }, ttl) 95 + } 96 + 97 + /** 98 + * Gets all links from constellation and their counts 99 + * @param target - A uri encoded link. did, url, or at-uri 100 + * @param ttl - The ttl to use for the cache 101 + */ 102 + async getAllLinks(target: string, ttl: number | undefined = undefined) { 103 + return await this.cachedFetch<AllLinksResponse>( 104 + `https://${CONSTELLATION_HOST}/links/all?target=${target}`, 105 + { headers: HEADERS }, 106 + ttl, 107 + ) 108 + } 109 + }
+16 -3
shared/utils/fetch-cache-config.ts
··· 5 5 * using Nitro's storage layer (backed by Vercel's runtime cache in production). 6 6 */ 7 7 8 - import { CONSTELLATION_ENDPOINT, SLINGSHOT_ENDPOINT } from './constants' 8 + import { CONSTELLATION_HOST, SLINGSHOT_HOST } from './constants' 9 9 10 10 /** 11 11 * Domains that should have their fetch responses cached. ··· 27 27 'codeberg.org', // Codeberg (Gitea-based) 28 28 'gitee.com', // Gitee API 29 29 // microcosm endpoints for atproto data 30 - CONSTELLATION_ENDPOINT, 31 - SLINGSHOT_ENDPOINT, 30 + CONSTELLATION_HOST, 31 + SLINGSHOT_HOST, 32 32 ] as const 33 33 34 34 /** ··· 99 99 /** Unix timestamp when the data was cached, or null if fresh fetch */ 100 100 cachedAt: number | null 101 101 } 102 + 103 + /** 104 + * Type for the cachedFetch function attached to event context. 105 + */ 106 + export type CachedFetchFunction = <T = unknown>( 107 + url: string, 108 + options?: { 109 + method?: string 110 + body?: unknown 111 + headers?: Record<string, string> 112 + }, 113 + ttl?: number, 114 + ) => Promise<CachedFetchResult<T>>