Calendar/wwwroot/js/storage/IndexedDBService.js

340 lines
12 KiB
JavaScript
Raw Permalink Normal View History

2026-02-03 00:02:25 +01:00
/**
* IndexedDB Service for Calendar App
* Handles local storage of events and sync queue
*/
export class IndexedDBService {
constructor() {
this.db = null;
this.initialized = false;
}
/**
* Initialize and open the database
*/
async initialize() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(IndexedDBService.DB_NAME, IndexedDBService.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.result;
// Create events store
if (!db.objectStoreNames.contains(IndexedDBService.EVENTS_STORE)) {
const eventsStore = db.createObjectStore(IndexedDBService.EVENTS_STORE, { keyPath: 'id' });
eventsStore.createIndex('start', 'start', { unique: false });
eventsStore.createIndex('end', 'end', { unique: false });
eventsStore.createIndex('syncStatus', 'syncStatus', { unique: false });
}
// Create operation queue store
if (!db.objectStoreNames.contains(IndexedDBService.QUEUE_STORE)) {
const queueStore = db.createObjectStore(IndexedDBService.QUEUE_STORE, { keyPath: 'id' });
queueStore.createIndex('timestamp', 'timestamp', { unique: false });
}
// Create sync state store
if (!db.objectStoreNames.contains(IndexedDBService.SYNC_STATE_STORE)) {
db.createObjectStore(IndexedDBService.SYNC_STATE_STORE, { keyPath: 'key' });
}
};
});
}
/**
* Check if database is initialized
*/
isInitialized() {
return this.initialized;
}
/**
* Ensure database is initialized
*/
ensureDB() {
if (!this.db) {
throw new Error('IndexedDB not initialized. Call initialize() first.');
}
return this.db;
}
// ========================================
// Event CRUD Operations
// ========================================
/**
* Get a single event by ID
*/
async getEvent(id) {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([IndexedDBService.EVENTS_STORE], 'readonly');
const store = transaction.objectStore(IndexedDBService.EVENTS_STORE);
const request = store.get(id);
request.onsuccess = () => {
const event = request.result;
resolve(event ? this.deserializeEvent(event) : null);
};
request.onerror = () => {
reject(new Error(`Failed to get event ${id}: ${request.error}`));
};
});
}
/**
* Get all events
*/
async getAllEvents() {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([IndexedDBService.EVENTS_STORE], 'readonly');
const store = transaction.objectStore(IndexedDBService.EVENTS_STORE);
const request = store.getAll();
request.onsuccess = () => {
const events = request.result;
resolve(events.map(e => this.deserializeEvent(e)));
};
request.onerror = () => {
reject(new Error(`Failed to get all events: ${request.error}`));
};
});
}
/**
* Save an event (create or update)
*/
async saveEvent(event) {
const db = this.ensureDB();
const serialized = this.serializeEvent(event);
return new Promise((resolve, reject) => {
const transaction = db.transaction([IndexedDBService.EVENTS_STORE], 'readwrite');
const store = transaction.objectStore(IndexedDBService.EVENTS_STORE);
const request = store.put(serialized);
request.onsuccess = () => {
resolve();
};
request.onerror = () => {
reject(new Error(`Failed to save event ${event.id}: ${request.error}`));
};
});
}
/**
* Delete an event
*/
async deleteEvent(id) {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([IndexedDBService.EVENTS_STORE], 'readwrite');
const store = transaction.objectStore(IndexedDBService.EVENTS_STORE);
const request = store.delete(id);
request.onsuccess = () => {
resolve();
};
request.onerror = () => {
reject(new Error(`Failed to delete event ${id}: ${request.error}`));
};
});
}
// ========================================
// Queue Operations
// ========================================
/**
* Add operation to queue
*/
async addToQueue(operation) {
const db = this.ensureDB();
const queueItem = {
...operation,
id: `${operation.type}-${operation.eventId}-${Date.now()}`
};
return new Promise((resolve, reject) => {
const transaction = db.transaction([IndexedDBService.QUEUE_STORE], 'readwrite');
const store = transaction.objectStore(IndexedDBService.QUEUE_STORE);
const request = store.put(queueItem);
request.onsuccess = () => {
resolve();
};
request.onerror = () => {
reject(new Error(`Failed to add to queue: ${request.error}`));
};
});
}
/**
* Get all queue operations (sorted by timestamp)
*/
async getQueue() {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([IndexedDBService.QUEUE_STORE], 'readonly');
const store = transaction.objectStore(IndexedDBService.QUEUE_STORE);
const index = store.index('timestamp');
const request = index.getAll();
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
reject(new Error(`Failed to get queue: ${request.error}`));
};
});
}
/**
* Remove operation from queue
*/
async removeFromQueue(id) {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([IndexedDBService.QUEUE_STORE], 'readwrite');
const store = transaction.objectStore(IndexedDBService.QUEUE_STORE);
const request = store.delete(id);
request.onsuccess = () => {
resolve();
};
request.onerror = () => {
reject(new Error(`Failed to remove from queue: ${request.error}`));
};
});
}
/**
* Clear entire queue
*/
async clearQueue() {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([IndexedDBService.QUEUE_STORE], 'readwrite');
const store = transaction.objectStore(IndexedDBService.QUEUE_STORE);
const request = store.clear();
request.onsuccess = () => {
resolve();
};
request.onerror = () => {
reject(new Error(`Failed to clear queue: ${request.error}`));
};
});
}
// ========================================
// Sync State Operations
// ========================================
/**
* Save sync state value
*/
async setSyncState(key, value) {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([IndexedDBService.SYNC_STATE_STORE], 'readwrite');
const store = transaction.objectStore(IndexedDBService.SYNC_STATE_STORE);
const request = store.put({ key, value });
request.onsuccess = () => {
resolve();
};
request.onerror = () => {
reject(new Error(`Failed to set sync state ${key}: ${request.error}`));
};
});
}
/**
* Get sync state value
*/
async getSyncState(key) {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([IndexedDBService.SYNC_STATE_STORE], 'readonly');
const store = transaction.objectStore(IndexedDBService.SYNC_STATE_STORE);
const request = store.get(key);
request.onsuccess = () => {
const result = request.result;
resolve(result ? result.value : null);
};
request.onerror = () => {
reject(new Error(`Failed to get sync state ${key}: ${request.error}`));
};
});
}
// ========================================
// Serialization Helpers
// ========================================
/**
* Serialize event for IndexedDB storage (convert Dates to ISO strings)
*/
serializeEvent(event) {
return {
...event,
start: event.start instanceof Date ? event.start.toISOString() : event.start,
end: event.end instanceof Date ? event.end.toISOString() : event.end
};
}
/**
* Deserialize event from IndexedDB (convert ISO strings to Dates)
*/
deserializeEvent(event) {
return {
...event,
start: typeof event.start === 'string' ? new Date(event.start) : event.start,
end: typeof event.end === 'string' ? new Date(event.end) : event.end
};
}
/**
* Close database connection
*/
close() {
if (this.db) {
this.db.close();
this.db = null;
}
}
/**
* Delete entire database (for testing/reset)
*/
static async deleteDatabase() {
return new Promise((resolve, reject) => {
const request = indexedDB.deleteDatabase(IndexedDBService.DB_NAME);
request.onsuccess = () => {
resolve();
};
request.onerror = () => {
reject(new Error(`Failed to delete database: ${request.error}`));
};
});
}
/**
* Seed IndexedDB with mock data if empty
*/
async seedIfEmpty(mockDataUrl = 'data/mock-events.json') {
try {
const existingEvents = await this.getAllEvents();
if (existingEvents.length > 0) {
console.log(`IndexedDB already has ${existingEvents.length} events - skipping seed`);
return;
}
console.log('IndexedDB is empty - seeding with mock data');
// Check if online to fetch mock data
if (!navigator.onLine) {
console.warn('Offline and IndexedDB empty - starting with no events');
return;
}
// Fetch mock events
const response = await fetch(mockDataUrl);
if (!response.ok) {
throw new Error(`Failed to fetch mock events: ${response.statusText}`);
}
const mockEvents = await response.json();
// Convert and save to IndexedDB
for (const event of mockEvents) {
const calendarEvent = {
...event,
start: new Date(event.start),
end: new Date(event.end),
allDay: event.allDay || false,
syncStatus: 'synced'
};
await this.saveEvent(calendarEvent);
}
console.log(`Seeded IndexedDB with ${mockEvents.length} mock events`);
}
catch (error) {
console.error('Failed to seed IndexedDB:', error);
// Don't throw - allow app to start with empty calendar
}
}
}
IndexedDBService.DB_NAME = 'CalendarDB';
IndexedDBService.DB_VERSION = 1;
IndexedDBService.EVENTS_STORE = 'events';
IndexedDBService.QUEUE_STORE = 'operationQueue';
IndexedDBService.SYNC_STATE_STORE = 'syncState';
//# sourceMappingURL=IndexedDBService.js.map