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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
239 lines · 6.7 KB Raw History
// 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);
}