chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
import { useQuery } from '@tanstack/react-query';
import { ChevronDown, ChevronRight } from 'lucide-react';
import { useState } from 'react';
import { ago, fileSize } from '#/lib/format.js';
import { identityOf } from '#/lib/identity.js';
import { cn } from '#/lib/utils.js';
// A record arrives as the JSON form of DAG-CBOR, so three of its leaves are
// objects rather than scalars: a CID link, a byte string, and a blob reference.
// Each reads as one thing on the page instead of as a nested object.
/**
* @param {unknown} value
* @returns {value is {$link: string}}
*/
function isLink(value) {
return Boolean(value && typeof value === 'object' && '$link' in value);
}
/**
* @param {unknown} value
* @returns {value is {$bytes: string}}
*/
function isBytes(value) {
return Boolean(value && typeof value === 'object' && '$bytes' in value);
}
/**
* @param {any} value
* @returns {boolean}
*/
function isBlob(value) {
return value?.$type === 'blob' && isLink(value.ref);
}
const ISO_DATE = /^\d{4}-\d{2}-\d{2}T[\d:.]+Z?$/;
/**
* Whoever a DID in a record belongs to, added beside it. A record names an
* account by DID and nothing else, so without this a reader has to copy the
* string somewhere to find out who wrote what.
*/
function IdentityHint({ did }) {
const { data } = useQuery({
queryKey: ['identity', did],
queryFn: () => identityOf(did),
retry: false,
staleTime: Number.POSITIVE_INFINITY,
});
if (!data?.handle) return null;
return (
<span
className="ml-2 text-[var(--code-meta)] text-[11px]"
title={data.verified ? undefined : 'Claimed but not confirmed.'}
>
{data.handle}
{data.verified ? '' : '?'}
</span>
);
}
/** One scalar, coloured by what it is. */
function Leaf({ value, onBlob }) {
if (value === null) {
return <span className="text-[var(--code-keyword)]">null</span>;
}
if (typeof value === 'boolean' || typeof value === 'number') {
return <span className="text-[var(--code-number)]">{String(value)}</span>;
}
if (isBlob(value)) {
return (
<button
type="button"
onClick={() => onBlob?.(value.ref.$link)}
className="inline-flex items-center gap-1.5 rounded-[5px] border border-key/25 bg-key/10 px-1.5 py-px text-[11px] text-key hover:border-key/50"
>
<span>{value.mimeType || 'blob'}</span>
<span className="text-key/70">{fileSize(Number(value.size) || 0)}</span>
</button>
);
}
if (isLink(value)) {
return (
<span className="text-[var(--code-title)]" title="CID">
{value.$link}
</span>
);
}
if (isBytes(value)) {
// The decoded length rather than the base64 text: a hash, a signature and a
// MAC are all opaque here, and their size is the part worth reading.
const bytes = Math.floor((value.$bytes.length * 3) / 4);
return <span className="text-[var(--code-meta)]">{bytes} bytes</span>;
}
const text = String(value);
// The DID is the value, so it stays whole. The handle is added beside it
// rather than in place of it, because a record says one and a reader knows
// the other.
if (text.startsWith('did:')) {
return (
<span className="text-[var(--code-title)]">
"{text}"
<IdentityHint did={text} />
</span>
);
}
if (text.startsWith('at://')) {
return <span className="text-[var(--code-title)]">"{text}"</span>;
}
return (
<span className="text-[var(--code-string)]">
"{text}"
{ISO_DATE.test(text) && (
<span className="ml-2 text-[var(--code-meta)] text-[11px]">
{ago(text)}
</span>
)}
</span>
);
}
/** One key and its value, with a disclosure when the value has parts. */
function Entry({ name, value, depth, onBlob }) {
const branching =
value !== null &&
typeof value === 'object' &&
!isLink(value) &&
!isBytes(value) &&
!isBlob(value);
// Two levels open is enough to see a record's shape without scrolling past
// the parts of it that are lists of their own.
const [open, setOpen] = useState(depth < 2);
if (!branching) {
return (
<div className="flex flex-wrap items-baseline gap-x-2 py-px">
{name !== undefined && (
<span className="text-[var(--code-attr)]">{name}:</span>
)}
<Leaf value={value} onBlob={onBlob} />
</div>
);
}
const entries = Array.isArray(value)
? value.map((item, index) => [String(index), item])
: Object.entries(value);
const Chevron = open ? ChevronDown : ChevronRight;
const summary = Array.isArray(value)
? `${entries.length} item${entries.length === 1 ? '' : 's'}`
: `${entries.length} field${entries.length === 1 ? '' : 's'}`;
return (
<div className="py-px">
<button
type="button"
onClick={() => setOpen(!open)}
className="flex cursor-pointer items-center gap-1 text-left hover:opacity-80"
>
<Chevron className="size-3 text-faint" />
{name !== undefined && (
<span className="text-[var(--code-attr)]">{name}</span>
)}
<span className="text-[var(--code-meta)]">
{Array.isArray(value) ? '[' : '{'}
{!open && ` ${summary} `}
{!open && (Array.isArray(value) ? ']' : '}')}
</span>
</button>
{open && (
<div className="ml-[7px] border-border border-l pl-3">
{entries.map(([key, item]) => (
<Entry
key={key}
name={key}
value={item}
depth={depth + 1}
onBlob={onBlob}
/>
))}
<span className="text-[var(--code-meta)]">
{Array.isArray(value) ? ']' : '}'}
</span>
</div>
)}
</div>
);
}
/**
* A record value, read rather than pretty-printed. Pass `onBlob` to make a blob
* reference open.
*/
export function JsonView({ value, onBlob, className }) {
return (
<div
className={cn(
'overflow-x-auto font-mono text-[12.5px] leading-[1.7]',
className,
)}
>
<Entry value={value} depth={0} onBlob={onBlob} />
</div>
);
}