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:
parent
2aa9d06fab
commit
8e52d670d6
30 changed files with 1960 additions and 526 deletions
|
|
@ -1,14 +1,28 @@
|
|||
import { IEventBus } from '../types/CalendarTypes';
|
||||
import { IEventBus, EntityType, ISync } from '../types/CalendarTypes';
|
||||
import { CoreEvents } from '../constants/CoreEvents';
|
||||
import { OperationQueue } from '../storage/OperationQueue';
|
||||
import { IQueueOperation } from '../storage/IndexedDBService';
|
||||
import { IndexedDBService } from '../storage/IndexedDBService';
|
||||
import { ApiEventRepository } from '../repositories/ApiEventRepository';
|
||||
import { IApiRepository } from '../repositories/IApiRepository';
|
||||
import { IEntityService } from '../storage/IEntityService';
|
||||
|
||||
/**
|
||||
* SyncManager - Background sync worker
|
||||
* Processes operation queue and syncs with API when online
|
||||
*
|
||||
* GENERIC ARCHITECTURE:
|
||||
* - Handles all entity types (Event, Booking, Customer, Resource)
|
||||
* - Routes operations based on IQueueOperation.dataEntity.typename
|
||||
* - Uses IApiRepository<T> pattern for type-safe API calls
|
||||
* - Uses IEntityService<T> polymorphism for sync status management
|
||||
*
|
||||
* POLYMORFI DESIGN:
|
||||
* - Services implement IEntityService<T extends ISync> interface
|
||||
* - SyncManager uses Array.find() for service lookup (simple, only 4 entities)
|
||||
* - Services encapsulate sync status manipulation (markAsSynced, markAsError)
|
||||
* - SyncManager does NOT manipulate entity.syncStatus directly
|
||||
* - Open/Closed Principle: Adding new entity requires only DI registration
|
||||
*
|
||||
* Features:
|
||||
* - Monitors online/offline status
|
||||
* - Processes queue with FIFO order
|
||||
|
|
@ -20,7 +34,8 @@ export class SyncManager {
|
|||
private eventBus: IEventBus;
|
||||
private queue: OperationQueue;
|
||||
private indexedDB: IndexedDBService;
|
||||
private apiRepository: ApiEventRepository;
|
||||
private repositories: Map<EntityType, IApiRepository<any>>;
|
||||
private entityServices: IEntityService<any>[];
|
||||
|
||||
private isOnline: boolean = navigator.onLine;
|
||||
private isSyncing: boolean = false;
|
||||
|
|
@ -32,16 +47,22 @@ export class SyncManager {
|
|||
eventBus: IEventBus,
|
||||
queue: OperationQueue,
|
||||
indexedDB: IndexedDBService,
|
||||
apiRepository: ApiEventRepository
|
||||
apiRepositories: IApiRepository<any>[],
|
||||
entityServices: IEntityService<any>[]
|
||||
) {
|
||||
this.eventBus = eventBus;
|
||||
this.queue = queue;
|
||||
this.indexedDB = indexedDB;
|
||||
this.apiRepository = apiRepository;
|
||||
this.entityServices = entityServices;
|
||||
|
||||
// Build map: EntityType → IApiRepository
|
||||
this.repositories = new Map(
|
||||
apiRepositories.map(repo => [repo.entityType, repo])
|
||||
);
|
||||
|
||||
this.setupNetworkListeners();
|
||||
this.startSync();
|
||||
console.log('SyncManager initialized and started');
|
||||
console.log(`SyncManager initialized with ${apiRepositories.length} entity repositories and ${entityServices.length} entity services`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -147,13 +168,22 @@ export class SyncManager {
|
|||
|
||||
/**
|
||||
* Process a single operation
|
||||
* Generic - routes to correct API repository based on entity type
|
||||
*/
|
||||
private async processOperation(operation: IQueueOperation): Promise<void> {
|
||||
// Check if max retries exceeded
|
||||
if (operation.retryCount >= this.maxRetries) {
|
||||
console.error(`SyncManager: Max retries exceeded for operation ${operation.id}`, operation);
|
||||
await this.queue.remove(operation.id);
|
||||
await this.markEventAsError(operation.eventId);
|
||||
await this.markEntityAsError(operation.dataEntity.typename, operation.entityId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the appropriate API repository for this entity type
|
||||
const repository = this.repositories.get(operation.dataEntity.typename);
|
||||
if (!repository) {
|
||||
console.error(`SyncManager: No repository found for entity type ${operation.dataEntity.typename}`);
|
||||
await this.queue.remove(operation.id);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -161,15 +191,15 @@ export class SyncManager {
|
|||
// Send to API based on operation type
|
||||
switch (operation.type) {
|
||||
case 'create':
|
||||
await this.apiRepository.sendCreate(operation.data as any);
|
||||
await repository.sendCreate(operation.dataEntity.data);
|
||||
break;
|
||||
|
||||
case 'update':
|
||||
await this.apiRepository.sendUpdate(operation.eventId, operation.data);
|
||||
await repository.sendUpdate(operation.entityId, operation.dataEntity.data);
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
await this.apiRepository.sendDelete(operation.eventId);
|
||||
await repository.sendDelete(operation.entityId);
|
||||
break;
|
||||
|
||||
default:
|
||||
|
|
@ -180,9 +210,9 @@ export class SyncManager {
|
|||
|
||||
// Success - remove from queue and mark as synced
|
||||
await this.queue.remove(operation.id);
|
||||
await this.markEventAsSynced(operation.eventId);
|
||||
await this.markEntityAsSynced(operation.dataEntity.typename, operation.entityId);
|
||||
|
||||
console.log(`SyncManager: Successfully synced operation ${operation.id}`);
|
||||
console.log(`SyncManager: Successfully synced ${operation.dataEntity.typename} operation ${operation.id}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`SyncManager: Failed to sync operation ${operation.id}:`, error);
|
||||
|
|
@ -202,32 +232,38 @@ export class SyncManager {
|
|||
}
|
||||
|
||||
/**
|
||||
* Mark event as synced in IndexedDB
|
||||
* Mark entity as synced in IndexedDB
|
||||
* Uses polymorphism - delegates to IEntityService.markAsSynced()
|
||||
*/
|
||||
private async markEventAsSynced(eventId: string): Promise<void> {
|
||||
private async markEntityAsSynced(entityType: EntityType, entityId: string): Promise<void> {
|
||||
try {
|
||||
const event = await this.indexedDB.getEvent(eventId);
|
||||
if (event) {
|
||||
event.syncStatus = 'synced';
|
||||
await this.indexedDB.saveEvent(event);
|
||||
const service = this.entityServices.find(s => s.entityType === entityType);
|
||||
if (!service) {
|
||||
console.error(`SyncManager: No service found for entity type ${entityType}`);
|
||||
return;
|
||||
}
|
||||
|
||||
await service.markAsSynced(entityId);
|
||||
} catch (error) {
|
||||
console.error(`SyncManager: Failed to mark event ${eventId} as synced:`, error);
|
||||
console.error(`SyncManager: Failed to mark ${entityType} ${entityId} as synced:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark event as error in IndexedDB
|
||||
* Mark entity as error in IndexedDB
|
||||
* Uses polymorphism - delegates to IEntityService.markAsError()
|
||||
*/
|
||||
private async markEventAsError(eventId: string): Promise<void> {
|
||||
private async markEntityAsError(entityType: EntityType, entityId: string): Promise<void> {
|
||||
try {
|
||||
const event = await this.indexedDB.getEvent(eventId);
|
||||
if (event) {
|
||||
event.syncStatus = 'error';
|
||||
await this.indexedDB.saveEvent(event);
|
||||
const service = this.entityServices.find(s => s.entityType === entityType);
|
||||
if (!service) {
|
||||
console.error(`SyncManager: No service found for entity type ${entityType}`);
|
||||
return;
|
||||
}
|
||||
|
||||
await service.markAsError(entityId);
|
||||
} catch (error) {
|
||||
console.error(`SyncManager: Failed to mark event ${eventId} as error:`, error);
|
||||
console.error(`SyncManager: Failed to mark ${entityType} ${entityId} as error:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue