chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
// The Files page reads blobs through listBlobDetails and getBlobFacets:
// metadata plus referencing records, filterable by kind, app, and usage,
// ordered newest-first or largest-first with a keyset cursor.
import Database from 'better-sqlite3';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { createActorStorage } from '../src/index.js';
/** A TID rkey for the given epoch ms, clock id 0. @param {number} ms */
function tid(ms) {
const chars = '234567abcdefghijklmnopqrstuvwxyz';
let value = BigInt(ms) * 1000n * 1024n;
let out = '';
for (let i = 0; i < 13; i++) {
out = chars[Number(value % 32n)] + out;
value /= 32n;
}
return out;
}
describe('listBlobDetails', () => {
/** @type {Required<import('@pdsjs/core/ports').ActorStoragePort>} */
let storage;
beforeEach(() => {
vi.useFakeTimers();
storage =
/** @type {Required<import('@pdsjs/core/ports').ActorStoragePort>} */ (
createActorStorage(new Database(':memory:'))
);
});
afterEach(() => {
vi.useRealTimers();
});
const seed = async () => {
vi.setSystemTime(1000);
await storage.putBlob('cid-a', 'image/jpeg', 100);
vi.setSystemTime(2000);
await storage.putBlob('cid-b', 'video/mp4', 300);
vi.setSystemTime(3000);
await storage.putBlob('cid-c', 'image/png', 50);
await storage.linkBlobToRecord('cid-a', 'at://did:x/app.bsky.feed.post/1');
await storage.linkBlobToRecord('cid-a', 'at://did:x/app.bsky.feed.post/2');
await storage.linkBlobToRecord('cid-c', 'at://did:x/social.grain.photo/3');
};
it('lists newest first with metadata and referencing records', async () => {
await seed();
const { blobs, cursor } = await storage.listBlobDetails(null, 10);
expect(cursor).toBe(null);
expect(blobs.map((b) => b.cid)).toEqual(['cid-c', 'cid-b', 'cid-a']);
expect(blobs[2]).toEqual({
cid: 'cid-a',
mimeType: 'image/jpeg',
size: 100,
createdAt: 1000,
// Non-TID rkeys carry no time; falls back to when the blob was stored.
time: 1000,
timeFromRecord: false,
records: [
'at://did:x/app.bsky.feed.post/1',
'at://did:x/app.bsky.feed.post/2',
],
});
// A blob nothing links to still lists, with no records.
expect(blobs[1].records).toEqual([]);
});
it('pages with a keyset cursor across equal timestamps', async () => {
vi.setSystemTime(5000);
for (const cid of ['cid-1', 'cid-2', 'cid-3']) {
await storage.putBlob(cid, 'image/jpeg', 10);
}
const first = await storage.listBlobDetails(null, 2);
expect(first.blobs.map((b) => b.cid)).toEqual(['cid-3', 'cid-2']);
expect(first.cursor).toBe('5000:cid-2');
const second = await storage.listBlobDetails(first.cursor, 2);
expect(second.blobs.map((b) => b.cid)).toEqual(['cid-1']);
expect(second.cursor).toBe(null);
});
it('orders by the referencing record time, not arrival time', async () => {
// A migrated repo: blobs stored during the import (same arrival time),
// while their records keep the original TID timestamps.
vi.setSystemTime(5000);
await storage.putBlob('cid-old-post', 'image/jpeg', 10);
await storage.putBlob('cid-new-post', 'image/jpeg', 10);
await storage.linkBlobToRecord(
'cid-old-post',
`at://did:x/social.grain.photo/${tid(1000)}`,
);
await storage.linkBlobToRecord(
'cid-new-post',
`at://did:x/social.grain.photo/${tid(3000)}`,
);
vi.setSystemTime(2000);
await storage.putBlob('cid-unlinked', 'image/jpeg', 10);
const first = await storage.listBlobDetails(null, 2);
expect(first.blobs.map((b) => [b.cid, b.time])).toEqual([
['cid-new-post', 3000],
['cid-unlinked', 2000],
]);
expect(first.cursor).toBe('2000:cid-unlinked');
const second = await storage.listBlobDetails(first.cursor, 2);
expect(second.blobs.map((b) => [b.cid, b.time])).toEqual([
['cid-old-post', 1000],
]);
});
it("prefers the record's stored timestamp over its TID and arrival", async () => {
vi.setSystemTime(5000);
await storage.putBlob('cid-x', 'image/jpeg', 10);
await storage.putBlob('cid-y', 'image/jpeg', 10);
// A self-keyed record (no TID) whose value carried createdAt 1500.
await storage.linkBlobToRecord(
'cid-x',
'at://did:x/actor.rpg.sprite/self',
1500,
);
// A TID-keyed record: the stored record time still wins over the TID.
await storage.linkBlobToRecord(
'cid-y',
`at://did:x/app.test.photo/${tid(4000)}`,
2500,
);
const { blobs } = await storage.listBlobDetails(null, 10);
expect(blobs.map((b) => [b.cid, b.time, b.timeFromRecord])).toEqual([
['cid-y', 2500, true],
['cid-x', 1500, true],
]);
});
it('sorts largest first, with a size-keyed cursor', async () => {
await seed();
const first = await storage.listBlobDetails(null, 2, { sort: 'largest' });
expect(first.blobs.map((b) => b.cid)).toEqual(['cid-b', 'cid-a']);
expect(first.cursor).toBe('100:cid-a');
const second = await storage.listBlobDetails(first.cursor, 2, {
sort: 'largest',
});
expect(second.blobs.map((b) => b.cid)).toEqual(['cid-c']);
});
it('filters by media kind, including the other bucket', async () => {
await seed();
vi.setSystemTime(4000);
await storage.putBlob('cid-d', 'application/pdf', 70);
const images = await storage.listBlobDetails(null, 10, { kind: 'image' });
expect(images.blobs.map((b) => b.cid)).toEqual(['cid-c', 'cid-a']);
const other = await storage.listBlobDetails(null, 10, { kind: 'other' });
expect(other.blobs.map((b) => b.cid)).toEqual(['cid-d']);
});
it('filters by referencing app authority', async () => {
await seed();
const bsky = await storage.listBlobDetails(null, 10, {
appAuthority: 'app.bsky',
});
expect(bsky.blobs.map((b) => b.cid)).toEqual(['cid-a']);
const grain = await storage.listBlobDetails(null, 10, {
appAuthority: 'social.grain',
});
expect(grain.blobs.map((b) => b.cid)).toEqual(['cid-c']);
});
it('filters to unused blobs and composes filters', async () => {
await seed();
const unused = await storage.listBlobDetails(null, 10, { unused: true });
expect(unused.blobs.map((b) => b.cid)).toEqual(['cid-b']);
const unusedImages = await storage.listBlobDetails(null, 10, {
unused: true,
kind: 'image',
});
expect(unusedImages.blobs).toEqual([]);
});
it('aggregates facets by kind, app, and usage', async () => {
await seed();
// cid-c used by two apps: counts once in each. cid-a used twice by one
// app: counts once there.
await storage.linkBlobToRecord('cid-c', 'at://did:x/app.bsky.feed.post/9');
const facets = await storage.getBlobFacets();
expect(facets.total).toEqual({ count: 3, bytes: 450 });
expect(facets.unused).toEqual({ count: 1, bytes: 300 });
expect(facets.kinds).toEqual([
{ kind: 'video', count: 1, bytes: 300 },
{ kind: 'image', count: 2, bytes: 150 },
]);
expect(facets.apps).toEqual([
{ authority: 'app.bsky', count: 2, bytes: 150 },
{ authority: 'social.grain', count: 1, bytes: 50 },
]);
});
it('reports empty storage as zero', async () => {
expect(await storage.getBlobFacets()).toEqual({
total: { count: 0, bytes: 0 },
unused: { count: 0, bytes: 0 },
kinds: [],
apps: [],
});
const { blobs, cursor } = await storage.listBlobDetails(null, 10);
expect(blobs).toEqual([]);
expect(cursor).toBe(null);
});
});