Implements offline-first calendar sync infrastructure
Adds IndexedDB and operation queue for robust offline synchronization Introduces SyncManager to handle background data synchronization Supports local event operations with automatic remote sync queuing Enhances application reliability and user experience in low/no connectivity scenarios
This commit is contained in:
parent
9c765b35ab
commit
e7011526e3
20 changed files with 3822 additions and 57 deletions
145
src/repositories/IndexedDBEventRepository.ts
Normal file
145
src/repositories/IndexedDBEventRepository.ts
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
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}`;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue