atproto user agency toolkit for individuals and groups
1/**
2 * SQLite-backed Blockstore for Helia.
3 *
4 * Replaces FsBlockstore to avoid creating thousands of tiny files
5 * that hammer macOS fseventsd. All blocks stored in a single SQLite table.
6 */
7
8import type Database from "better-sqlite3";
9import { CID } from "multiformats";
10
11export class SqliteBlockstore {
12 private db: Database.Database;
13
14 constructor(db: Database.Database) {
15 this.db = db;
16 this.db.exec(`
17 CREATE TABLE IF NOT EXISTS ipfs_blocks (
18 cid TEXT PRIMARY KEY,
19 bytes BLOB NOT NULL
20 )
21 `);
22 }
23
24 async put(key: CID, val: Uint8Array): Promise<CID> {
25 this.db
26 .prepare("INSERT OR REPLACE INTO ipfs_blocks (cid, bytes) VALUES (?, ?)")
27 .run(key.toString(), Buffer.from(val));
28 return key;
29 }
30
31 async * get(key: CID): AsyncGenerator<Uint8Array> {
32 const row = this.db
33 .prepare("SELECT bytes FROM ipfs_blocks WHERE cid = ?")
34 .get(key.toString()) as { bytes: Buffer } | undefined;
35 if (!row) {
36 throw new Error(`Block not found: ${key.toString()}`);
37 }
38 yield new Uint8Array(row.bytes);
39 }
40
41 async has(key: CID): Promise<boolean> {
42 const row = this.db
43 .prepare("SELECT 1 FROM ipfs_blocks WHERE cid = ?")
44 .get(key.toString());
45 return row !== undefined;
46 }
47
48 async delete(key: CID): Promise<void> {
49 this.db
50 .prepare("DELETE FROM ipfs_blocks WHERE cid = ?")
51 .run(key.toString());
52 }
53
54 async * putMany(source: AsyncIterable<{ cid: CID; block: Uint8Array }> | Iterable<{ cid: CID; block: Uint8Array }>): AsyncGenerator<CID> {
55 for await (const { cid, block } of source) {
56 await this.put(cid, block);
57 yield cid;
58 }
59 }
60
61 async * getMany(source: AsyncIterable<CID> | Iterable<CID>): AsyncGenerator<{ cid: CID; block: Uint8Array }> {
62 for await (const cid of source) {
63 let block: Uint8Array = new Uint8Array(0);
64 for await (const chunk of this.get(cid)) {
65 block = chunk;
66 }
67 yield { cid, block };
68 }
69 }
70
71 async * deleteMany(source: AsyncIterable<CID> | Iterable<CID>): AsyncGenerator<CID> {
72 for await (const cid of source) {
73 await this.delete(cid);
74 yield cid;
75 }
76 }
77
78 /**
79 * Delete all blocks from the blockstore.
80 * Used during full disconnect to wipe the node clean.
81 */
82 clear(): void {
83 this.db.prepare("DELETE FROM ipfs_blocks").run();
84 }
85
86 async * getAll(): AsyncGenerator<{ cid: CID; block: Uint8Array }> {
87 const rows = this.db
88 .prepare("SELECT cid, bytes FROM ipfs_blocks")
89 .all() as Array<{ cid: string; bytes: Buffer }>;
90 for (const row of rows) {
91 yield { cid: CID.parse(row.cid), block: new Uint8Array(row.bytes) };
92 }
93 }
94}