chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
import {
createRootRouteWithContext,
createRoute,
createRouter,
} from '@tanstack/react-router';
import { useEffect, useRef, useState } from 'react';
import { App } from '#/app.jsx';
import { Link } from '#/components/atoms/link.jsx';
import { Skeleton } from '#/components/atoms/skeleton.jsx';
import { PageNotice } from '#/components/molecules/page-notice.jsx';
import { RepoPageSkeleton } from '#/components/molecules/repo-page-skeleton.jsx';
import { loadAccount, primeOnce } from '#/lib/account.js';
import { loadAuthors } from '#/lib/authors.js';
import { loadChecks } from '#/lib/checks.js';
import {
loadPulls,
PULL_COMMIT_LIMIT,
parsePullPath,
pullCommits,
pullStatements,
} from '#/lib/collab.js';
import {
account,
defaultRef,
isEmpty,
loadRepoSummary,
readChange,
repoRecord,
spaceOf,
splitRefPath,
} from '#/lib/git.js';
import { loadIssueStatements } from '#/lib/issues.js';
import { useNavigate, useRepoRecord, useSpace } from '#/lib/navigation.jsx';
import { clearRenewal, completeSignIn } from '#/lib/oauth.js';
import { loadPackages } from '#/lib/packages.js';
import { useSession } from '#/lib/session.jsx';
import { publishedPackages } from '#/lib/settings.js';
import {
directoryKey,
fileKey,
historyKey,
logKey,
readDirectory,
readHistory,
readLog,
readSource,
} from '#/lib/source.js';
import { CheckPage } from '#/pages/check.jsx';
import { ChecksPage } from '#/pages/checks.jsx';
import { CommitPage } from '#/pages/commit.jsx';
import { CommitsPage } from '#/pages/commits.jsx';
import { FilePage } from '#/pages/file.jsx';
import { IssuePage } from '#/pages/issue.jsx';
import { NewIssuePage } from '#/pages/issue-new.jsx';
import { IssuesPage } from '#/pages/issues.jsx';
import { PackagesPage } from '#/pages/packages.jsx';
import { PullPage } from '#/pages/pull.jsx';
import { PullsPage } from '#/pages/pulls.jsx';
import { NewRepoPage } from '#/pages/repo-new.jsx';
import { ReposPage } from '#/pages/repos.jsx';
import { SettingsPage } from '#/pages/settings.jsx';
import { TreePage } from '#/pages/tree.jsx';
/**
* The routes, and what each one reads before it renders.
*
* A route's loader warms the query the page then subscribes to, under the
* same key, so the page renders with the answer in hand and a link starts
* the read when the pointer settles on it. The page keeps its own query:
* that is what re-renders it when a publish invalidates the key.
*
* The account gate stays in the shell. The last visit's account and
* repository rows are read synchronously there, so a loader below can ask
* the server for a repository on the first frame.
*/
/** @typedef {{queryClient: import('@tanstack/react-query').QueryClient}} RouterContext */
const rootRoute = createRootRouteWithContext()({
component: App,
// Every loader below asks the account's server, so the server has to be
// known first. Primed from the last visit, that is immediate and the fresh
// discovery runs behind the paint; on a first visit the routes wait for it.
beforeLoad: async ({ context }) => {
const query = {
queryKey: ['account'],
queryFn: () => loadAccount(context.queryClient),
};
if (primeOnce()) {
void context.queryClient.prefetchQuery(query);
return;
}
await context.queryClient.ensureQueryData(query);
},
});
/**
* Where the authorization server sends the browser back. The code is spent
* once, so the address bar is cleaned before anything else renders: a reload
* of this URL would otherwise present a code the server has already refused.
*/
function CallbackPage() {
const navigate = useNavigate();
const { setSession } = useSession();
const [error, setError] = useState(null);
// The exchange consumes the pending sign-in the moment it starts, so it
// must run once however many times the effect fires. StrictMode fires it
// twice in development, and a second run would find nothing pending and
// report a failure over a sign-in that succeeded. The ref survives the
// simulated remount; a cleanup-cancelled flag would not, and would also
// cancel the first run's redirect.
const ran = useRef(false);
useEffect(() => {
if (ran.current) return;
ran.current = true;
// Read from the address bar rather than from a prop: the code is spent
// once and this effect must not re-run, so it depends on nothing a render
// makes anew.
completeSignIn(new URLSearchParams(window.location.search))
.then(({ session, returnTo }) => {
clearRenewal();
setSession(session);
navigate(returnTo);
})
.catch((err) => setError(err.message));
}, [navigate, setSession]);
if (error) {
return (
<PageNotice tone="error">
{error}
<Link href="/" className="mt-3 block text-[13px]">
Start again
</Link>
</PageNotice>
);
}
return (
<div className="min-h-0 flex-1 overflow-y-auto px-(--gutter)">
<Skeleton className="mx-auto mt-16 h-24 max-w-md" />
</div>
);
}
/**
* Warm one query. What a loader does: the page subscribes to the same key.
* @param {RouterContext} context
* @param {unknown[]} queryKey
* @param {() => Promise<unknown>} queryFn
*/
const warm = ({ queryClient }, queryKey, queryFn) =>
queryClient.ensureQueryData({ queryKey, queryFn });
/**
* Start one query without waiting on it. The route renders at once and the
* page's own query, on the same key, shows its placeholder and streams the
* answer in. A loader that computes from the result still awaits; one that
* only fills the page's cache does not.
* @param {RouterContext} context
* @param {unknown[]} queryKey
* @param {() => Promise<unknown>} queryFn
*/
const prefetch = ({ queryClient }, queryKey, queryFn) =>
void queryClient.prefetchQuery({ queryKey, queryFn });
/**
* The DID whose copy an address names, where it names one, and the rest.
* A commit on a contributor's branch is addressed by whose copy holds it,
* so this account's own commits keep the shorter address they always had.
* @param {string} splat
*/
function copyOf(splat) {
const parts = splat.split('/').filter(Boolean);
const from = parts[0]?.startsWith('did:') ? parts[0] : '';
return { from, rest: from ? parts.slice(1) : parts };
}
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
loader: ({ context }) => prefetch(context, ['repo-summary'], loadRepoSummary),
component: ReposPage,
});
const callbackRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/callback',
component: CallbackPage,
});
/** Creating a repository, a page of its own like the file-issue one. */
const newRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/new',
component: NewRepoPage,
});
/**
* One repository. Every section below reads its refs out of the record, and
* a name the account does not hold has no sections. A private repository is
* addressed by its name like any other; the listing says which space holds
* it, and every read below takes the space from there.
*/
const repoRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/$repo',
loader: ({ context }) => prefetch(context, ['authors'], loadAuthors),
});
/**
* What a directory listing shows: its entries and the ref's history, which
* the latest-commit line and the gauges read. An empty repository has
* neither and asks for nothing.
* @param {RouterContext} context
* @param {string} repo @param {string} refName @param {string} path
* @param {string} space
*/
function warmTree(context, repo, refName, path, space) {
const record = repoRecord(repo, space);
if (!record || isEmpty(record)) return;
prefetch(context, directoryKey(repo, refName, path, space), () =>
readDirectory(repo, refName, path, space),
);
prefetch(context, historyKey(repo, refName, space), () =>
readHistory(repo, refName, space),
);
}
const treeIndexRoute = createRoute({
getParentRoute: () => repoRoute,
path: '/',
loader: ({ context, params }) => {
const space = spaceOf(params.repo);
const record = repoRecord(params.repo, space);
if (!record) return;
warmTree(context, params.repo, defaultRef(record), '', space);
},
component: function TreeIndex() {
const { repo } = treeIndexRoute.useParams();
const space = useSpace();
const { record, settled } = useRepoRecord(repo, space);
if (!record) return settled ? <NoSuchRepository /> : <RepoPageSkeleton />;
return (
<TreePage
repo={repo}
refName={defaultRef(record)}
path=""
space={space}
/>
);
},
});
/**
* A ref name may hold slashes, so where the ref ends and the path begins is
* read against the repository's own refs.
*/
const treeRoute = createRoute({
getParentRoute: () => repoRoute,
path: '/tree/$',
loader: ({ context, params }) => {
const space = spaceOf(params.repo);
const record = repoRecord(params.repo, space);
if (!record) return;
const { ref, path } = splitRefPath(record, params._splat ?? '');
warmTree(context, params.repo, ref, path, space);
},
component: function Tree() {
const { repo, _splat } = treeRoute.useParams();
const space = useSpace();
const { record, settled } = useRepoRecord(repo, space);
if (!record) return settled ? <NoSuchRepository /> : <RepoPageSkeleton />;
const { ref, path } = splitRefPath(record, _splat ?? '');
return <TreePage repo={repo} refName={ref} path={path} space={space} />;
},
});
const blobRoute = createRoute({
getParentRoute: () => repoRoute,
path: '/blob/$',
loader: ({ context, params }) => {
const space = spaceOf(params.repo);
const record = repoRecord(params.repo, space);
if (!record) return;
const { ref, path } = splitRefPath(record, params._splat ?? '');
prefetch(context, fileKey(params.repo, ref, path, space), () =>
readSource(params.repo, ref, path, space),
);
},
component: function Blob() {
const { repo, _splat } = blobRoute.useParams();
const space = useSpace();
const { record, settled } = useRepoRecord(repo, space);
if (!record) return settled ? <NoSuchRepository /> : <RepoPageSkeleton />;
const { ref, path } = splitRefPath(record, _splat ?? '');
return <FilePage repo={repo} refName={ref} path={path} space={space} />;
},
});
/**
* A history: a ref's, or the commits across a range, in this account's copy
* or another's. A segment carrying `..` is a range rather than a branch,
* since git forbids `..` in a ref name.
*/
const commitsRoute = createRoute({
getParentRoute: () => repoRoute,
path: '/commits/$',
loader: ({ context, params }) => {
const space = spaceOf(params.repo);
const record = repoRecord(params.repo, space);
if (!record || isEmpty(record)) return;
const { from, rest } = copyOf(params._splat ?? '');
const tail = rest.join('/');
const split = tail.includes('..') ? null : splitRefPath(record, tail);
const what = split
? { refName: split.ref, path: split.path }
: { range: tail, from: from || undefined };
prefetch(
context,
logKey(
params.repo,
what.refName,
what.range,
what.from,
space,
what.path,
),
() => readLog(params.repo, { ...what, limit: PULL_COMMIT_LIMIT, space }),
);
},
component: function Commits() {
const { repo, _splat } = commitsRoute.useParams();
const space = useSpace();
const { record, settled } = useRepoRecord(repo, space);
if (!record) return settled ? <NoSuchRepository /> : <RepoPageSkeleton />;
const { from, rest } = copyOf(_splat ?? '');
const tail = rest.join('/');
if (tail.includes('..')) {
return <CommitsPage repo={repo} range={tail} from={from} space={space} />;
}
const { ref, path } = splitRefPath(record, tail);
return <CommitsPage repo={repo} refName={ref} path={path} space={space} />;
},
});
const commitsIndexRoute = createRoute({
getParentRoute: () => repoRoute,
path: '/commits',
loader: ({ context, params }) => {
const space = spaceOf(params.repo);
const record = repoRecord(params.repo, space);
if (!record || isEmpty(record)) return;
const refName = splitRefPath(record, '').ref;
prefetch(
context,
logKey(params.repo, refName, undefined, undefined, space),
() => readLog(params.repo, { refName, limit: PULL_COMMIT_LIMIT, space }),
);
},
component: function CommitsIndex() {
const { repo } = commitsIndexRoute.useParams();
const space = useSpace();
const { record, settled } = useRepoRecord(repo, space);
if (!record) return settled ? <NoSuchRepository /> : <RepoPageSkeleton />;
return (
<CommitsPage
repo={repo}
refName={splitRefPath(record, '').ref}
space={space}
/>
);
},
});
const commitRoute = createRoute({
getParentRoute: () => repoRoute,
path: '/commit/$',
loader: ({ context, params }) => {
const space = spaceOf(params.repo);
if (!repoRecord(params.repo, space)) return;
if (isEmpty(repoRecord(params.repo, space))) return;
const { from, rest } = copyOf(params._splat ?? '');
const sha = rest.join('/');
prefetch(context, ['commit', params.repo, sha, from, space], () =>
readChange(params.repo, sha, from, space),
);
},
component: function Commit() {
const { repo, _splat } = commitRoute.useParams();
const space = useSpace();
const { record, settled } = useRepoRecord(repo, space);
if (!record) return settled ? <NoSuchRepository /> : <RepoPageSkeleton />;
const { from, rest } = copyOf(_splat ?? '');
return (
<CommitPage repo={repo} sha={rest.join('/')} from={from} space={space} />
);
},
});
const checksRoute = createRoute({
getParentRoute: () => repoRoute,
path: '/checks',
loader: ({ context, params }) => {
const space = spaceOf(params.repo);
prefetch(context, ['checks', params.repo, space], () =>
loadChecks(params.repo, space),
);
},
component: function Checks() {
const { repo } = checksRoute.useParams();
const space = useSpace();
const { record, settled } = useRepoRecord(repo, space);
if (!record) return settled ? <NoSuchRepository /> : <RepoPageSkeleton />;
return <ChecksPage repo={repo} space={space} />;
},
});
const checkRoute = createRoute({
getParentRoute: () => repoRoute,
path: '/checks/$rkey',
loader: ({ context, params }) => {
const space = spaceOf(params.repo);
prefetch(context, ['checks', params.repo, space], () =>
loadChecks(params.repo, space),
);
},
component: function Check() {
const { repo, rkey } = checkRoute.useParams();
const space = useSpace();
const { record, settled } = useRepoRecord(repo, space);
if (!record) return settled ? <NoSuchRepository /> : <RepoPageSkeleton />;
return <CheckPage repo={repo} rkey={rkey} space={space} />;
},
});
const pullsRoute = createRoute({
getParentRoute: () => repoRoute,
path: '/pulls',
loader: ({ context, params }) => {
const space = spaceOf(params.repo);
prefetch(context, ['pulls', params.repo, space], () =>
loadPulls(params.repo, space),
);
},
component: function Pulls() {
const { repo } = pullsRoute.useParams();
const space = useSpace();
const { record, settled } = useRepoRecord(repo, space);
if (!record) return settled ? <NoSuchRepository /> : <RepoPageSkeleton />;
return <PullsPage repo={repo} tab="" space={space} />;
},
});
const pullsTabRoute = createRoute({
getParentRoute: () => repoRoute,
path: '/pulls/$tab',
loader: ({ context, params }) => {
const space = spaceOf(params.repo);
prefetch(context, ['pulls', params.repo, space], () =>
loadPulls(params.repo, space),
);
},
component: function PullsTab() {
const { repo, tab } = pullsTabRoute.useParams();
const space = useSpace();
const { record, settled } = useRepoRecord(repo, space);
if (!record) return settled ? <NoSuchRepository /> : <RepoPageSkeleton />;
return <PullsPage repo={repo} tab={tab} space={space} />;
},
});
/**
* A pull request is addressed by the account whose copy holds the work and
* the branch, so the address survives the rebases that move its commits.
* parsePullPath reads that address, beside the diffHref that writes it.
*
* The list names the pull request, so the loader reads the list and then
* warms what its page shows: the diff across it, its commits, and the
* statements about it, together rather than one after the other. None of it
* holds the render: the page paints its placeholder and each answer lands
* in it, the way every other route streams.
*/
const pullRoute = createRoute({
getParentRoute: () => repoRoute,
path: '/pull/$',
loader: ({ context, params }) => {
const { repo } = params;
const space = spaceOf(params.repo);
const { did, branch, commit } = parsePullPath(
(params._splat ?? '').split('/').filter(Boolean),
);
void warm(context, ['pulls', repo, space], () => loadPulls(repo, space))
.then((listed) => {
const pulls = /** @type {import('#/lib/collab.js').Pulls} */ (listed);
const pull = [...pulls.open, ...pulls.merged].find(
(one) => one.author.did === did && one.branch === branch,
);
if (!pull) return;
const from = pull.author.did === account.did ? '' : pull.author.did;
const sha =
commit || (pull.base ? `${pull.base}..${pull.sha}` : pull.sha);
prefetch(context, ['commit', repo, sha, from, space], () =>
readChange(repo, sha, from, space),
);
if (!commit) {
prefetch(context, ['commits', repo, '', sha, from, space], () =>
pullCommits(repo, pull.base, pull.sha, from, 50),
);
}
prefetch(context, ['statements', repo, space], () =>
pullStatements(repo, space),
);
})
.catch(() => {
// The page's own query reports the failure where the reader is.
});
},
component: function Pull() {
const { repo, _splat } = pullRoute.useParams();
const space = useSpace();
const { record, settled } = useRepoRecord(repo, space);
if (!record) return settled ? <NoSuchRepository /> : <RepoPageSkeleton />;
const { did, branch, tab, commit } = parsePullPath(
(_splat ?? '').split('/').filter(Boolean),
);
return (
<PullPage
repo={repo}
did={did}
branch={branch}
tab={tab}
commit={commit}
space={space}
/>
);
},
});
const issuesRoute = createRoute({
getParentRoute: () => repoRoute,
path: '/issues',
loader: ({ context, params }) => {
const space = spaceOf(params.repo);
prefetch(context, ['issues', params.repo, space], () =>
loadIssueStatements(params.repo, space),
);
},
component: function Issues() {
const { repo } = issuesRoute.useParams();
const space = useSpace();
const { record, settled } = useRepoRecord(repo, space);
if (!record) return settled ? <NoSuchRepository /> : <RepoPageSkeleton />;
return <IssuesPage repo={repo} tab="" space={space} />;
},
});
/**
* Filing is a page of its own. A DID is never the word `new`, so this and a
* thread cannot be mistaken for each other; it is listed before the tab
* route so the word is not read as a tab.
*/
const issueNewRoute = createRoute({
getParentRoute: () => repoRoute,
path: '/issues/new',
component: function IssueNew() {
const { repo } = issueNewRoute.useParams();
const space = useSpace();
const { record, settled } = useRepoRecord(repo, space);
if (!record) return settled ? <NoSuchRepository /> : <RepoPageSkeleton />;
return <NewIssuePage repo={repo} space={space} />;
},
});
const issuesTabRoute = createRoute({
getParentRoute: () => repoRoute,
path: '/issues/$tab',
loader: ({ context, params }) => {
const space = spaceOf(params.repo);
prefetch(context, ['issues', params.repo, space], () =>
loadIssueStatements(params.repo, space),
);
},
component: function IssuesTab() {
const { repo, tab } = issuesTabRoute.useParams();
const space = useSpace();
const { record, settled } = useRepoRecord(repo, space);
if (!record) return settled ? <NoSuchRepository /> : <RepoPageSkeleton />;
return <IssuesPage repo={repo} tab={tab} space={space} />;
},
});
/**
* An issue thread is addressed by its opener: whose repo holds it, and the
* record key there. The pull requests are read too, for the ones whose
* commits claim to fix it.
*/
const issueRoute = createRoute({
getParentRoute: () => repoRoute,
path: '/issues/$did/$rkey',
loader: ({ context, params }) => {
const space = spaceOf(params.repo);
prefetch(context, ['issues', params.repo, space], () =>
loadIssueStatements(params.repo, space),
);
prefetch(context, ['pulls', params.repo, space], () =>
loadPulls(params.repo, space),
);
},
component: function Issue() {
const { repo, did, rkey } = issueRoute.useParams();
const space = useSpace();
const { record, settled } = useRepoRecord(repo, space);
if (!record) return settled ? <NoSuchRepository /> : <RepoPageSkeleton />;
return <IssuePage repo={repo} did={did} rkey={rkey} space={space} />;
},
});
const packagesRoute = createRoute({
getParentRoute: () => repoRoute,
path: '/packages',
loader: ({ context, params }) =>
prefetch(context, ['packages', params.repo], () =>
loadPackages(params.repo),
),
component: function Packages() {
const { repo } = packagesRoute.useParams();
const space = useSpace();
const { record, settled } = useRepoRecord(repo, space);
if (!record) return settled ? <NoSuchRepository /> : <RepoPageSkeleton />;
return <PackagesPage repo={repo} registry="" space={space} />;
},
});
const packagesTabRoute = createRoute({
getParentRoute: () => repoRoute,
path: '/packages/$registry',
loader: ({ context, params }) =>
prefetch(context, ['packages', params.repo], () =>
loadPackages(params.repo),
),
component: function PackagesTab() {
const { repo, registry } = packagesTabRoute.useParams();
const space = useSpace();
const { record, settled } = useRepoRecord(repo, space);
if (!record) return settled ? <NoSuchRepository /> : <RepoPageSkeleton />;
return <PackagesPage repo={repo} registry={registry} space={space} />;
},
});
/**
* The repository's own settings. Every field is already in hand from the
* listing, so the loader reads only what the form offers to link: the
* packages this account publishes.
*/
const settingsRoute = createRoute({
getParentRoute: () => repoRoute,
path: '/settings',
loader: ({ context }) =>
prefetch(context, ['published-packages'], publishedPackages),
component: function Settings() {
const { repo } = settingsRoute.useParams();
const space = useSpace();
const { record, settled } = useRepoRecord(repo, space);
if (!record) return settled ? <NoSuchRepository /> : <RepoPageSkeleton />;
return <SettingsPage repo={repo} space={space} />;
},
});
function NoSuchRepository() {
return <PageNotice>No such repository.</PageNotice>;
}
const routeTree = rootRoute.addChildren([
indexRoute,
newRoute,
callbackRoute,
repoRoute.addChildren([
treeIndexRoute,
treeRoute,
blobRoute,
commitsIndexRoute,
commitsRoute,
commitRoute,
checksRoute,
checkRoute,
pullsRoute,
pullsTabRoute,
pullRoute,
issuesRoute,
issueNewRoute,
issuesTabRoute,
issueRoute,
packagesRoute,
packagesTabRoute,
settingsRoute,
]),
]);
/**
* @param {import('@tanstack/react-query').QueryClient} queryClient
*/
export function createAppRouter(queryClient) {
return createRouter({
routeTree,
context: { queryClient },
// A link's page is read when the pointer settles on it. The query
// client says whether the answer is fresh, so the router never re-reads
// what the client already holds.
defaultPreload: 'intent',
defaultPreloadStaleTime: 0,
scrollRestoration: false,
});
}