History Adapter API
Complete API reference for the History adapter with IndexedDB persistence.
createHistoryAdapter()
Creates a history adapter with IndexedDB-based event storage.
Signature
function createHistoryAdapter(
config: HistoryAdapterConfig
): HistoryAdapterParameters
interface HistoryAdapterConfig {
dbName: string; // IndexedDB database name
namespace?: string; // Namespace prefix for storage (default: 'default')
ttlSeconds?: number; // Time-to-live in seconds (default: 3600 (1 hour))
maxMessages?: number; // Maximum number of messages to persist globally (default: 1000)
gcIntervalMs?: number; // Garbage collection interval (default: 60000 (1 minute))
debug?: boolean; // Enable debug logging
onError?: (error: Error) => void; // Error callback
}Returns
HistoryAdapter instance
Example
import { createHistoryAdapter } from '@belyas/pubsub-mfe/adapters/history';
const history = createHistoryAdapter({
dbName: 'my-app-history',
namespace: 'events',
ttlSeconds: 7 * 24 * 60 * 60 * 1000, // 7 days
maxMessages: 500,
gcIntervalMs: 60000,
debug: true
});
await history.attach(bus);HistoryAdapter
Methods
attach(bus)
Attaches the adapter to a PubSub bus and starts storing events.
async attach(bus: PubSubBus): Promise<void>Example:
const bus = createPubSub();
await history.attach(bus);
// Events are now being stored
bus.publish('orders.created', { orderId: '123' });detach()
Detaches the adapter and stops storing events.
async detach(): Promise<void>Example:
await history.detach();
// Storage continues to exist, but no new events storedgetHistory(options)
Retrieves stored events with optional filtering.
async getHistory(
topic: Topic,
options: HistoryQueryOptions = {}
): Promise<Message<T>[]>
interface HistoryQueryOptions {
fromTime?: number; // From timestamp (ms)
limit?: number; // Max results
}Examples:
// Get all events
const allEvents = await history.getHistory('cart.add.item');
// Get events from specific topic
const orderEvents = await history.getHistory('orders.*');
// Get events with time and limit
const recentEvents = await history.getHistory('cart.add.item', {
startTime: Date.now() - 60000, // Last minute
limit: 100,
});clearHistory()
Clears stored events with optional filtering.
async clearHistory(): Promise<void>Examples:
// Clear all events
await history.clearHistory();getStats()
Returns adapter statistics and storage information.
async getStats(): Promise<HistoryAdapterStats>
interface HistoryAdapterStats {
messagesPersisted: number;
messagesRetrieved: number;
messagesGarbageCollected: number;
duplicatesSkipped: number;
gcCyclesCompleted: number;
estimatedStorageCount: number;
lastGcTimestamp: number | null;
attached: boolean;
namespace: string;
}Example:
const stats = await history.getStats();
console.log(`GC cycles completed: ${stats.gcCyclesCompleted}`);
console.log(`Storage size: ${stats.estimatedStorageCount}`);
console.log(`Persisted messages:, ${stats.messagesPersisted});forceGc()
Manually triggers garbage collection to remove expired events.
async forceGc(): Promise<void>Example:
await history.forceGc();Advanced Usage
Event Replay
Replay historical events to rebuild state.
const history = createHistoryAdapter({
dbName: 'app-events',
ttlSeconds: 30 * 24 * 60 * 60 * 1000 // 30 days
});
await history.attach(bus);
// Later: Replay events to rebuild state
async function rebuildState() {
const events = await history.getHistory('inventory.*');
for (const event of events) {
await processEvent(event);
}
}Time-Travel Debugging
Debug issues by replaying events from specific time.
async function debugIssue(incidentTime: number) {
// Get events 5 minutes before incident
const events = await history.getHistory('order.command.updated', {
startTime: incidentTime - 5 * 60 * 1000,
limit: 100,
});
console.log('Events leading to incident:', events);
// Replay in test environment
for (const event of events) {
testBus.publish(event.topic, event.payload);
}
}Audit Logging
Store all events for audit purposes.
const auditHistory = createHistoryAdapter({...});
await auditHistory.attach(bus);
// Later: Generate audit report
async function generateAuditReport(userId: string, startDate: Date, limit: number) {
const events = await auditHistory.getHistory('cart.item.add', {
startTime: startDate.getTime(),
limit,
});
const userEvents = events.filter(e =>
e.payload.userId === userId
);
return createReport(userEvents);
}Offline Event Storage
Store events while offline, sync when back online.
const offlineHistory = createHistoryAdapter({
dbName: 'offline-queue',
ttlSeconds: 7 * 24 * 60 * 60 * 1000,
});
await offlineHistory.attach(bus);
// When back online
window.addEventListener('online', async () => {
const pendingEvents = await offlineHistory.getHistory('cart.item.add', {
startTime: lastSyncTime,
});
for (const event of pendingEvents) {
await syncToServer(event);
}
// Clear synced events
await offlineHistory.clearHistory();
});Storage Management
Capacity Management
const history = createHistoryAdapter({
dbName: 'my-app',
maxMessages: 1000, // Keep only last 1000 events
gcIntervalMs: 30000 // Check every 30s
});
// Monitor capacity
setInterval(async () => {
const stats = await history.getStats();
if (stats.messagesPersisted > 900) {
console.warn('Reaching capacity limit');
}
}, 60000);TTL Configuration
// Short-term cache
const shortTerm = createHistoryAdapter({
dbName: 'cache',
ttlSeconds: 60 * 60 * 1000 // 1 hour
});
// Long-term storage
const longTerm = createHistoryAdapter({
dbName: 'archive',
ttlSeconds: 365 * 24 * 60 * 60 * 1000 // 1 year
});Performance Optimization
Batch Queries
// ❌ Bad: Multiple small queries
for (const topic of topics) {
await history.getHistory(topic);
}
// ✅ Good: Single query with pattern
const events = await history.getHistory('users.*');Index Optimization
The adapter creates indexes on:
timestamp- For TTL queriestopic- For topic filtering
// ✅ Fast: Uses timestamp index
await history.getHistory('cart.item.add', {
startTime: start,
limit: 100
});
// ✅ Fast: Uses topic index
await history.getHistory('orders.*');Memory Management
// Handle large result sets
async function* streamEvents(topic: string) {
const batchSize = 100;
let offset = 0;
while (true) {
const batch = await history.getHistory(topic, {
limit: batchSize,
});
if (batch.length === 0) break;
for (const event of batch) {
yield event;
}
offset += batchSize;
}
}Error Handling
Common Errors
const history = createHistoryAdapter({
dbName: 'my-app',
onError: (error) => {
if (error.name === 'QuotaExceededError') {
console.error('Storage quota exceeded');
history.clearHistory();
} else if (error.name === 'InvalidStateError') {
console.error('IndexedDB not available');
// Fallback to memory storage
} else {
console.error('History adapter error:', error);
}
}
});Database Errors
QuotaExceededError- Storage quota exceededInvalidStateError- IndexedDB not availableVersionError- Database version mismatchAbortError- Transaction aborted
Type Definitions
interface HistoryAdapter {
async attach(bus: PubSubBus): Promise<void>;
async detach(): Promise<void>;
async getHistory<T = unknown>(
topic: Topic,
options: HistoryQueryOptions = {}
): Promise<Message<T>[]>;
async clearHistory(): Promise<void>;
async getStats(): Promise<HistoryAdapterStats>;
async forceGc(): Promise<void>;
}
interface HistoryQueryOptions {
fromTime?: number;
limit?: number;
}
interface HistoryAdapterStats {
messagesPersisted: number;
messagesRetrieved: number;
messagesGarbageCollected: number;
duplicatesSkipped: number;
gcCyclesCompleted: number;
estimatedStorageCount: number;
lastGcTimestamp: number | null;
attached: boolean;
namespace: string;
}Browser Support
The History adapter requires IndexedDB support:
- ✅ Chrome 24+
- ✅ Firefox 16+
- ✅ Safari 10+
- ✅ Edge 12+
- ❌ IE (use polyfill)
Feature Detection:
if (!window.indexedDB) {
console.warn('IndexedDB not supported');
// Use alternative storage
}Next Steps
- History Guide - Usage guide
- Cross-Tab API - Cross-tab adapter
- Types - Complete type definitions
- Core API - Core PubSub API