chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
// @pdsjs/core/space-browser - Account-page view of permissioned-data spaces
//
// The counting a space storage does not do itself. Core stays free of any
// @pdsjs/spaces import: a platform package hands the storage in, and what
// comes back is the plain data the account page renders.
import { cborDecode, cborDecodeJson, tidToMs } from './repo.js';
/**
* @typedef {Object} SpaceStorageLike
* @property {(options: {type?: string|null, did?: string|null, cursor?: string|null, limit?: number}) => Promise<{spaces: import('./ports.js').SpaceRow[], cursor: string|null}>} listSpaces
* @property {(space: string, cursor: string|null, limit: number) => Promise<{members: import('./ports.js').SpaceMemberRow[], cursor: string|null}>} listMembers
* @property {(space: string) => Promise<Array<{collection: string, rkey: string, cid: string}>>} listAllSpaceRecords
* @property {(space: string, opts: {collection?: string|null, cursor?: string|null, limit: number, reverse?: boolean, excludeValues?: boolean}) => Promise<{records: Array<{rkey: string, cid: string, value?: Uint8Array, indexedAt: string}>, cursor: string|null}>} listSpaceRecords
*/
// A personal server holds few spaces; this caps a runaway listing rather
// than paging an account page that would be unreadable long before it.
const SPACE_LIMIT = 100;
const MEMBER_LIMIT = 500;
// Paged rather than capped: the reap guard has to see every record, since a
// blob it misses is a blob that gets deleted.
const RECORD_PAGE = 500;
/**
* Build the read-only space view the account page consumes.
* @param {SpaceStorageLike} spaceStorage
* @returns {import('./ports.js').SpaceBrowserPort}
*/
export function createSpaceBrowser(spaceStorage) {
return {
async listSpaces() {
const { spaces } = await spaceStorage.listSpaces({ limit: SPACE_LIMIT });
return spaces;
},
// The write time comes from the rkey's TID, the same way the repo's own
// collections report one, so a record with a non-TID rkey counts toward
// the total and carries no time.
async countRecords(uri) {
/** @type {Map<string, {count: number, lastRecordAt: number|null}>} */
const counts = new Map();
for (const record of await spaceStorage.listAllSpaceRecords(uri)) {
const entry = counts.get(record.collection) || {
count: 0,
lastRecordAt: null,
};
entry.count += 1;
const at = tidToMs(record.rkey);
if (at !== null && at > (entry.lastRecordAt || 0)) {
entry.lastRecordAt = at;
}
counts.set(record.collection, entry);
}
return [...counts]
.map(([collection, entry]) => ({ collection, ...entry }))
.sort((a, b) => a.collection.localeCompare(b.collection));
},
async listRecords(uri, collection, limit) {
// One over the limit, so the page can say there are more without
// paging through a collection it is not going to show in full.
const { records } = await spaceStorage.listSpaceRecords(uri, {
collection,
limit: limit + 1,
reverse: true,
});
return {
records: records.slice(0, limit).map((record) => ({
rkey: record.rkey,
cid: record.cid,
indexedAt: record.indexedAt,
value: cborDecodeJson(/** @type {Uint8Array} */ (record.value)),
})),
truncated: records.length > limit,
};
},
// Feeds the blob reap guard, which reads every record's content to find
// out what it references. Values are decoded as stored, which
// findBlobRefs reads directly.
async listAllRecords() {
const { spaces } = await spaceStorage.listSpaces({ limit: SPACE_LIMIT });
/** @type {Array<{space: string, collection: string, rkey: string, value: unknown}>} */
const all = [];
for (const space of spaces) {
let cursor = null;
do {
const page = await spaceStorage.listSpaceRecords(space.uri, {
cursor,
limit: RECORD_PAGE,
});
for (const record of page.records) {
all.push({
space: space.uri,
collection: /** @type {any} */ (record).collection,
rkey: record.rkey,
value: cborDecode(/** @type {Uint8Array} */ (record.value)),
});
}
cursor = page.cursor;
} while (cursor);
}
return all;
},
async countMembers(uri) {
const { members } = await spaceStorage.listMembers(
uri,
null,
MEMBER_LIMIT,
);
return members.length;
},
async listMembers(uri) {
const { members } = await spaceStorage.listMembers(
uri,
null,
MEMBER_LIMIT,
);
return members;
},
};
}