Refactor entity services with hybrid sync pattern

Introduces BaseEntityService and SyncPlugin to eliminate code duplication across entity services

Improves:
- Code reusability through inheritance and composition
- Sync infrastructure for all entity types
- Polymorphic sync status management
- Reduced boilerplate code by ~75%

Supports generic sync for Event, Booking, Customer, and Resource entities
This commit is contained in:
Janus C. H. Knudsen 2025-11-18 16:37:33 +01:00
parent 2aa9d06fab
commit 8e52d670d6
30 changed files with 1960 additions and 526 deletions

View file

@ -1,6 +1,7 @@
import { ICalendarEvent } from '../types/CalendarTypes';
import { IEventRepository, UpdateSource } from './IEventRepository';
import { IndexedDBService } from '../storage/IndexedDBService';
import { EventService } from '../storage/events/EventService';
import { OperationQueue } from '../storage/OperationQueue';
/**
@ -8,31 +9,45 @@ import { OperationQueue } from '../storage/OperationQueue';
* Offline-first repository using IndexedDB as single source of truth
*
* All CRUD operations:
* - Save to IndexedDB immediately (always succeeds)
* - Save to IndexedDB immediately via EventService (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 eventService: EventService;
private queue: OperationQueue;
constructor(indexedDB: IndexedDBService, queue: OperationQueue) {
this.indexedDB = indexedDB;
this.queue = queue;
// EventService will be initialized after IndexedDB is ready
this.eventService = null as any;
}
/**
* Ensure EventService is initialized with database connection
*/
private ensureEventService(): void {
if (!this.eventService && this.indexedDB.isInitialized()) {
const db = (this.indexedDB as any).db; // Access private db property
this.eventService = new EventService(db);
}
}
/**
* Load all events from IndexedDB
* Ensures IndexedDB is initialized and seeded on first call
* Ensures IndexedDB is initialized on first call
*/
async loadEvents(): Promise<ICalendarEvent[]> {
// Lazy initialization on first data load
if (!this.indexedDB.isInitialized()) {
await this.indexedDB.initialize();
await this.indexedDB.seedIfEmpty();
// TODO: Seeding should be done at application level, not here
}
return await this.indexedDB.getAllEvents();
this.ensureEventService();
return await this.eventService.getAll();
}
/**
@ -55,15 +70,19 @@ export class IndexedDBEventRepository implements IEventRepository {
syncStatus
} as ICalendarEvent;
// Save to IndexedDB
await this.indexedDB.saveEvent(newEvent);
// Save to IndexedDB via EventService
this.ensureEventService();
await this.eventService.save(newEvent);
// If local change, add to sync queue
if (source === 'local') {
await this.queue.enqueue({
type: 'create',
eventId: id,
data: newEvent,
entityId: id,
dataEntity: {
typename: 'Event',
data: newEvent
},
timestamp: Date.now(),
retryCount: 0
});
@ -78,8 +97,9 @@ export class IndexedDBEventRepository implements IEventRepository {
* - 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);
// Get existing event via EventService
this.ensureEventService();
const existingEvent = await this.eventService.get(id);
if (!existingEvent) {
throw new Error(`Event with ID ${id} not found`);
}
@ -95,15 +115,18 @@ export class IndexedDBEventRepository implements IEventRepository {
syncStatus
};
// Save to IndexedDB
await this.indexedDB.saveEvent(updatedEvent);
// Save to IndexedDB via EventService
await this.eventService.save(updatedEvent);
// If local change, add to sync queue
if (source === 'local') {
await this.queue.enqueue({
type: 'update',
eventId: id,
data: updates,
entityId: id,
dataEntity: {
typename: 'Event',
data: updates
},
timestamp: Date.now(),
retryCount: 0
});
@ -118,8 +141,9 @@ export class IndexedDBEventRepository implements IEventRepository {
* - 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);
// Check if event exists via EventService
this.ensureEventService();
const existingEvent = await this.eventService.get(id);
if (!existingEvent) {
throw new Error(`Event with ID ${id} not found`);
}
@ -129,15 +153,18 @@ export class IndexedDBEventRepository implements IEventRepository {
if (source === 'local') {
await this.queue.enqueue({
type: 'delete',
eventId: id,
data: {}, // No data needed for delete
entityId: id,
dataEntity: {
typename: 'Event',
data: { id } // Minimal data for delete - just ID
},
timestamp: Date.now(),
retryCount: 0
});
}
// Delete from IndexedDB
await this.indexedDB.deleteEvent(id);
// Delete from IndexedDB via EventService
await this.eventService.delete(id);
}
/**