From 87a41db5ec3ce3ee84254fc581d63f45123b2acb Mon Sep 17 00:00:00 2001 From: Brian Richter Date: Thu, 19 Jun 2025 22:21:27 -0700 Subject: [PATCH] feat(indexer): add pluggable storage backend --- .../ordinalsplus/src/db/storage-backend.ts | 25 ++++ packages/ordinalsplus/src/index.ts | 4 +- packages/ordinalsplus/src/indexer/index.ts | 1 + .../ordinalsplus/src/indexer/memory-db.ts | 112 +++++++++++------- 4 files changed, 99 insertions(+), 43 deletions(-) create mode 100644 packages/ordinalsplus/src/db/storage-backend.ts diff --git a/packages/ordinalsplus/src/db/storage-backend.ts b/packages/ordinalsplus/src/db/storage-backend.ts new file mode 100644 index 0000000..210bd47 --- /dev/null +++ b/packages/ordinalsplus/src/db/storage-backend.ts @@ -0,0 +1,25 @@ +export interface StorageBackend { + get(key: string): Promise; + set(key: string, value: T | null): Promise; + clear(): Promise; +} + +export class InMemoryBackend implements StorageBackend { + private store = new Map(); + + async get(key: string): Promise { + return this.store.has(key) ? (this.store.get(key) as T) : null; + } + + async set(key: string, value: T | null): Promise { + if (value === null) { + this.store.delete(key); + return; + } + this.store.set(key, value); + } + + async clear(): Promise { + this.store.clear(); + } +} diff --git a/packages/ordinalsplus/src/index.ts b/packages/ordinalsplus/src/index.ts index ddc3b7f..4980490 100644 --- a/packages/ordinalsplus/src/index.ts +++ b/packages/ordinalsplus/src/index.ts @@ -123,7 +123,9 @@ export { // --- Indexer Exports --- export { OrdinalsIndexer, - MemoryIndexerDatabase + MemoryIndexerDatabase, + StorageBackend, + InMemoryBackend } from './indexer'; export { diff --git a/packages/ordinalsplus/src/indexer/index.ts b/packages/ordinalsplus/src/indexer/index.ts index 9aeef92..4050b8e 100644 --- a/packages/ordinalsplus/src/indexer/index.ts +++ b/packages/ordinalsplus/src/indexer/index.ts @@ -5,6 +5,7 @@ export * from './ordinals-indexer'; export * from './memory-db'; +export * from '../db/storage-backend'; export * from './logger'; export * from './errors'; export * from './error-handling'; diff --git a/packages/ordinalsplus/src/indexer/memory-db.ts b/packages/ordinalsplus/src/indexer/memory-db.ts index 68f4cea..4a7a633 100644 --- a/packages/ordinalsplus/src/indexer/memory-db.ts +++ b/packages/ordinalsplus/src/indexer/memory-db.ts @@ -3,6 +3,7 @@ */ import { IndexerDatabase, IndexerInscription } from '../types'; +import { StorageBackend, InMemoryBackend } from '../db/storage-backend'; /** * MemoryIndexerDatabase provides a simple in-memory implementation of IndexerDatabase @@ -11,35 +12,57 @@ import { IndexerDatabase, IndexerInscription } from '../types'; * Production applications should implement a persistent database solution. */ export class MemoryIndexerDatabase implements IndexerDatabase { - private inscriptions: Map = new Map(); - private contents: Map = new Map(); - private metadata: Map = new Map(); - private satoshipIndexes: Map = new Map(); - private didDocuments: Map = new Map(); - private credentials: Map = new Map(); - private lastSyncedHeight: number = 0; + private backend: StorageBackend; + private ttlMs: number | null; + + constructor(options: { backend?: StorageBackend; ttlMs?: number } = {}) { + this.backend = options.backend ?? new InMemoryBackend(); + this.ttlMs = options.ttlMs ?? null; + } + + private makeKey(type: string, id: string): string { + return `${type}:${id}`; + } + + private async getEntry(key: string): Promise { + const entry = await this.backend.get<{ data: T; expiresAt: number | null }>(key); + if (!entry) return null; + if (entry.expiresAt && Date.now() > entry.expiresAt) { + await this.backend.set(key, null); + return null; + } + return entry.data; + } + + private async setEntry(key: string, data: T): Promise { + const expiresAt = this.ttlMs ? Date.now() + this.ttlMs : null; + await this.backend.set(key, { data, expiresAt }); + } /** * Get an inscription by its ID */ async getInscription(id: string): Promise { - return this.inscriptions.get(id) || null; + return this.getEntry(this.makeKey('inscription', id)); } /** * Store an inscription */ async storeInscription(inscription: IndexerInscription): Promise { - this.inscriptions.set(inscription.id, inscription); - - // Update satoshi index - const satoshi = inscription.satoshi; - if (satoshi) { - const ids = this.satoshipIndexes.get(satoshi) || []; - if (!ids.includes(inscription.id)) { - ids.push(inscription.id); - this.satoshipIndexes.set(satoshi, ids); - } + await this.setEntry(this.makeKey('inscription', inscription.id), inscription); + + const satKey = this.makeKey('satoshi', inscription.satoshi); + const ids = (await this.getEntry(satKey)) || []; + if (!ids.includes(inscription.id)) { + ids.push(inscription.id); + await this.setEntry(satKey, ids); + } + + const allIds = (await this.getEntry(this.makeKey('all', 'ids'))) || []; + if (!allIds.includes(inscription.id)) { + allIds.push(inscription.id); + await this.setEntry(this.makeKey('all', 'ids'), allIds); } } @@ -47,99 +70,104 @@ export class MemoryIndexerDatabase implements IndexerDatabase { * Get inscriptions associated with a satoshi */ async getInscriptionsBySatoshi(satoshi: string): Promise { - const ids = this.satoshipIndexes.get(satoshi) || []; - return ids - .map(id => this.inscriptions.get(id)) - .filter(Boolean) as IndexerInscription[]; + const ids = (await this.getEntry(this.makeKey('satoshi', satoshi))) || []; + const results: IndexerInscription[] = []; + for (const id of ids) { + const ins = await this.getInscription(id); + if (ins) results.push(ins); + } + return results; } /** * Get raw inscription content */ async getInscriptionContent(id: string): Promise { - return this.contents.get(id) || null; + return this.getEntry(this.makeKey('content', id)); } /** * Store raw inscription content */ async storeInscriptionContent(id: string, content: Buffer): Promise { - this.contents.set(id, content); + await this.setEntry(this.makeKey('content', id), content); } /** * Get decoded metadata for an inscription */ async getInscriptionMetadata(id: string): Promise { - return this.metadata.get(id) || null; + return this.getEntry(this.makeKey('metadata', id)); } /** * Store decoded metadata for an inscription */ async storeInscriptionMetadata(id: string, metadata: any): Promise { - this.metadata.set(id, metadata); + await this.setEntry(this.makeKey('metadata', id), metadata); } /** * Get the last synced block height */ async getLastSyncedHeight(): Promise { - return this.lastSyncedHeight; + return this.getEntry('lastSyncedHeight'); } /** * Update the last synced block height */ async setLastSyncedHeight(height: number): Promise { - this.lastSyncedHeight = height; + await this.setEntry('lastSyncedHeight', height); } /** * Store a DID document */ async storeDIDDocument(didId: string, document: any): Promise { - this.didDocuments.set(didId, document); + await this.setEntry(this.makeKey('did', didId), document); } /** * Store a verifiable credential */ async storeCredential(inscriptionId: string, credential: any): Promise { - this.credentials.set(inscriptionId, credential); + await this.setEntry(this.makeKey('credential', inscriptionId), credential); } /** * Get all stored inscriptions (for testing) */ async getAllInscriptions(): Promise { - return Array.from(this.inscriptions.values()); + // naive implementation: rely on satoshi index + const idsEntry = await this.backend.get(this.makeKey('all', 'ids')); + const ids = idsEntry || []; + const res: IndexerInscription[] = []; + for (const id of ids) { + const ins = await this.getInscription(id); + if (ins) res.push(ins); + } + return res; } /** * Get a DID document by ID (for testing) */ async getDIDDocument(didId: string): Promise { - return this.didDocuments.get(didId) || null; + return this.getEntry(this.makeKey('did', didId)); } /** * Get a credential by inscription ID (for testing) */ async getCredential(inscriptionId: string): Promise { - return this.credentials.get(inscriptionId) || null; + return this.getEntry(this.makeKey('credential', inscriptionId)); } /** * Clear all stored data (for testing) */ async clearAll(): Promise { - this.inscriptions.clear(); - this.contents.clear(); - this.metadata.clear(); - this.satoshipIndexes.clear(); - this.didDocuments.clear(); - this.credentials.clear(); - this.lastSyncedHeight = 0; - } -} \ No newline at end of file + await this.backend.clear(); + } +}