340 lines
No EOL
12 KiB
JavaScript
340 lines
No EOL
12 KiB
JavaScript
/**
|
|
* 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
|