chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
// The directory behind the source view's left pane.
//
// Both halves of the view want the same listing: the pane lists it, and a
// directory's own page reads it again to find the README. One query key
// serves both, so opening a file beside a listing costs no second walk.
import { creditAuthors } from '@pdsjs/git/authors';
import { useQuery } from '@tanstack/react-query';
import { ACTIVITY_WINDOW_MS } from './activity.js';
import { pullCommits } from './collab.js';
import {
account,
isEmpty,
lastCommitOf,
readerFor,
repoRecord,
} from './git.js';
/** Deep enough to count and date every commit a repository of this shape holds. */
const COUNT_LIMIT = 50000;
/**
* The directory holding a path. A path with no slash is at the root, whose
* parent is the root itself.
* @param {string} path
* @returns {string}
*/
export function parentOf(path) {
const cut = path.lastIndexOf('/');
return cut === -1 ? '' : path.slice(0, cut);
}
/**
* One directory's entries at one ref. An empty repository has none, and asks
* for nothing.
* @param {string} repo
* @param {string} refName
* @param {string} path - the directory, '' for the root
* @param {string} [space] - the space holding a private repository
*/
export function useDirectory(repo, refName, path, space) {
const record = repoRecord(repo, space);
return useQuery({
queryKey: directoryKey(repo, refName, path, space),
enabled: Boolean(record) && !isEmpty(record),
queryFn: () => readDirectory(repo, refName, path, space),
});
}
/**
* Every file at one ref, with its size, as one listing. The rail draws the
* whole tree from it and the language gauge counts it, so one read serves
* both for as long as the ref does.
* @param {string} repo
* @param {string} refName
* @param {string} [space] - the space holding a private repository
*/
export function useFileList(repo, refName, space) {
const record = repoRecord(repo, space);
return useQuery({
queryKey: ['files', repo, refName, space ?? ''],
enabled: Boolean(record) && !isEmpty(record),
queryFn: async () => {
const listing = await readerFor(repo, undefined, space).listFiles(
account.did,
repo,
refName,
);
return listing?.files ?? [];
},
staleTime: Number.POSITIVE_INFINITY,
});
}
/**
* The last commit to change one file. The answer holds for as long as the
* ref does, so one read serves every visit to the file in a session.
* @param {string} repo
* @param {string} refName
* @param {string} path
* @param {string} [space] - the space holding a private repository
*/
export function useFileCommit(repo, refName, path, space) {
const record = repoRecord(repo, space);
return useQuery({
queryKey: ['file-commit', repo, refName, path, space ?? ''],
enabled: Boolean(record) && !isEmpty(record) && Boolean(path),
queryFn: () => lastCommitOf(repo, refName, path, space),
staleTime: Number.POSITIVE_INFINITY,
});
}
/**
* The tip of a ref, how far its history runs, and when its recent commits
* were made. The walk reads every commit, which on an indexed chain is a
* download of its own, so a caller paints first and fills this in.
*
* Three readers want the same answer: the latest-commit line, the commit
* count beside it, and the activity gauge. One key serves them all.
* @param {string} repo
* @param {string} refName
* @param {string} [space] - the space holding a private repository
*/
export function useHistory(repo, refName, space) {
const record = repoRecord(repo, space);
return useQuery({
queryKey: historyKey(repo, refName, space),
enabled: Boolean(record) && !isEmpty(record),
queryFn: () => readHistory(repo, refName, space),
});
}
/**
* @param {string} repo @param {string} refName @param {string} path
* @param {string} [space]
*/
export const directoryKey = (repo, refName, path, space) => [
'dir',
repo,
refName,
path,
space ?? '',
];
/**
* @param {string} repo @param {string} refName @param {string} path
* @param {string} [space]
*/
export async function readDirectory(repo, refName, path, space) {
const listing = await readerFor(repo, undefined, space).listTree(
account.did,
repo,
refName,
path,
);
return listing?.entries ?? [];
}
/**
* @param {string} repo @param {string} refName @param {string} [space]
*/
export const historyKey = (repo, refName, space) => [
'history',
repo,
refName,
space ?? '',
];
/**
* The tip of a ref, how far its history runs, and when its recent commits
* were made.
* @param {string} repo @param {string} refName @param {string} [space]
*/
export async function readHistory(repo, refName, space) {
const log = await readerFor(repo, undefined, space).listCommits(
account.did,
repo,
refName,
COUNT_LIMIT,
);
const floor = Date.now() - ACTIVITY_WINDOW_MS;
/** @type {number[]} */
const dates = [];
const commits = log?.commits ?? [];
for (const commit of commits) {
if (commit.committedAt >= floor) dates.push(commit.committedAt);
}
return {
head: commits[0] ?? null,
count: commits.length,
truncated: Boolean(log?.truncated),
dates,
authors: creditAuthors(commits),
};
}
/**
* @param {string} repo @param {string} refName @param {string} path
* @param {string} [space]
*/
export const fileKey = (repo, refName, path, space) => [
'file',
repo,
refName,
path,
space ?? '',
];
/**
* One file at a ref, with its bytes where it is an image.
* @param {string} repo @param {string} refName @param {string} path
* @param {string} [space]
*/
export async function readSource(repo, refName, path, space) {
const reader = readerFor(repo, undefined, space);
const file = await reader.readFile(account.did, repo, refName, path);
const image = file?.mediaType
? await reader.readImage(account.did, repo, refName, path)
: null;
return { file, image };
}
/**
* @param {string} repo @param {string|undefined} refName
* @param {string|undefined} range @param {string|undefined} from
* @param {string} [space] @param {string} [path]
*/
export const logKey = (repo, refName, range, from, space, path) => [
'commits',
repo,
refName ?? '',
range ?? '',
from ?? '',
space ?? '',
path ?? '',
];
/**
* A history: a ref's, one path's along it, or the commits across a range in
* whichever copy holds them.
* @param {string} repo
* @param {{refName?: string, range?: string, from?: string, limit: number, space?: string, path?: string}} what
*/
export async function readLog(
repo,
{ refName, range, from, limit, space, path },
) {
if (!range) {
return readerFor(repo, undefined, space).listCommits(
account.did,
repo,
refName,
limit,
path,
);
}
const [base, tip] = range.split('..');
return pullCommits(repo, base || null, tip, from ?? '', limit);
}