chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
import {
CID,
cborEncodeDagCbor,
cidToString,
createBlobCid,
createCid,
} from '@pdsjs/core/repo';
import { describe, expect, it } from 'vitest';
import { createSiteBrowser } from '../src/browser.js';
const DID = 'did:plc:browsertest';
/** @param {Array<{name: string, files: Array<{path: string, size: number}>, createdAt?: string}>} sites */
async function storageWith(sites) {
/** @type {Array<{uri: string, cid: string, value: Uint8Array}>} */
const rows = [];
for (const site of sites) {
const files = [];
for (const file of site.files) {
const data = new Uint8Array(file.size);
files.push({
path: file.path,
blob: {
$type: 'blob',
ref: new CID(await createBlobCid(data)),
mimeType: 'application/octet-stream',
size: file.size,
},
});
}
const value = cborEncodeDagCbor({
$type: 'dev.pdsjs.site.deploy',
files,
createdAt: site.createdAt ?? '2026-08-06T00:00:00.000Z',
});
rows.push({
uri: `at://${DID}/dev.pdsjs.site.deploy/${site.name}`,
cid: cidToString(await createCid(value)),
value,
});
}
return /** @type {any} */ ({
listRecords: async (
/** @type {string} */ _collection,
/** @type {string|null} */ cursor,
/** @type {number} */ limit,
) => {
const start = cursor ? Number(cursor) : 0;
const records = rows.slice(start, start + limit);
const next = start + records.length;
return { records, cursor: next < rows.length ? String(next) : null };
},
getRecord: async (/** @type {string} */ uri) =>
rows.find((row) => row.uri === uri) ?? null,
});
}
describe('createSiteBrowser', () => {
it('throws on missing ctx entries', () => {
expect(() => createSiteBrowser(/** @type {any} */ ({}))).toThrow(
/actorStorage/,
);
});
it('lists sites with counts, sizes, urls, and servability', async () => {
const browser = createSiteBrowser({
actorStorage: await storageWith([
{
name: 'home',
files: [
{ path: 'index.html', size: 1000 },
{ path: 'style.css', size: 500 },
],
},
{ name: 'blog', files: [{ path: 'index.html', size: 250 }] },
{ name: 'Bad_Name', files: [] },
]),
getDid: async () => DID,
siteDomain: 'example.com',
});
expect(browser.domain).toBe('example.com');
expect(browser.apexName).toBe('home');
const sites = await browser.listSites();
expect(sites).toHaveLength(3);
const home = sites.find((site) => site.name === 'home');
expect(home).toMatchObject({
url: 'https://example.com',
fileCount: 2,
totalSize: 1500,
deployedAt: '2026-08-06T00:00:00.000Z',
servable: true,
});
expect(home?.cid).toMatch(/^b/);
expect(sites.find((site) => site.name === 'blog')).toMatchObject({
url: 'https://blog.example.com',
servable: true,
});
expect(sites.find((site) => site.name === 'Bad_Name')).toMatchObject({
servable: false,
});
});
it("lists a site's files sorted by path, and null for a missing site", async () => {
const browser = createSiteBrowser({
actorStorage: await storageWith([
{
name: 'home',
files: [
{ path: 'style.css', size: 500 },
{ path: 'index.html', size: 1000 },
],
},
]),
getDid: async () => DID,
siteDomain: 'example.com',
});
const files = await browser.listFiles('home');
expect(files?.map((file) => file.path)).toEqual([
'index.html',
'style.css',
]);
expect(files?.[0]).toMatchObject({
size: 1000,
contentType: 'application/octet-stream',
});
expect(files?.[0].cid).toMatch(/^b/);
expect(await browser.listFiles('nope')).toBeNull();
});
it('pages through more than one listRecords call', async () => {
const many = Array.from({ length: 150 }, (_, i) => ({
name: `site-${i}`,
files: [],
}));
const browser = createSiteBrowser({
actorStorage: await storageWith(many),
getDid: async () => DID,
siteDomain: 'example.com',
});
expect(await browser.listSites()).toHaveLength(150);
});
});