UNPKG

@toolkit-p2p/mesh-cache

Version:

Content-addressed mesh cache for toolkit-p2p

248 lines (207 loc) 5.31 kB
/** * 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; } } }