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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
206 lines · 6.0 KB Raw History
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>
  );
}