chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
/**
* Issue threads, read from the statements in every voice's own repo.
*
* An issue is a record in its filer's repo, a reply is a record naming its
* parent, and the repository owner's disposition record is what a reader
* shows as the issue's state. Nothing here is a mutable field on a shared
* object: closed is a statement the owner signed, displayed as such, and the
* filer's disagreement can sit beside it.
*
* These are the parts of that reading that touch no network.
*
* Workers-clean: no node imports.
*/
/**
* The state each disposition names. A Map rather than an object, so a
* statement carrying `constructor` reads as the comment it is.
* @type {Map<string, 'open'|'resolved'|'declined'>}
*/
const STATE_OF = new Map([
['resolved', 'resolved'],
['declined', 'declined'],
['reopened', 'open'],
]);
/**
* The state an issue thread displays, which is the repository owner's latest
* word on it and nobody else's. Anyone may publish a disposition; anyone may
* read it; only the owner's names the state, because the repository is what
* the issue is about and the owner is who the repository answers to.
*
* The latest wins, so every word the owner says is reversible by saying
* another one. `reopened` is how they get back to the state a thread starts
* in, which no amount of `resolved` and `declined` can reach.
*
* The statement that names the state comes back with it, because when the
* owner said it is a fact a row wants and only this read knows which of the
* statements it was.
* @param {Array<Record<string, *>>} thread - every statement in one thread
* @param {string} ownerDid
* @returns {{state: 'open'|'resolved'|'declined',
* said?: Record<string, *>}}
*/
export function issueState(thread, ownerDid) {
let latest = null;
for (const statement of thread) {
if (statement.by?.did !== ownerDid) continue;
if (!STATE_OF.has(statement.disposition)) continue;
if (!latest || String(statement.createdAt) > String(latest.createdAt)) {
latest = statement;
}
}
if (!latest) return { state: 'open' };
return { state: STATE_OF.get(latest.disposition) ?? 'open', said: latest };
}
/**
* Group statements into threads: each opener with everything that answers
* it, however deep the reply chain, ordered oldest first inside a thread and
* newest activity first across threads.
* @param {Array<Record<string, *>>} statements
* @param {string} ownerDid
* @returns {Array<{root: Record<string, *>, thread: Array<Record<string, *>>,
* state: ReturnType<typeof issueState>, latest: string}>}
*/
export function issueThreads(statements, ownerDid) {
const byUri = new Map(statements.filter((s) => s.uri).map((s) => [s.uri, s]));
/** @param {Record<string, *>} statement */
const rootOf = (statement) => {
let here = statement;
for (let step = 0; step < 20; step++) {
const parent = here.replyTo?.uri && byUri.get(here.replyTo.uri);
if (!parent) return here;
here = parent;
}
return here;
};
/** @type {Map<Record<string, *>, Array<Record<string, *>>>} */
const groups = new Map();
for (const statement of statements) {
const root = rootOf(statement);
groups.set(root, [...(groups.get(root) ?? []), statement]);
}
const threads = [...groups.entries()].map(([root, thread]) => {
thread.sort((a, b) =>
String(a.createdAt ?? '').localeCompare(String(b.createdAt ?? '')),
);
return {
root,
thread,
state: issueState(thread, ownerDid),
latest: String(thread[thread.length - 1]?.createdAt ?? ''),
};
});
return threads.sort((a, b) => b.latest.localeCompare(a.latest));
}