/** * Discovery: which records, in which accounts, are about one repository. * * A pull request record, a review and an issue each name the repository at * `subject.uri` and live in their author's own repo. atproto keeps no reverse * index, so the question "who has written about this repository" has no * answer on the repository's own server. This port answers it. Whoever * answers can add noise or omit; they cannot forge a word, because the caller * reads each record from its author. * * Workers-clean: no node imports. */ /** * One record about the subject, by its address. * @typedef {Object} About * @property {string} did * @property {string} collection * @property {string} rkey */ /** * @typedef {Object} DiscoveryPort * @property {(subjectUri: string, collection: string, path?: string) => Promise} about - * every record of the collection naming the subject at the path, which is * `subject.uri` unless given. Empty when the provider cannot answer, so a * caller falls back on what it can list directly. */ /** The public instance. */ export const CONSTELLATION_URL = 'https://constellation.microcosm.blue'; /** The most an index answers per page, by its own contract. */ const PAGE_LIMIT = 100; /** Pages read per question before the answer is taken as it stands. */ const MAX_PAGES = 10; const READ_TIMEOUT_MS = 5000; /** * A discovery port over Constellation, a hosted reverse index that walks * every record on the public firehose and indexes each link by its target, * the linking record's collection, and the JSON path to the link. One query * per collection and path. * * Nothing written into a space reaches the firehose, so the index knows * nothing of a repository held in one. * @param {Object} [options] * @param {string} [options.service] - the index to ask * @param {typeof fetch} [options.fetch] - what carries the request; the global by default * @param {string} [options.userAgent] - who is asking, which the public instance asks for * @param {number} [options.timeoutMs] * @param {number} [options.maxPages] * @returns {DiscoveryPort} */ export function createConstellationDiscovery({ service = CONSTELLATION_URL, fetch: carrier, userAgent = 'pds.js (@pdsjs/git)', timeoutMs = READ_TIMEOUT_MS, maxPages = MAX_PAGES, } = {}) { const base = service.replace(/\/+$/, ''); const send = carrier ?? ((input, init) => globalThis.fetch(input, init)); return { async about(subjectUri, collection, path = 'subject.uri') { /** @type {About[]} */ const found = []; let cursor = ''; for (let page = 0; page < maxPages; page++) { const params = new URLSearchParams({ subject: subjectUri, source: `${collection}:${path}`, limit: String(PAGE_LIMIT), }); if (cursor) params.set('cursor', cursor); /** @type {{records?: unknown[], cursor?: unknown}} */ let body; try { const res = await send( `${base}/xrpc/blue.microcosm.links.getBacklinks?${params}`, { headers: { 'User-Agent': userAgent }, signal: AbortSignal.timeout(timeoutMs), }, ); if (!res.ok) return found; body = await res.json(); } catch { // An index that does not answer costs the strangers, not the list. return found; } for (const entry of body.records ?? []) { const { did, collection: from, rkey, } = /** @type {Record} */ (entry ?? {}); if ( typeof did !== 'string' || typeof from !== 'string' || typeof rkey !== 'string' ) { continue; } found.push({ did, collection: from, rkey }); } cursor = typeof body.cursor === 'string' ? body.cursor : ''; if (!cursor) break; } return found; }, }; } /** * A discovery port that finds nothing, for a server with no index to ask. * A caller then lists only what the repository's config names. * @returns {DiscoveryPort} */ export const noDiscovery = () => ({ about: async () => [] });