146 lines
3.9 KiB
TypeScript
146 lines
3.9 KiB
TypeScript
|
|
import { ICalendarEvent } from '../types/CalendarTypes';
|
||
|
|
import { IEventRepository, UpdateSource } from './IEventRepository';
|
||
|
|
import { IndexedDBService } from '../storage/IndexedDBService';
|
||
|
|
import { OperationQueue } from '../storage/OperationQueue';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* IndexedDBEventRepository
|
||
|
|
* Offline-first repository using IndexedDB as single source of truth
|
||
|
|
*
|
||
|
|
* All CRUD operations:
|
||
|
|
* - Save to IndexedDB immediately (always succeeds)
|
||
|
|
* - Add to sync queue if source is 'local'
|
||
|
|
* - Background SyncManager processes queue to sync with API
|
||
|
|
*/
|
||
|
|
export class IndexedDBEventRepository implements IEventRepository {
|
||
|
|
private indexedDB: IndexedDBService;
|
||
|
|
private queue: OperationQueue;
|
||
|
|
|
||
|
|
constructor(indexedDB: IndexedDBService, queue: OperationQueue) {
|
||
|
|
this.indexedDB = indexedDB;
|
||
|
|
this.queue = queue;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Load all events from IndexedDB
|
||
|
|
*/
|
||
|
|
async loadEvents(): Promise<ICalendarEvent[]> {
|
||
|
|
return await this.indexedDB.getAllEvents();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create a new event
|
||
|
|
* - Generates ID
|
||
|
|
* - Saves to IndexedDB
|
||
|
|
* - Adds to queue if local (needs sync)
|
||
|
|
*/
|
||
|
|
async createEvent(event: Omit<ICalendarEvent, 'id'>, source: UpdateSource = 'local'): Promise<ICalendarEvent> {
|
||
|
|
// Generate unique ID
|
||
|
|
const id = this.generateEventId();
|
||
|
|
|
||
|
|
// Determine sync status based on source
|
||
|
|
const syncStatus = source === 'local' ? 'pending' : 'synced';
|
||
|
|
|
||
|
|
// Create full event object
|
||
|
|
const newEvent: ICalendarEvent = {
|
||
|
|
...event,
|
||
|
|
id,
|
||
|
|
syncStatus
|
||
|
|
} as ICalendarEvent;
|
||
|
|
|
||
|
|
// Save to IndexedDB
|
||
|
|
await this.indexedDB.saveEvent(newEvent);
|
||
|
|
|
||
|
|
// If local change, add to sync queue
|
||
|
|
if (source === 'local') {
|
||
|
|
await this.queue.enqueue({
|
||
|
|
type: 'create',
|
||
|
|
eventId: id,
|
||
|
|
data: newEvent,
|
||
|
|
timestamp: Date.now(),
|
||
|
|
retryCount: 0
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
return newEvent;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Update an existing event
|
||
|
|
* - Updates in IndexedDB
|
||
|
|
* - Adds to queue if local (needs sync)
|
||
|
|
*/
|
||
|
|
async updateEvent(id: string, updates: Partial<ICalendarEvent>, source: UpdateSource = 'local'): Promise<ICalendarEvent> {
|
||
|
|
// Get existing event
|
||
|
|
const existingEvent = await this.indexedDB.getEvent(id);
|
||
|
|
if (!existingEvent) {
|
||
|
|
throw new Error(`Event with ID ${id} not found`);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Determine sync status based on source
|
||
|
|
const syncStatus = source === 'local' ? 'pending' : 'synced';
|
||
|
|
|
||
|
|
// Merge updates
|
||
|
|
const updatedEvent: ICalendarEvent = {
|
||
|
|
...existingEvent,
|
||
|
|
...updates,
|
||
|
|
id, // Ensure ID doesn't change
|
||
|
|
syncStatus
|
||
|
|
};
|
||
|
|
|
||
|
|
// Save to IndexedDB
|
||
|
|
await this.indexedDB.saveEvent(updatedEvent);
|
||
|
|
|
||
|
|
// If local change, add to sync queue
|
||
|
|
if (source === 'local') {
|
||
|
|
await this.queue.enqueue({
|
||
|
|
type: 'update',
|
||
|
|
eventId: id,
|
||
|
|
data: updates,
|
||
|
|
timestamp: Date.now(),
|
||
|
|
retryCount: 0
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
return updatedEvent;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Delete an event
|
||
|
|
* - Removes from IndexedDB
|
||
|
|
* - Adds to queue if local (needs sync)
|
||
|
|
*/
|
||
|
|
async deleteEvent(id: string, source: UpdateSource = 'local'): Promise<void> {
|
||
|
|
// Check if event exists
|
||
|
|
const existingEvent = await this.indexedDB.getEvent(id);
|
||
|
|
if (!existingEvent) {
|
||
|
|
throw new Error(`Event with ID ${id} not found`);
|
||
|
|
}
|
||
|
|
|
||
|
|
// If local change, add to sync queue BEFORE deleting
|
||
|
|
// (so we can send the delete operation to API later)
|
||
|
|
if (source === 'local') {
|
||
|
|
await this.queue.enqueue({
|
||
|
|
type: 'delete',
|
||
|
|
eventId: id,
|
||
|
|
data: {}, // No data needed for delete
|
||
|
|
timestamp: Date.now(),
|
||
|
|
retryCount: 0
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Delete from IndexedDB
|
||
|
|
await this.indexedDB.deleteEvent(id);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Generate unique event ID
|
||
|
|
* Format: {timestamp}-{random}
|
||
|
|
*/
|
||
|
|
private generateEventId(): string {
|
||
|
|
const timestamp = Date.now();
|
||
|
|
const random = Math.random().toString(36).substring(2, 9);
|
||
|
|
return `${timestamp}-${random}`;
|
||
|
|
}
|
||
|
|
}
|