chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
/**
* 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[]>} 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<string, unknown>} */ (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 () => [] });