UNPKG

@tanstack/offline-transactions

Version:

Offline-first transaction capabilities for TanStack DB

167 lines (149 loc) 4.81 kB
import { BaseStorageAdapter } from './StorageAdapter' export class IndexedDBAdapter extends BaseStorageAdapter { private dbName: string private storeName: string private db: IDBDatabase | null = null constructor(dbName = `offline-transactions`, storeName = `transactions`) { super() this.dbName = dbName this.storeName = storeName } /** * Probe IndexedDB availability by attempting to open a test database. * This catches private mode and other restrictions that block IndexedDB. */ static async probe(): Promise<{ available: boolean; error?: Error }> { // Check if IndexedDB exists if (typeof indexedDB === `undefined`) { return { available: false, error: new Error(`IndexedDB is not available in this environment`), } } // Try to actually open a test database to verify it works try { const testDbName = `__offline-tx-probe__` const request = indexedDB.open(testDbName, 1) return new Promise((resolve) => { request.onerror = () => { const error = request.error || new Error(`IndexedDB open failed`) resolve({ available: false, error }) } request.onsuccess = () => { // Clean up test database const db = request.result db.close() indexedDB.deleteDatabase(testDbName) resolve({ available: true }) } request.onblocked = () => { resolve({ available: false, error: new Error(`IndexedDB is blocked`), }) } }) } catch (error) { return { available: false, error: error instanceof Error ? error : new Error(String(error)), } } } private async openDB(): Promise<IDBDatabase> { if (this.db) { return this.db } return new Promise((resolve, reject) => { const request = indexedDB.open(this.dbName, 1) request.onerror = () => reject(request.error) request.onsuccess = () => { this.db = request.result resolve(this.db) } request.onupgradeneeded = (event) => { const db = (event.target as IDBOpenDBRequest).result if (!db.objectStoreNames.contains(this.storeName)) { db.createObjectStore(this.storeName) } } }) } private async getStore( mode: IDBTransactionMode = `readonly`, ): Promise<IDBObjectStore> { const db = await this.openDB() const transaction = db.transaction([this.storeName], mode) return transaction.objectStore(this.storeName) } async get(key: string): Promise<string | null> { try { const store = await this.getStore(`readonly`) return new Promise((resolve, reject) => { const request = store.get(key) request.onerror = () => reject(request.error) request.onsuccess = () => resolve(request.result ?? null) }) } catch (error) { console.warn(`IndexedDB get failed:`, error) return null } } async set(key: string, value: string): Promise<void> { try { const store = await this.getStore(`readwrite`) return new Promise((resolve, reject) => { const request = store.put(value, key) request.onerror = () => reject(request.error) request.onsuccess = () => resolve() }) } catch (error) { if ( error instanceof DOMException && error.name === `QuotaExceededError` ) { throw new Error( `Storage quota exceeded. Consider clearing old transactions.`, ) } throw error } } async delete(key: string): Promise<void> { try { const store = await this.getStore(`readwrite`) return new Promise((resolve, reject) => { const request = store.delete(key) request.onerror = () => reject(request.error) request.onsuccess = () => resolve() }) } catch (error) { console.warn(`IndexedDB delete failed:`, error) } } async keys(): Promise<Array<string>> { try { const store = await this.getStore(`readonly`) return new Promise((resolve, reject) => { const request = store.getAllKeys() request.onerror = () => reject(request.error) request.onsuccess = () => resolve(request.result as Array<string>) }) } catch (error) { console.warn(`IndexedDB keys failed:`, error) return [] } } async clear(): Promise<void> { try { const store = await this.getStore(`readwrite`) return new Promise((resolve, reject) => { const request = store.clear() request.onerror = () => reject(request.error) request.onsuccess = () => resolve() }) } catch (error) { console.warn(`IndexedDB clear failed:`, error) } } }