Calendar/src/storage/IndexedDBContext.ts
Janus C. H. Knudsen 9ea98e3a04 Adds audit logging and sync management infrastructure
Introduces comprehensive audit trail system with:
- AuditService to track entity changes
- SyncManager for background sync of audit entries
- New CoreEvents for entity and audit tracking
- Simplified sync architecture with event-driven approach

Prepares system for enhanced compliance and change tracking
2025-11-21 23:23:04 +01:00

128 lines
3.8 KiB
TypeScript

import { IStore } from './IStore';
/**
* IndexedDBContext - Database connection manager and provider
*
* RESPONSIBILITY:
* - Opens and manages IDBDatabase connection lifecycle
* - Creates object stores via injected IStore implementations
* - Provides shared IDBDatabase instance to all services
*
* SEPARATION OF CONCERNS:
* - This class: Connection management ONLY
* - OperationQueue: Queue and sync state operations
* - Entity Services: CRUD operations for specific entities
*
* USAGE:
* Services inject IndexedDBContext and call getDatabase() to access db.
* This lazy access pattern ensures db is ready when requested.
*/
export class IndexedDBContext {
private static readonly DB_NAME = 'CalendarDB';
private static readonly DB_VERSION = 3; // Bumped for audit store
static readonly QUEUE_STORE = 'operationQueue';
static readonly SYNC_STATE_STORE = 'syncState';
private db: IDBDatabase | null = null;
private initialized: boolean = false;
private stores: IStore[];
/**
* @param stores - Array of IStore implementations injected via DI
*/
constructor(stores: IStore[]) {
this.stores = stores;
}
/**
* Initialize and open the database
* Creates all entity stores, queue store, and sync state store
*/
async initialize(): Promise<void> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(IndexedDBContext.DB_NAME, IndexedDBContext.DB_VERSION);
request.onerror = () => {
reject(new Error(`Failed to open IndexedDB: ${request.error}`));
};
request.onsuccess = () => {
this.db = request.result;
this.initialized = true;
resolve();
};
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
// Create all entity stores via injected IStore implementations
// Open/Closed Principle: Adding new entity only requires DI registration
this.stores.forEach(store => {
if (!db.objectStoreNames.contains(store.storeName)) {
store.create(db);
}
});
// Create operation queue store (sync infrastructure)
if (!db.objectStoreNames.contains(IndexedDBContext.QUEUE_STORE)) {
const queueStore = db.createObjectStore(IndexedDBContext.QUEUE_STORE, { keyPath: 'id' });
queueStore.createIndex('timestamp', 'timestamp', { unique: false });
}
// Create sync state store (sync metadata)
if (!db.objectStoreNames.contains(IndexedDBContext.SYNC_STATE_STORE)) {
db.createObjectStore(IndexedDBContext.SYNC_STATE_STORE, { keyPath: 'key' });
}
};
});
}
/**
* Check if database is initialized
*/
public isInitialized(): boolean {
return this.initialized;
}
/**
* Get IDBDatabase instance
* Used by services to access the database
*
* @throws Error if database not initialized
* @returns IDBDatabase instance
*/
public getDatabase(): IDBDatabase {
if (!this.db) {
throw new Error('IndexedDB not initialized. Call initialize() first.');
}
return this.db;
}
/**
* Close database connection
*/
close(): void {
if (this.db) {
this.db.close();
this.db = null;
this.initialized = false;
}
}
/**
* Delete entire database (for testing/reset)
*/
static async deleteDatabase(): Promise<void> {
return new Promise((resolve, reject) => {
const request = indexedDB.deleteDatabase(IndexedDBContext.DB_NAME);
request.onsuccess = () => {
resolve();
};
request.onerror = () => {
reject(new Error(`Failed to delete database: ${request.error}`));
};
});
}
}