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
13 changes: 0 additions & 13 deletions app/composables/useCachedFetch.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,5 @@
import type { CachedFetchResult } from '#shared/utils/fetch-cache-config'

/**
* Type for the cachedFetch function attached to event context.
*/
export type CachedFetchFunction = <T = unknown>(
url: string,
options?: {
method?: string
body?: unknown
headers?: Record<string, string>
},
ttl?: number,
) => Promise<CachedFetchResult<T>>

/**
* Get the cachedFetch function from the current request context.
*
Expand Down
2 changes: 1 addition & 1 deletion app/composables/useNpmRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type { ReleaseType } from 'semver'
import { maxSatisfying, prerelease, major, minor, diff, gt, compare } from 'semver'
import { isExactVersion } from '~/utils/versions'
import { extractInstallScriptsInfo } from '~/utils/install-scripts'
import type { CachedFetchFunction } from '~/composables/useCachedFetch'
import type { CachedFetchFunction } from '#shared/utils/fetch-cache-config'

const NPM_REGISTRY = 'https://registry.npmjs.org'
const NPM_API = 'https://api.npmjs.org'
Expand Down
24 changes: 3 additions & 21 deletions app/composables/useRepoMeta.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { ProviderId, RepoRef } from '#shared/utils/git-providers'
import { parseRepoUrl, GITLAB_HOSTS } from '#shared/utils/git-providers'
import type { CachedFetchFunction } from '~/composables/useCachedFetch'

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

/** microcosm's constellation API response for /links/all to get tangled.org stats */
type ConstellationAllLinksResponse = {
links: Record<
string,
Record<
string,
{
records: number
distinct_dids: number
}
>
>
}

