chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
// 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;
}