~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
123 lines · 4.1 KB Raw History
/**
 * 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 () => [] });