UNPKG

@simplepg/common

Version:

Common utilities for SimplePage packages

169 lines (152 loc) 4.7 kB
import { MemoryBlockstore } from 'blockstore-core/memory' import { IDBBlockstore } from 'blockstore-idb' import { CID } from 'multiformats/cid' import { fromString } from 'uint8arrays/from-string' import { toString } from 'uint8arrays/to-string' const toBase64 = (bytes) => toString(bytes, 'base64') const fromBase64 = (base64) => fromString(base64, 'base64') const CID_PREFIX = 'ipld_block:' const IDB_NAME = 'hybrid-blockstore' /** * A blockstore that combines memory and IndexedDB storage. * Writes to memory immediately and IndexedDB asynchronously for performance. */ export class HybridBlockstore { #openPromise #memory #idb #pendingWrites #storage #flushTimer /** * @param {LocalStorage} storage */ constructor(storage) { this.#memory = new MemoryBlockstore() this.#idb = new IDBBlockstore(IDB_NAME) this.#pendingWrites = new Map() // Track pending IndexedDB writes this.#storage = storage this.#openPromise = (async () => { await this.#idb.open() await this.#importWAL() })() this.#flushTimer = setInterval(() => { this.flush().catch(console.error) }, 10000) // every 10s } async #importWAL() { if (this.#storage) { const keys = Object.keys(this.#storage).filter(key => key.startsWith(CID_PREFIX)) for (const key of keys) { const cid = CID.parse(key.slice(CID_PREFIX.length)) const value = this.#storage.getItem(key) if (value) { await this.#idb.put(cid, fromBase64(value)) } } } } async #afterOpen(action) { await this.#openPromise return action() } /** * Put a block into both memory and IndexedDB (async) */ async put(cid, bytes, options = {}) { // Write to memory immediately await this.#memory.put(cid, bytes, options) // For blocks larger than 256KB, await IndexedDB write directly const isLargeBlock = bytes.length > 256 * 1024 // 256KB const idbPromise = this.#afterOpen(() => this.#idb.put(cid, bytes, options)) if (isLargeBlock) { // Await the IndexedDB write for large blocks await idbPromise } else { // Write to IndexedDB asynchronously for smaller blocks (don't await) this.#pendingWrites.set(cid.toString(), idbPromise) if (this.#storage) { this.#storage.setItem(`${CID_PREFIX}${cid.toString()}`, toBase64(bytes)) } } return cid } /** * Get a block from memory first, then IndexedDB if not found */ async get(cid, options = {}) { try { // Try memory first return await this.#memory.get(cid, options) } catch (error) { if (error.code === 'ERR_NOT_FOUND') { // Try IndexedDB const bytes = await this.#afterOpen(() => this.#idb.get(cid, options)) // Add to memory for faster subsequent access this.#memory.put(cid, bytes, options) return bytes } throw error } } /** * Check if a block exists in memory or IndexedDB */ async has(cid, options = {}) { // Check memory first if (await this.#memory.has(cid, options)) { return true } // Check IndexedDB const exists = await this.#afterOpen(() => this.#idb.has(cid, options)) if (exists) { // If it exists in IndexedDB, also add it to memory for faster access try { this.#afterOpen(() => this.#idb.get(cid, options)).then(bytes => { this.#memory.put(cid, bytes, options) }) } catch (error) { console.error('Error getting from IndexedDB', error) } } return exists } /** * Delete a block from both memory and IndexedDB */ async delete(cid, options = {}) { // Remove from pending writes this.#pendingWrites.delete(cid.toString()) // Delete from both stores await Promise.all([ this.#memory.delete(cid, options), this.#afterOpen(() => this.#idb.delete(cid, options)) ]) if (this.#storage) { this.#storage.removeItem(`${CID_PREFIX}${cid.toString()}`) } } /** * Wait for all pending IndexedDB writes to complete */ async flush() { const promises = Array.from(this.#pendingWrites.values()) this.#pendingWrites.clear() // Wait for all pending writes to complete const results = await Promise.all(promises) // If storage exists, remove all successfully written cids from local storage if (this.#storage) { results.map(cid => { this.#storage.removeItem(`${CID_PREFIX}${cid}`) }) } } /** * Close both blockstores */ async close() { clearInterval(this.#flushTimer) await this.flush() await this.#idb.close() } }