Skip to content

Commit 7937192

Browse files
feat: constellation 🌌 client (#474)
Co-authored-by: Daniel Roe <daniel@roe.dev>
1 parent 2dc73cf commit 7937192

9 files changed

Lines changed: 164 additions & 52 deletions

File tree

‎app/composables/useCachedFetch.ts‎

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,5 @@
11
import type { CachedFetchResult } from '#shared/utils/fetch-cache-config'
22

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-
163
/**
174
* Get the cachedFetch function from the current request context.
185
*

‎app/composables/useNpmRegistry.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type { ReleaseType } from 'semver'
1212
import { maxSatisfying, prerelease, major, minor, diff, gt, compare } from 'semver'
1313
import { isExactVersion } from '~/utils/versions'
1414
import { extractInstallScriptsInfo } from '~/utils/install-scripts'
15-
import type { CachedFetchFunction } from '~/composables/useCachedFetch'
15+
import type { CachedFetchFunction } from '#shared/utils/fetch-cache-config'
1616

1717
const NPM_REGISTRY = 'https://registry.npmjs.org'
1818
const NPM_API = 'https://api.npmjs.org'

‎app/composables/useRepoMeta.ts‎

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { ProviderId, RepoRef } from '#shared/utils/git-providers'
22
import { parseRepoUrl, GITLAB_HOSTS } from '#shared/utils/git-providers'
3-
import type { CachedFetchFunction } from '~/composables/useCachedFetch'
43

54
// TTL for git repo metadata (10 minutes - repo stats don't change frequently)
65
const REPO_META_TTL = 60 * 10
@@ -86,20 +85,6 @@ type RadicleProjectResponse = {
8685
issues?: { open: number; closed: number }
8786
}
8887

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-
}
102-
10388
type ProviderAdapter = {
10489
id: ProviderId
10590
parse(url: URL): RepoRef | null
@@ -597,14 +582,11 @@ const tangledAdapter: ProviderAdapter = {
597582
let forks = 0
598583
const atUri = atUriMatch?.[1]
599584

600-
if (atUriMatch) {
585+
if (atUri) {
601586
try {
587+
const constellation = new Constellation(cachedFetch)
602588
//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)
608590
stars = allLinks.links['sh.tangled.feed.star']?.['.subject']?.distinct_dids ?? stars
609591
forks = allLinks.links['sh.tangled.repo']?.['.source']?.distinct_dids ?? stars
610592
} catch {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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+
}

‎server/api/auth/atproto.get.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Agent } from '@atproto/api'
22
import { NodeOAuthClient } from '@atproto/oauth-client-node'
33
import { createError, getQuery, sendRedirect } from 'h3'
44
import { useOAuthStorage } from '#server/utils/atproto/storage'
5-
import { SLINGSHOT_ENDPOINT } from '#shared/utils/constants'
5+
import { SLINGSHOT_HOST } from '#shared/utils/constants'
66
import type { UserSession } from '#shared/schemas/userSession'
77

88
export default defineEventHandler(async event => {
@@ -53,7 +53,7 @@ export default defineEventHandler(async event => {
5353
})
5454

5555
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}`,
5757
{ headers: { 'User-Agent': 'npmx' } },
5858
)
5959
const miniDoc = (await response.json()) as UserSession

‎server/plugins/fetch-cache.ts‎

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,6 @@ function generateFetchCacheKey(url: string | URL, method: string = 'GET', body?:
4141
return parts.join(':')
4242
}
4343

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-
5444
/**
5545
* Server plugin that attaches a cachedFetch function to the event context.
5646
* This allows app composables to access the cached fetch via useRequestEvent().

‎shared/utils/constants.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ export const UNSET_NUXT_SESSION_PASSWORD = 'NUXT_SESSION_PASSWORD not set'
2121
export const ERROR_SUGGESTIONS_FETCH_FAILED = 'Failed to fetch suggestions.'
2222

2323
// 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'
2626

2727
// Theming
2828
export const ACCENT_COLORS = {
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
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+
}

‎shared/utils/fetch-cache-config.ts‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* using Nitro's storage layer (backed by Vercel's runtime cache in production).
66
*/
77

8-
import { CONSTELLATION_ENDPOINT, SLINGSHOT_ENDPOINT } from './constants'
8+
import { CONSTELLATION_HOST, SLINGSHOT_HOST } from './constants'
99

1010
/**
1111
* Domains that should have their fetch responses cached.
@@ -27,8 +27,8 @@ export const FETCH_CACHE_ALLOWED_DOMAINS = [
2727
'codeberg.org', // Codeberg (Gitea-based)
2828
'gitee.com', // Gitee API
2929
// microcosm endpoints for atproto data
30-
CONSTELLATION_ENDPOINT,
31-
SLINGSHOT_ENDPOINT,
30+
CONSTELLATION_HOST,
31+
SLINGSHOT_HOST,
3232
] as const
3333

3434
/**
@@ -99,3 +99,16 @@ export interface CachedFetchResult<T> {
9999
/** Unix timestamp when the data was cached, or null if fresh fetch */
100100
cachedAt: number | null
101101
}
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>>

0 commit comments

Comments
 (0)