open-source, lexicon-agnostic PDS for AI agents. welcome-mat enrollment, AT Proto federation.
agents atprotocol pds cloudflare
0

Configure Feed

Select the types of activity you want to include in your feed.

rookery / src / storage.ts
4.3 kB 141 lines
1import { CID } from "@atproto/lex-data"; 2import { 3 BlockMap, 4 ReadableBlockstore, 5 cborToLex, 6 type CommitData, 7 type RepoStorage, 8} from "@atproto/repo"; 9import type Database from "better-sqlite3"; 10 11export class SqliteRepoStorage 12 extends ReadableBlockstore 13 implements RepoStorage 14{ 15 constructor( 16 private db: Database.Database, 17 private accountId: number, 18 ) { 19 super(); 20 } 21 22 async getBytes(cid: CID): Promise<Uint8Array | null> { 23 const row = this.db 24 .prepare("SELECT bytes FROM blocks WHERE account_id = ? AND cid = ?") 25 .get(this.accountId, cid.toString()) as { bytes: Buffer } | undefined; 26 return row?.bytes ?? null; 27 } 28 29 async has(cid: CID): Promise<boolean> { 30 const row = this.db 31 .prepare( 32 "SELECT 1 FROM blocks WHERE account_id = ? AND cid = ? LIMIT 1", 33 ) 34 .get(this.accountId, cid.toString()); 35 return !!row; 36 } 37 38 async getBlocks(cids: CID[]): Promise<{ blocks: BlockMap; missing: CID[] }> { 39 const blocks = new BlockMap(); 40 const missing: CID[] = []; 41 for (const cid of cids) { 42 const bytes = await this.getBytes(cid); 43 if (bytes) { 44 blocks.set(cid, bytes); 45 } else { 46 missing.push(cid); 47 } 48 } 49 return { blocks, missing }; 50 } 51 52 async getRoot(): Promise<CID | null> { 53 const row = this.db 54 .prepare("SELECT root_cid FROM accounts WHERE id = ?") 55 .get(this.accountId) as { root_cid: string | null } | undefined; 56 if (!row?.root_cid) return null; 57 return CID.parse(row.root_cid); 58 } 59 60 async getRev(): Promise<string | null> { 61 const row = this.db 62 .prepare("SELECT rev FROM accounts WHERE id = ?") 63 .get(this.accountId) as { rev: string | null } | undefined; 64 return row?.rev ?? null; 65 } 66 67 async putBlock(cid: CID, block: Uint8Array, rev: string): Promise<void> { 68 this.db 69 .prepare( 70 "INSERT OR REPLACE INTO blocks (account_id, cid, bytes, rev) VALUES (?, ?, ?, ?)", 71 ) 72 .run(this.accountId, cid.toString(), block, rev); 73 } 74 75 async putMany(blocks: BlockMap, rev: string): Promise<void> { 76 const stmt = this.db.prepare( 77 "INSERT OR REPLACE INTO blocks (account_id, cid, bytes, rev) VALUES (?, ?, ?, ?)", 78 ); 79 for (const [cid, bytes] of blocks) { 80 stmt.run(this.accountId, cid.toString(), bytes, rev); 81 } 82 } 83 84 async updateRoot(cid: CID, rev: string): Promise<void> { 85 this.db 86 .prepare("UPDATE accounts SET root_cid = ?, rev = ? WHERE id = ?") 87 .run(cid.toString(), rev, this.accountId); 88 } 89 90 async applyCommit(commit: CommitData): Promise<void> { 91 const doCommit = this.db.transaction(() => { 92 const insertBlock = this.db.prepare( 93 "INSERT OR REPLACE INTO blocks (account_id, cid, bytes, rev) VALUES (?, ?, ?, ?)", 94 ); 95 for (const [cid, bytes] of commit.newBlocks) { 96 insertBlock.run(this.accountId, cid.toString(), bytes, commit.rev); 97 } 98 99 const deleteBlock = this.db.prepare( 100 "DELETE FROM blocks WHERE account_id = ? AND cid = ?", 101 ); 102 for (const cid of commit.removedCids) { 103 deleteBlock.run(this.accountId, cid.toString()); 104 } 105 106 this.db 107 .prepare( 108 "UPDATE accounts SET root_cid = ?, rev = ? WHERE id = ?", 109 ) 110 .run(commit.cid.toString(), commit.rev, this.accountId); 111 112 // Extract MST data root CID from the commit block for prev_data_cid 113 const commitBytes = commit.newBlocks.get(commit.cid); 114 if (!commitBytes) { 115 throw new Error(`Missing commit block for CID ${commit.cid.toString()}`); 116 } 117 const commitObj = cborToLex(commitBytes) as { data: CID }; 118 this.db 119 .prepare("UPDATE accounts SET prev_data_cid = ? WHERE id = ?") 120 .run(commitObj.data.toString(), this.accountId); 121 }); 122 doCommit(); 123 } 124 125 addCollection(collection: string): void { 126 this.db 127 .prepare( 128 "INSERT OR IGNORE INTO collections (account_id, collection) VALUES (?, ?)", 129 ) 130 .run(this.accountId, collection); 131 } 132 133 getCollections(): string[] { 134 const rows = this.db 135 .prepare( 136 "SELECT collection FROM collections WHERE account_id = ? ORDER BY collection", 137 ) 138 .all(this.accountId) as { collection: string }[]; 139 return rows.map((r) => r.collection); 140 } 141}