@toolkit-p2p/mesh-cache
Version:
Content-addressed mesh cache for toolkit-p2p
248 lines (207 loc) • 5.31 kB
text/typescript
/**
* Block storage using IndexedDB
*/
import { openDB, IDBPDatabase } from 'idb';
import type { Block, BlockMetadata, CacheStats, CID } from './types.js';
export class BlockStorage {
private db: IDBPDatabase | null = null;
private dbName = 'zetaMeshCache';
/**
* Initialize storage
*/
async init(): Promise<void> {
this.db = await openDB(this.dbName, 1, {
upgrade(db) {
const store = db.createObjectStore('blocks', { keyPath: 'cid' });
store.createIndex('by-pinned', 'pinned');
store.createIndex('by-added', 'addedAt');
store.createIndex('by-accessed', 'lastAccessedAt');
},
});
}
/**
* Store a block
*/
async put(block: Block): Promise<void> {
if (!this.db) {
throw new Error('Storage not initialized');
}
await this.db.put('blocks', block);
}
/**
* Get a block by CID
*/
async get(cid: CID): Promise<Block | null> {
if (!this.db) {
throw new Error('Storage not initialized');
}
const block = await this.db.get('blocks', cid);
if (!block) {
return null;
}
// Update last accessed timestamp
block.lastAccessedAt = Date.now();
await this.db.put('blocks', block);
return block;
}
/**
* Check if a block exists
*/
async has(cid: CID): Promise<boolean> {
if (!this.db) {
throw new Error('Storage not initialized');
}
const count = await this.db.count('blocks', cid);
return count > 0;
}
/**
* Delete a block
*/
async delete(cid: CID): Promise<void> {
if (!this.db) {
throw new Error('Storage not initialized');
}
await this.db.delete('blocks', cid);
}
/**
* Get block metadata (without data)
*/
async getMetadata(cid: CID): Promise<BlockMetadata | null> {
if (!this.db) {
throw new Error('Storage not initialized');
}
const block = await this.db.get('blocks', cid);
if (!block) {
return null;
}
return {
cid: block.cid,
size: block.size,
addedAt: block.addedAt,
lastAccessedAt: block.lastAccessedAt,
pinned: block.pinned,
ttl: block.ttl,
};
}
/**
* List all CIDs
*/
async list(): Promise<CID[]> {
if (!this.db) {
throw new Error('Storage not initialized');
}
const keys = await this.db.getAllKeys('blocks');
return keys as string[];
}
/**
* Get cache statistics
*/
async getStats(): Promise<CacheStats> {
if (!this.db) {
throw new Error('Storage not initialized');
}
const blocks = await this.db.getAll('blocks');
let totalSize = 0;
let pinnedBlocks = 0;
let pinnedSize = 0;
for (const block of blocks) {
totalSize += block.size;
if (block.pinned) {
pinnedBlocks++;
pinnedSize += block.size;
}
}
return {
blockCount: blocks.length,
totalSize,
pinnedBlocks,
pinnedSize,
};
}
/**
* Pin a block (prevent garbage collection)
*/
async pin(cid: CID): Promise<void> {
if (!this.db) {
throw new Error('Storage not initialized');
}
const block = await this.db.get('blocks', cid);
if (!block) {
throw new Error(`Block not found: ${cid}`);
}
block.pinned = true;
await this.db.put('blocks', block);
}
/**
* Unpin a block (allow garbage collection)
*/
async unpin(cid: CID): Promise<void> {
if (!this.db) {
throw new Error('Storage not initialized');
}
const block = await this.db.get('blocks', cid);
if (!block) {
return; // Block doesn't exist, nothing to unpin
}
block.pinned = false;
await this.db.put('blocks', block);
}
/**
* Run garbage collection
* Removes expired and unpinned blocks to free space
*/
async gc(maxSize?: number): Promise<number> {
if (!this.db) {
throw new Error('Storage not initialized');
}
const now = Date.now();
let removed = 0;
// Get all blocks sorted by last access (oldest first)
const blocks = await this.db.getAllFromIndex('blocks', 'by-accessed');
// First pass: Remove expired blocks
for (const block of blocks) {
if (!block.pinned && block.ttl > 0) {
const expiresAt = block.addedAt + block.ttl * 1000;
if (now > expiresAt) {
await this.db.delete('blocks', block.cid);
removed++;
}
}
}
// Second pass: Remove oldest unpinned blocks if over max size
if (maxSize !== undefined) {
const stats = await this.getStats();
if (stats.totalSize > maxSize) {
const unpinnedBlocks = blocks.filter((b) => !b.pinned);
let currentSize = stats.totalSize;
for (const block of unpinnedBlocks) {
if (currentSize <= maxSize) {
break;
}
await this.db.delete('blocks', block.cid);
currentSize -= block.size;
removed++;
}
}
}
return removed;
}
/**
* Clear all blocks
*/
async clear(): Promise<void> {
if (!this.db) {
throw new Error('Storage not initialized');
}
await this.db.clear('blocks');
}
/**
* Close storage
*/
async close(): Promise<void> {
if (this.db) {
this.db.close();
this.db = null;
}
}
}