IndexedDB Adapter
Browser IndexedDB adapter for client-side analytics database ✅ Production Ready
The IndexedDB adapter stores analytics data in the browser's IndexedDB database. Ideal for larger datasets and better performance compared to localStorage.
Status: ✅ Production Ready
Fully implemented with proper async operations, database schema, and indexing.
Usage
Basic Setup
import { IndexDBAdapter } from 'honolytics/storage'
const adapter = new IndexDBAdapter()
await adapter.connect()
// Insert analytics data
await adapter.insertEvent({
id: 'evt_123',
timestamp: Date.now(),
sessionId: 'session_abc',
url: '/dashboard',
event: 'pageview',
userAgent: navigator.userAgent,
duration: 45
})
// Query analytics
const metrics = await adapter.queryFullMetrics(
new Date('2024-01-01'),
new Date('2024-01-31')
)With Storage Factory
import { initStorage, indexdb } from 'honolytics/storage'
await initStorage(indexdb())
// Now use with React hooks in storage mode
<HonolyticsProvider storageMode={true}>
<App />
</HonolyticsProvider>Database Schema
The adapter automatically creates the IndexedDB schema:
Database: 'honolytics' (version 1)
├── events (ObjectStore)
│ ├── keyPath: 'id'
│ ├── index: 'timestamp'
│ └── index: 'sessionId'
└── sessions (ObjectStore)
└── keyPath: 'id'API Reference
Constructor
new IndexDBAdapter()No configuration required. Uses database name honolytics with version 1.
Connection Management
async connect(): Promise<void>Opens IndexedDB connection, creates schema if needed. Handles version upgrades automatically.
async disconnect(): Promise<void>Closes IndexedDB connection and cleans up resources.
Data Insertion
async insertEvent(event: TEvent): Promise<void>Inserts or updates event using put() operation. Events with same ID are overwritten.
async insertSession(session: TSession): Promise<void>Inserts or updates session using put() operation. Sessions with same ID are overwritten.
Query Methods
Time Series Metrics
async queryMetrics(start: Date, end: Date): Promise<TMetric[]>Returns daily aggregated metrics. More efficient than localStorage due to indexing.
Event Filtering
async queryEvents(filters: TEventFilter): Promise<TEvent[]>Filter events using indexed queries where possible:
// Optimized: Uses timestamp index
const events = await adapter.queryEvents({
start: new Date('2024-01-01'),
end: new Date('2024-01-31')
})
// Optimized: Uses sessionId index
const sessionEvents = await adapter.queryEvents({
sessionId: 'session_abc'
})
// Less optimal: Full table scan
const userEvents = await adapter.queryEvents({
userId: 'user_123'
})Breakdown Queries
All breakdown queries have the same API as localStorage but with better performance:
async queryTopPages(start: Date, end: Date, limit = 10): Promise<TPageStat[]>
async queryCountries(start: Date, end: Date, limit = 10): Promise<TCountryStat[]>
async queryBrowsers(start: Date, end: Date, limit = 10): Promise<TBrowserStat[]>
async queryDevices(start: Date, end: Date, limit = 10): Promise<TDeviceStat[]>Aggregate Queries
async queryTotals(start: Date, end: Date): Promise<TTotals>
async queryFullMetrics(start: Date, end: Date): Promise<TFullMetrics>Performance Advantages
Indexing Benefits
// ✅ Fast: Uses timestamp index
const recentEvents = await adapter.queryEvents({
start: new Date(Date.now() - 24 * 60 * 60 * 1000)
})
// ✅ Fast: Uses sessionId index
const sessionEvents = await adapter.queryEvents({
sessionId: 'session_123'
})
// ⚠️ Slower: No index available
const userEvents = await adapter.queryEvents({
userId: 'user_456' // Full table scan
})Memory Efficiency
- Lazy Loading: Data fetched only when needed
- Streaming: Can handle large datasets without memory issues
- Garbage Collection: Better memory management than localStorage
Storage Capacity
- Large Storage: ~250MB-1GB+ per origin (browser dependent)
- Structured Data: Binary data support, complex objects
- Transactions: ACID compliance for data integrity
Browser Compatibility
- Modern Browsers: Full support (Chrome, Firefox, Safari, Edge)
- IE 10+: Basic support (older versions)
- Mobile Browsers: Full support on iOS/Android
- Private Mode: Available but data cleared when session ends
Database Operations
Error Handling
try {
await adapter.connect()
} catch (error) {
if (error.message.includes('IndexedDB not available')) {
// Fallback to localStorage adapter
console.warn('IndexedDB not supported, using fallback')
}
}Connection States
// Check if connected before operations
if (adapter.db) {
await adapter.insertEvent(event)
} else {
throw new Error('Database not connected')
}Schema Upgrades
The adapter handles schema migrations automatically:
// Version 1: Initial schema (current)
onupgradeneeded: (event) => {
const db = event.target.result
// Create events store with indices
const evStore = db.createObjectStore('events', { keyPath: 'id' })
evStore.createIndex('timestamp', 'timestamp')
evStore.createIndex('sessionId', 'sessionId')
// Create sessions store
db.createObjectStore('sessions', { keyPath: 'id' })
}
// Future versions would add migration logic hereData Processing
Async Operations
All operations are properly async and return Promises:
// ✅ Concurrent operations
const [events, sessions] = await Promise.all([
adapter.queryEvents({ start, end }),
adapter.getAllSessions()
])
// ✅ Sequential with proper error handling
try {
await adapter.insertEvent(event)
await adapter.insertSession(session)
} catch (error) {
console.error('Failed to insert data:', error)
}Transaction Safety
IndexedDB provides transaction safety for data integrity:
// Each operation runs in its own transaction
await adapter.insertEvent(event1) // Transaction 1
await adapter.insertEvent(event2) // Transaction 2
// If event2 fails, event1 is still savedMigration from localStorage
Data Transfer
// Example migration from localStorage to IndexedDB
const localAdapter = new LocalAdapter()
const indexdbAdapter = new IndexDBAdapter()
await localAdapter.connect()
await indexdbAdapter.connect()
// Transfer events
const events = await localAdapter.queryEvents({})
for (const event of events) {
await indexdbAdapter.insertEvent(event)
}
// Transfer sessions
const sessions = localAdapter.getSessions()
for (const session of sessions) {
await indexdbAdapter.insertSession(session)
}
// Clean up localStorage
localStorage.removeItem('honolytics:events')
localStorage.removeItem('honolytics:sessions')Best Practices
Connection Management
// ✅ Good: Proper cleanup
class AnalyticsService {
private adapter = new IndexDBAdapter()
async init() {
await this.adapter.connect()
}
async cleanup() {
await this.adapter.disconnect()
}
}
// Use with React
useEffect(() => {
const service = new AnalyticsService()
service.init()
return () => {
service.cleanup() // Cleanup on unmount
}
}, [])Error Recovery
// ✅ Good: Graceful degradation
async function initAnalytics() {
try {
const adapter = new IndexDBAdapter()
await adapter.connect()
return adapter
} catch (error) {
console.warn('IndexedDB failed, falling back to localStorage')
const fallback = new LocalAdapter()
await fallback.connect()
return fallback
}
}Query Optimization
// ✅ Good: Use indexed fields for filtering
const optimizedQuery = await adapter.queryEvents({
start: dateRange.start, // Uses timestamp index
sessionId: currentSession // Uses sessionId index
})
// ❌ Avoid: Complex filters that require full scans
const slowQuery = await adapter.queryEvents({
event: 'click', // No index, full table scan
meta: { button: 'signup' } // No index support
})Limitations
- Single Origin: Data scoped to domain/subdomain
- User Controlled: Users can clear via browser settings
- No Cross-Tab Real-Time: Changes in one tab not immediately visible in others
- No Geo-IP: Country queries return 'Unknown' like localStorage
Use Cases
Perfect for:
- Large Analytics Datasets: Better performance than localStorage
- Offline-First Apps: Robust offline analytics storage
- Progressive Web Apps: Better storage for PWAs
- Complex Queries: Index-optimized filtering
Advantages over localStorage:
- Better Performance: Indexed queries and lazy loading
- Larger Capacity: 50x-200x more storage space
- Data Integrity: Transaction support and error recovery
- Memory Efficient: Doesn't load all data into memory
Consider PostgreSQL/Turso adapters for:
- Server-Side Analytics: Cross-device tracking
- Large Scale: Millions of events
- Advanced Queries: Complex aggregations and joins
- Multi-User: Shared analytics across users