import * as CAR from "@atcute/car"; import * as CBOR from "@atcute/cbor"; import * as CID from "@atcute/cid"; import { Did } from "@atcute/lexicons"; import { fromStream, isCommit } from "@atcute/repo"; import * as TID from "@atcute/tid"; import { Title } from "@solidjs/meta"; import { useLocation, useNavigate } from "@solidjs/router"; import { createEffect, createMemo, createSignal, For, Match, Show, Switch, untrack, } from "solid-js"; import { Button } from "../../components/button.jsx"; import { Favicon } from "../../components/favicon.jsx"; import HoverCard from "../../components/hover-card/base"; import { JSONValue } from "../../components/json.jsx"; import { TextInput } from "../../components/text-input.jsx"; import { didDocCache, resolveDidDoc } from "../../utils/api.js"; import { localDateFromTimestamp } from "../../utils/date.js"; import { createDebouncedValue } from "../../utils/hooks/debounced.js"; import { createDropHandler, createFileChangeHandler, handleDragOver } from "./file-handlers.js"; import { type Archive, type CollectionEntry, type RecordEntry, type View, toJsonValue, WelcomeView, } from "./shared.jsx"; const viewToHash = (view: View): string => { switch (view.type) { case "repo": return ""; case "collection": return `#${view.collection.name}`; case "record": return `#${view.collection.name}/${view.record.key}`; } }; const hashToView = (hash: string, archive: Archive): View => { if (!hash || hash === "#") return { type: "repo" }; const raw = hash.startsWith("#") ? hash.slice(1) : hash; const slashIdx = raw.indexOf("/"); if (slashIdx === -1) { const collection = archive.entries.find((e) => e.name === raw); if (collection) return { type: "collection", collection }; return { type: "repo" }; } const collectionName = raw.slice(0, slashIdx); const recordKey = raw.slice(slashIdx + 1); const collection = archive.entries.find((e) => e.name === collectionName); if (collection) { const record = collection.entries.find((r) => r.key === recordKey); if (record) return { type: "record", collection, record }; return { type: "collection", collection }; } return { type: "repo" }; }; export const ExploreToolView = () => { const location = useLocation(); const navigate = useNavigate(); const [archive, setArchive] = createSignal(null); const [loading, setLoading] = createSignal(false); const [progress, setProgress] = createSignal(0); const [error, setError] = createSignal(); const view = createMemo((): View => { const arch = archive(); if (!arch) return { type: "repo" }; return hashToView(location.hash, arch); }); const navigateToView = (newView: View) => { const hash = viewToHash(newView); navigate(`${location.pathname}${hash}`); }; const parseCarFile = async (file: File) => { setLoading(true); setProgress(0); setError(undefined); try { // Read file as ArrayBuffer to extract DID from commit block const buffer = new Uint8Array(await file.arrayBuffer()); const car = CAR.fromUint8Array(buffer); // Get DID from commit block let did = "Repository"; const rootCid = car.roots[0]?.$link; if (rootCid) { for (const entry of car) { try { if (CID.toString(entry.cid) === rootCid) { const commit = CBOR.decode(entry.bytes); if (isCommit(commit)) { did = commit.did; } break; } } catch { // Skip entries with invalid CIDs } } } const collections = new Map(); const result: Archive = { file, did, entries: [], }; const stream = file.stream(); const repo = fromStream(stream); try { let count = 0; for await (const entry of repo) { try { let list = collections.get(entry.collection); if (list === undefined) { collections.set(entry.collection, (list = [])); result.entries.push({ name: entry.collection, entries: list, }); } const record = toJsonValue(entry.record); list.push({ key: entry.rkey, cid: entry.cid.$link, record, }); if (++count % 10000 === 0) { setProgress(count); await new Promise((resolve) => setTimeout(resolve, 0)); } } catch { // Skip entries with invalid data } } } finally { await repo.dispose(); } // Resolve DID document to populate handle in cache if (did !== "Repository") { try { const doc = await resolveDidDoc(did as Did); didDocCache[did] = doc; } catch (err) { console.error("Failed to resolve DID document:", err); } } setArchive(result); if (location.hash) navigate(location.pathname, { replace: true }); } catch (err) { console.error("Failed to parse CAR file:", err); setError(err instanceof Error ? err.message : "Failed to parse CAR file"); } finally { setLoading(false); } }; const handleFileChange = createFileChangeHandler(parseCarFile); const handleDrop = createDropHandler(parseCarFile); const reset = () => { setArchive(null); setError(undefined); if (location.hash) navigate(location.pathname, { replace: true }); }; return ( <> Explore archive - PDSls } > {(arch) => ( )} ); }; const ExploreView = (props: { archive: Archive; view: () => View; setView: (view: View) => void; onClose: () => void; }) => { const handle = didDocCache[props.archive.did]?.alsoKnownAs ?.filter((alias) => alias.startsWith("at://"))[0] ?.split("at://")[1] ?? props.archive.did; return (
{/* Collection Level */} { const v = props.view(); return v.type === "collection" || v.type === "record" ? v.collection : null; })()} > {(collection) => (
{collection().name}
} >
)}
{/* Record Level */} { const v = props.view(); return v.type === "record" ? v.record : null; })()} > {(record) => { const rkeyTimestamp = createMemo(() => { if (!record().key || !TID.validate(record().key)) return undefined; const timestamp = TID.parse(record().key).timestamp / 1000; return timestamp <= Date.now() ? timestamp : undefined; }); return (
{record().key} ({localDateFromTimestamp(rkeyTimestamp()!)})
); }}
{ const v = props.view(); return v.type === "collection" ? v : null; })()} keyed > {({ collection }) => ( )} { const v = props.view(); return v.type === "record" ? v : null; })()} keyed > {({ collection, record }) => ( )}
); }; const RepoSubview = (props: { archive: Archive; onRoute: (view: View) => void }) => { const [filter, setFilter] = createSignal(""); const sortedEntries = createMemo(() => { return [...props.archive.entries].sort((a, b) => a.name.localeCompare(b.name)); }); const filteredEntries = createMemo(() => { const f = filter().toLowerCase().trim(); if (!f) return sortedEntries(); return sortedEntries().filter((entry) => entry.name.toLowerCase().includes(f)); }); const totalRecords = createMemo(() => props.archive.entries.reduce((sum, entry) => sum + entry.entries.length, 0), ); return (
{props.archive.entries.length} collection{props.archive.entries.length !== 1 ? "s" : ""} ยท {totalRecords()} record{totalRecords() !== 1 ? "s" : ""}
setFilter(e.currentTarget.value)} class="text-sm" />
    {(entry) => { const hasSingleEntry = entry.entries.length === 1; const authority = () => entry.name.split(".").slice(0, 2).join("."); return (
  • ); }}

No collections match your filter

); }; const RECORDS_PER_PAGE = 100; const CollectionSubview = (props: { archive: Archive; collection: CollectionEntry; onRoute: (view: View) => void; }) => { const [filter, setFilter] = createSignal(""); const debouncedFilter = createDebouncedValue(filter, 150); const [displayCount, setDisplayCount] = createSignal(RECORDS_PER_PAGE); // Sort entries by TID timestamp (most recent first), non-TID entries go to the end const sortedEntries = createMemo(() => { return [...props.collection.entries].sort((a, b) => { const aIsTid = TID.validate(a.key); const bIsTid = TID.validate(b.key); if (aIsTid && bIsTid) { return TID.parse(b.key).timestamp - TID.parse(a.key).timestamp; } if (aIsTid) return -1; if (bIsTid) return 1; return b.key.localeCompare(a.key); }); }); const searchableEntries = createMemo(() => { return sortedEntries().map((entry) => ({ entry, searchText: JSON.stringify(entry.record).toLowerCase(), })); }); const filteredEntries = createMemo(() => { const f = debouncedFilter().toLowerCase().trim(); if (!f) return sortedEntries(); return searchableEntries() .filter(({ searchText }) => searchText.includes(f)) .map(({ entry }) => entry); }); createEffect(() => { debouncedFilter(); untrack(() => setDisplayCount(RECORDS_PER_PAGE)); }); const displayedEntries = createMemo(() => { return filteredEntries().slice(0, displayCount()); }); const hasMore = createMemo(() => filteredEntries().length > displayCount()); const loadMore = () => { setDisplayCount((prev) => prev + RECORDS_PER_PAGE); }; return (
{filteredEntries().length} record{filteredEntries().length !== 1 ? "s" : ""} {filter() && filteredEntries().length !== props.collection.entries.length && ( {" "} (of {props.collection.entries.length}) )}
setFilter(e.currentTarget.value)} class="grow text-sm" /> {displayedEntries().length}/{filteredEntries().length}
{(entry) => { const isTid = TID.validate(entry.key); const timestamp = isTid ? TID.parse(entry.key).timestamp / 1_000 : null; return ( { props.onRoute({ type: "record", collection: props.collection, record: entry, }); }} class="flex w-full items-baseline gap-1 px-1 py-0.5" > {entry.key} {entry.cid} {(ts) => ( {localDateFromTimestamp(ts())} )} } > ); }}

No records match your filter

); }; const RecordSubview = (props: { archive: Archive; collection: CollectionEntry; record: RecordEntry; }) => { return (
{props.record.cid}
Failed to decode record
} >
); };