type ProviderAdapter = {
id: ProviderId
parse(url: URL): RepoRef | null
Expand Down Expand Up @@ -597,14 +582,11 @@ const tangledAdapter: ProviderAdapter = {
let forks = 0
const atUri = atUriMatch?.[1]

if (atUriMatch) {
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 cachedFetch<ConstellationAllLinksResponse>(
`https://constellation.microcosm.blue/links/all?target=${atUri}`,
{ headers: { 'User-Agent': 'npmx' } },
REPO_META_TTL,
)
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 {
Expand Down
31 changes: 31 additions & 0 deletions lexicons/dev/npmx/feed/like.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"defs": {
"main": {
"description": "A like of a package on npmx",
"key": "tid",
"record": {
"properties": {
"createdAt": {
"format": "datetime",
"type": "string"
},
"subject": {
"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.",
"type": "ref",
"ref": "com.atproto.repo.strongRef"
},
"subjectRef": {
"description": "The npmx URL to the package to allow for counting of packages that do not have a record in an atproto repo.",
"type": "string",
"format": "uri"
}
},
"required": ["createdAt", "subjectRef"],
"type": "object"
},
"type": "record"
}
},
"id": "dev.npmx.feed.like",
"lexicon": 1
}
4 changes: 2 additions & 2 deletions server/api/auth/atproto.get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Agent } from '@atproto/api'
import { NodeOAuthClient } from '@atproto/oauth-client-node'
import { createError, getQuery, sendRedirect } from 'h3'
import { useOAuthStorage } from '#server/utils/atproto/storage'
import { SLINGSHOT_ENDPOINT } from '#shared/utils/constants'
import { SLINGSHOT_HOST } from '#shared/utils/constants'
import type { UserSession } from '#shared/schemas/userSession'

export default defineEventHandler(async event => {
Expand Down Expand Up @@ -53,7 +53,7 @@ export default defineEventHandler(async event => {
})

const response = await fetch(
`${SLINGSHOT_ENDPOINT}/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${agent.did}`,
`https://${SLINGSHOT_HOST}/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${agent.did}`,
{ headers: { 'User-Agent': 'npmx' } },
)
const miniDoc = (await response.json()) as UserSession
Expand Down
10 changes: 0 additions & 10 deletions server/plugins/fetch-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,6 @@ function generateFetchCacheKey(url: string | URL, method: string = 'GET', body?:
return parts.join(':')
}

export type CachedFetchFunction = <T = unknown>(
url: string,
options?: {
method?: string
body?: unknown
headers?: Record<string, string>
},
ttl?: number,
) => Promise<CachedFetchResult<T>>

/**
* Server plugin that attaches a cachedFetch function to the event context.
* This allows app composables to access the cached fetch via useRequestEvent().
Expand Down
4 changes: 2 additions & 2 deletions shared/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ export const UNSET_NUXT_SESSION_PASSWORD = 'NUXT_SESSION_PASSWORD not set'
export const ERROR_SUGGESTIONS_FETCH_FAILED = 'Failed to fetch suggestions.'

// microcosm services
export const CONSTELLATION_ENDPOINT = 'https://constellation.microcosm.blue'
export const SLINGSHOT_ENDPOINT = 'https://slingshot.microcosm.blue'
export const CONSTELLATION_HOST = 'constellation.microcosm.blue'
export const SLINGSHOT_HOST = 'slingshot.microcosm.blue'

// Theming
export const ACCENT_COLORS = {
Expand Down
109 changes: 109 additions & 0 deletions shared/utils/constellation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { CONSTELLATION_HOST } from '#shared/utils/constants'
import type { CachedFetchFunction } from './fetch-cache-config'

export type Backlink = {
did: string
collection: string
rkey: string
}

export type BacklinksResponse = {
total: number
records: Backlink[]
cursor: string | undefined
}

export type LinksDistinctDidsResponse = {
total: number
linking_dids: string[]
cursor: string | undefined
}

export type AllLinksResponse = {
links: Record<
string,
Record<
string,
{
records: number
distinct_dids: number
}
>
>
}

const HEADERS = { 'User-Agent': 'npmx' }

/** @public */
export class Constellation {
private readonly cachedFetch: CachedFetchFunction
constructor(fetch: CachedFetchFunction) {
this.cachedFetch = fetch
}

/**
* Gets backlinks from constellation
* 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
* @param subject - A uri encoded link. did, url, or at-uri
* @param collection - The lexicon collection to check like dev.npmx.feed.like
* @param recordPath - Where in the record to check for the subject
* @param limit - The number of backlinks to return
* @param cursor - The cursor to use for pagination
* @param reverse - Whether to reverse the order of the results
* @param filterByDids - An array of dids to filter by in the results
* @param ttl - The ttl to use for the cache
*/
async getBackLinks(
subject: string,
collection: string,
recordPath: string,
limit = 16,
cursor?: string,
reverse = false,
filterByDids: [string][] = [],
ttl: number | undefined = undefined,
) {
const source = encodeURIComponent(`${collection}:${recordPath}`)
let urlToCall = `https://${CONSTELLATION_HOST}/xrpc/blue.microcosm.links.getBacklinks?subject=${encodeURIComponent(subject)}&source=${source}&limit=${limit}`
if (cursor) urlToCall += `&cursor=${cursor}`
if (reverse) urlToCall += '&reverse=true'
filterByDids.forEach(did => (urlToCall += `&did=${did}`))

return await this.cachedFetch<BacklinksResponse>(urlToCall, { headers: HEADERS }, ttl)
}

/**
* Gets the distinct dids that link to a target record
* @param target - A uri encoded link. did, url, or at-uri
* @param collection - The lexicon collection to check like dev.npmx.feed.like
* @param recordPath - Where in the record to check for the subject
* @param limit - The number of distinct dids to return
* @param cursor - The cursor to use for pagination
* @param ttl - The ttl to use for the cache
*/
async getLinksDistinctDids(
target: string,
collection: string,
recordPath: string,
limit: number = 16,
cursor?: string,
ttl: number | undefined = undefined,
) {
let urlToCall = `https://${CONSTELLATION_HOST}/links/distinct-dids?target=${encodeURIComponent(target)}&collection=${collection}&path=${recordPath}&limit=${limit}`
if (cursor) urlToCall += `&cursor=${cursor}`
return await this.cachedFetch<LinksDistinctDidsResponse>(urlToCall, { headers: HEADERS }, ttl)
}

/**
* Gets all links from constellation and their counts
* @param target - A uri encoded link. did, url, or at-uri
* @param ttl - The ttl to use for the cache
*/
async getAllLinks(target: string, ttl: number | undefined = undefined) {
return await this.cachedFetch<AllLinksResponse>(
`https://${CONSTELLATION_HOST}/links/all?target=${target}`,
{ headers: HEADERS },
ttl,
)
}
}
19 changes: 16 additions & 3 deletions shared/utils/fetch-cache-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* using Nitro's storage layer (backed by Vercel's runtime cache in production).
*/

import { CONSTELLATION_ENDPOINT, SLINGSHOT_ENDPOINT } from './constants'
import { CONSTELLATION_HOST, SLINGSHOT_HOST } from './constants'

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

/**
Expand Down Expand Up @@ -99,3 +99,16 @@ export interface CachedFetchResult<T> {
/** Unix timestamp when the data was cached, or null if fresh fetch */
cachedAt: number | null
}

/**
* Type for the cachedFetch function attached to event context.
*/
export type CachedFetchFunction = <T = unknown>(
url: string,
options?: {
method?: string
body?: unknown
headers?: Record<string, string>
},
ttl?: number,
) => Promise<CachedFetchResult<T>>
Loading