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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
101 lines · 3.2 KB Raw History
// Whether the account's PDS serves permissioned spaces, which is what a
// private repository is made of.
//
// `community.lexicon.service.describe` is the only way to ask. atproto
// publishes no method list, so the alternative, calling a space method and
// reading the failure, cannot tell a server without the feature apart from
// one that is broken. A PDS that does not answer describe reads as serving
// none, which is the right answer for every PDS without spaces anyway.
//
// A yes is kept for an hour, a no for five minutes. A server gains spaces by
// redeploying, not by telling a page, and a stale no would keep telling the
// owner they cannot make a private repository long after it turned on.

import { account } from './git.js';

const DESCRIBE = 'community.lexicon.service.describe';
const TIMEOUT_MS = 5000;
const YES_TTL_MS = 60 * 60 * 1000;
const NO_TTL_MS = 5 * 60 * 1000;
const KEY = 'git-ui:space-support';

/** The space methods a private repository needs, in the order a page uses them. */
const REQUIRED = [
  'com.atproto.space.createSpace',
  'com.atproto.space.createRecord',
];

/** Every method name in a describe answer, or null when it is not one. */
async function fetchMethods() {
  let res;
  try {
    res = await fetch(`${account.pds}/xrpc/${DESCRIBE}`, {
      signal: AbortSignal.timeout(TIMEOUT_MS),
    });
  } catch {
    return null;
  }
  if (!res.ok) return null;
  /** @type {{methods?: Array<{value?: unknown}>}|null} */
  const body = await res.json().catch(() => null);
  if (!Array.isArray(body?.methods)) return null;
  return new Set(
    body.methods
      .map((entry) => entry.value)
      .filter((value) => typeof value === 'string'),
  );
}

/** The probe that reads a cache in hand, then asks the PDS. */
/** @type {Promise<boolean>|null} */
let pending = null;
/** @type {boolean|null} */
let cached = null;
let cachedAt = 0;

/**
 * Whether the account's PDS serves the space methods a private repository
 * needs. One answer per PDS per visit, from the cache or a fresh probe.
 * @returns {Promise<boolean>}
 */
export function spaceSupport() {
  if (pending) return pending;
  const now = Date.now();
  const ttl = cached ? YES_TTL_MS : NO_TTL_MS;
  if (cached !== null && now - cachedAt < ttl) {
    return Promise.resolve(cached);
  }
  try {
    const stored = JSON.parse(localStorage.getItem(KEY) || 'null');
    if (stored && typeof stored.supported === 'boolean') {
      const age = now - (stored.at || 0);
      if (age < (stored.supported ? YES_TTL_MS : NO_TTL_MS)) {
        cached = stored.supported;
        cachedAt = stored.at;
        return Promise.resolve(/** @type {boolean} */ (cached));
      }
    }
  } catch {
    // Blocked or malformed storage reads as no cache.
  }
  const probing = fetchMethods()
    .then((methods) => {
      if (!methods) return false;
      return REQUIRED.every((name) => methods.has(name));
    })
    .then((supported) => {
      cached = supported;
      cachedAt = Date.now();
      try {
        localStorage.setItem(KEY, JSON.stringify({ supported, at: cachedAt }));
      } catch {
        // A page that cannot write storage probes again next time.
      }
      return supported;
    });
  void probing.finally(() => {
    pending = null;
  });
  pending = probing;
  return probing;
}