Refactors calendar project structure and build configuration
Consolidates V2 codebase into main project directory Updates build script to support simplified entry points Removes redundant files and cleans up project organization Simplifies module imports and entry points for calendar application
This commit is contained in:
parent
9f360237cf
commit
863b433eba
200 changed files with 2331 additions and 16193 deletions
|
|
@ -1,103 +1,73 @@
|
|||
import { IApiRepository } from '../repositories/IApiRepository';
|
||||
import { IEntityService } from '../storage/IEntityService';
|
||||
|
||||
/**
|
||||
* DataSeeder - Orchestrates initial data loading from repositories into IndexedDB
|
||||
*
|
||||
* ARCHITECTURE:
|
||||
* - Repository (Mock/Api): Fetches data from source (JSON file or backend API)
|
||||
* - DataSeeder (this class): Orchestrates fetch + save operations
|
||||
* - Service (EventService, etc.): Saves data to IndexedDB
|
||||
*
|
||||
* SEPARATION OF CONCERNS:
|
||||
* - Repository does NOT know about IndexedDB or storage
|
||||
* - Service does NOT know about where data comes from
|
||||
* - DataSeeder connects them together
|
||||
*
|
||||
* POLYMORPHIC DESIGN:
|
||||
* - Uses arrays of IEntityService<any>[] and IApiRepository<any>[]
|
||||
* - Matches services with repositories using entityType property
|
||||
* - Open/Closed Principle: Adding new entity requires no code changes here
|
||||
*
|
||||
* USAGE:
|
||||
* Called once during app initialization in index.ts:
|
||||
* 1. IndexedDBService.initialize() - open database
|
||||
* 2. dataSeeder.seedIfEmpty() - load initial data if needed
|
||||
* 3. CalendarManager.initialize() - start calendar
|
||||
*
|
||||
* NOTE: This is for INITIAL SEEDING only. Ongoing sync is handled by SyncManager.
|
||||
*/
|
||||
export class DataSeeder {
|
||||
constructor(
|
||||
// Arrays injected via DI - automatically includes all registered services/repositories
|
||||
private services: IEntityService<any>[],
|
||||
private repositories: IApiRepository<any>[]
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Seed all entity stores if they are empty
|
||||
* Runs on app initialization to load initial data from repositories
|
||||
*
|
||||
* Uses polymorphism: loops through all services and matches with repositories by entityType
|
||||
*/
|
||||
async seedIfEmpty(): Promise<void> {
|
||||
console.log('[DataSeeder] Checking if database needs seeding...');
|
||||
|
||||
try {
|
||||
// Loop through all entity services (Event, Booking, Customer, Resource, etc.)
|
||||
for (const service of this.services) {
|
||||
// Find matching repository for this service based on entityType
|
||||
const repository = this.repositories.find(repo => repo.entityType === service.entityType);
|
||||
|
||||
if (!repository) {
|
||||
console.warn(`[DataSeeder] No repository found for entity type: ${service.entityType}, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Seed this entity type
|
||||
await this.seedEntity(service.entityType, service, repository);
|
||||
}
|
||||
|
||||
console.log('[DataSeeder] Seeding complete');
|
||||
} catch (error) {
|
||||
console.error('[DataSeeder] Seeding failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic method to seed a single entity type
|
||||
*
|
||||
* @param entityType - Entity type ('Event', 'Booking', 'Customer', 'Resource')
|
||||
* @param service - Entity service for IndexedDB operations
|
||||
* @param repository - Repository for fetching data
|
||||
*/
|
||||
private async seedEntity<T>(
|
||||
entityType: string,
|
||||
service: IEntityService<any>,
|
||||
repository: IApiRepository<T>
|
||||
): Promise<void> {
|
||||
// Check if store is empty
|
||||
const existing = await service.getAll();
|
||||
|
||||
if (existing.length > 0) {
|
||||
console.log(`[DataSeeder] ${entityType} store already has ${existing.length} items, skipping seed`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[DataSeeder] ${entityType} store is empty, fetching from repository...`);
|
||||
|
||||
// Fetch from repository (Mock JSON or backend API)
|
||||
const data = await repository.fetchAll();
|
||||
|
||||
console.log(`[DataSeeder] Fetched ${data.length} ${entityType} items, saving to IndexedDB...`);
|
||||
|
||||
// Save each entity to IndexedDB
|
||||
// Note: Entities from repository should already have syncStatus='synced'
|
||||
for (const entity of data) {
|
||||
await service.save(entity);
|
||||
}
|
||||
|
||||
console.log(`[DataSeeder] ${entityType} seeding complete (${data.length} items saved)`);
|
||||
}
|
||||
}
|
||||
import { IApiRepository } from '../repositories/IApiRepository';
|
||||
import { IEntityService } from '../storage/IEntityService';
|
||||
import { ISync } from '../types/CalendarTypes';
|
||||
|
||||
/**
|
||||
* DataSeeder - Orchestrates initial data loading from repositories into IndexedDB
|
||||
*
|
||||
* ARCHITECTURE:
|
||||
* - Repository (Mock/Api): Fetches data from source (JSON file or backend API)
|
||||
* - DataSeeder (this class): Orchestrates fetch + save operations
|
||||
* - Service (EventService, etc.): Saves data to IndexedDB
|
||||
*
|
||||
* POLYMORPHIC DESIGN:
|
||||
* - Uses arrays of IEntityService[] and IApiRepository[]
|
||||
* - Matches services with repositories using entityType property
|
||||
* - Open/Closed Principle: Adding new entity requires no code changes here
|
||||
*/
|
||||
export class DataSeeder {
|
||||
constructor(
|
||||
private services: IEntityService<ISync>[],
|
||||
private repositories: IApiRepository<ISync>[]
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Seed all entity stores if they are empty
|
||||
*/
|
||||
async seedIfEmpty(): Promise<void> {
|
||||
console.log('[DataSeeder] Checking if database needs seeding...');
|
||||
|
||||
try {
|
||||
for (const service of this.services) {
|
||||
const repository = this.repositories.find(repo => repo.entityType === service.entityType);
|
||||
|
||||
if (!repository) {
|
||||
console.warn(`[DataSeeder] No repository found for entity type: ${service.entityType}, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.seedEntity(service.entityType, service, repository);
|
||||
}
|
||||
|
||||
console.log('[DataSeeder] Seeding complete');
|
||||
} catch (error) {
|
||||
console.error('[DataSeeder] Seeding failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async seedEntity<T extends ISync>(
|
||||
entityType: string,
|
||||
service: IEntityService<T>,
|
||||
repository: IApiRepository<T>
|
||||
): Promise<void> {
|
||||
const existing = await service.getAll();
|
||||
|
||||
if (existing.length > 0) {
|
||||
console.log(`[DataSeeder] ${entityType} store already has ${existing.length} items, skipping seed`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[DataSeeder] ${entityType} store is empty, fetching from repository...`);
|
||||
|
||||
const data = await repository.fetchAll();
|
||||
|
||||
console.log(`[DataSeeder] Fetched ${data.length} ${entityType} items, saving to IndexedDB...`);
|
||||
|
||||
for (const entity of data) {
|
||||
await service.save(entity, true); // silent = true to skip audit logging
|
||||
}
|
||||
|
||||
console.log(`[DataSeeder] ${entityType} seeding complete (${data.length} items saved)`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,259 +0,0 @@
|
|||
import { IEventBus } from '../types/CalendarTypes';
|
||||
import { CoreEvents } from '../constants/CoreEvents';
|
||||
import { IAuditEntry } from '../types/AuditTypes';
|
||||
import { AuditService } from '../storage/audit/AuditService';
|
||||
import { IApiRepository } from '../repositories/IApiRepository';
|
||||
|
||||
/**
|
||||
* SyncManager - Background sync worker
|
||||
* Syncs audit entries with backend API when online
|
||||
*
|
||||
* NEW ARCHITECTURE:
|
||||
* - Listens to AUDIT_LOGGED events (triggered after AuditService saves)
|
||||
* - Polls AuditService for pending audit entries
|
||||
* - Syncs audit entries to backend API
|
||||
* - Marks audit entries as synced when successful
|
||||
*
|
||||
* EVENT CHAIN:
|
||||
* Entity change → ENTITY_SAVED/DELETED → AuditService → AUDIT_LOGGED → SyncManager
|
||||
*
|
||||
* Features:
|
||||
* - Monitors online/offline status
|
||||
* - Processes pending audits with FIFO order
|
||||
* - Exponential backoff retry logic
|
||||
* - Updates syncStatus in IndexedDB after successful sync
|
||||
* - Emits sync events for UI feedback
|
||||
*/
|
||||
export class SyncManager {
|
||||
private eventBus: IEventBus;
|
||||
private auditService: AuditService;
|
||||
private auditApiRepository: IApiRepository<IAuditEntry>;
|
||||
|
||||
private isOnline: boolean = navigator.onLine;
|
||||
private isSyncing: boolean = false;
|
||||
private syncInterval: number = 5000; // 5 seconds
|
||||
private maxRetries: number = 5;
|
||||
private intervalId: number | null = null;
|
||||
|
||||
// Track retry counts per audit entry (in memory)
|
||||
private retryCounts: Map<string, number> = new Map();
|
||||
|
||||
constructor(
|
||||
eventBus: IEventBus,
|
||||
auditService: AuditService,
|
||||
auditApiRepository: IApiRepository<IAuditEntry>
|
||||
) {
|
||||
this.eventBus = eventBus;
|
||||
this.auditService = auditService;
|
||||
this.auditApiRepository = auditApiRepository;
|
||||
|
||||
this.setupNetworkListeners();
|
||||
this.setupAuditListener();
|
||||
this.startSync();
|
||||
console.log('SyncManager initialized - listening for AUDIT_LOGGED events');
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup listener for AUDIT_LOGGED events
|
||||
* Triggers immediate sync attempt when new audit entry is saved
|
||||
*/
|
||||
private setupAuditListener(): void {
|
||||
this.eventBus.on(CoreEvents.AUDIT_LOGGED, () => {
|
||||
// New audit entry saved - try to sync if online
|
||||
if (this.isOnline && !this.isSyncing) {
|
||||
this.processPendingAudits();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup online/offline event listeners
|
||||
*/
|
||||
private setupNetworkListeners(): void {
|
||||
window.addEventListener('online', () => {
|
||||
this.isOnline = true;
|
||||
this.eventBus.emit(CoreEvents.OFFLINE_MODE_CHANGED, {
|
||||
isOnline: true
|
||||
});
|
||||
console.log('SyncManager: Network online - starting sync');
|
||||
this.startSync();
|
||||
});
|
||||
|
||||
window.addEventListener('offline', () => {
|
||||
this.isOnline = false;
|
||||
this.eventBus.emit(CoreEvents.OFFLINE_MODE_CHANGED, {
|
||||
isOnline: false
|
||||
});
|
||||
console.log('SyncManager: Network offline - pausing sync');
|
||||
this.stopSync();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start background sync worker
|
||||
*/
|
||||
public startSync(): void {
|
||||
if (this.intervalId) {
|
||||
return; // Already running
|
||||
}
|
||||
|
||||
console.log('SyncManager: Starting background sync');
|
||||
|
||||
// Process immediately
|
||||
this.processPendingAudits();
|
||||
|
||||
// Then poll every syncInterval
|
||||
this.intervalId = window.setInterval(() => {
|
||||
this.processPendingAudits();
|
||||
}, this.syncInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop background sync worker
|
||||
*/
|
||||
public stopSync(): void {
|
||||
if (this.intervalId) {
|
||||
window.clearInterval(this.intervalId);
|
||||
this.intervalId = null;
|
||||
console.log('SyncManager: Stopped background sync');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process pending audit entries
|
||||
* Fetches from AuditService and syncs to backend
|
||||
*/
|
||||
private async processPendingAudits(): Promise<void> {
|
||||
// Don't sync if offline
|
||||
if (!this.isOnline) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't start new sync if already syncing
|
||||
if (this.isSyncing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isSyncing = true;
|
||||
|
||||
try {
|
||||
const pendingAudits = await this.auditService.getPendingAudits();
|
||||
|
||||
if (pendingAudits.length === 0) {
|
||||
this.isSyncing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.eventBus.emit(CoreEvents.SYNC_STARTED, {
|
||||
operationCount: pendingAudits.length
|
||||
});
|
||||
|
||||
// Process audits one by one (FIFO - oldest first by timestamp)
|
||||
const sortedAudits = pendingAudits.sort((a, b) => a.timestamp - b.timestamp);
|
||||
|
||||
for (const audit of sortedAudits) {
|
||||
await this.processAuditEntry(audit);
|
||||
}
|
||||
|
||||
this.eventBus.emit(CoreEvents.SYNC_COMPLETED, {
|
||||
operationCount: pendingAudits.length
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('SyncManager: Audit processing error:', error);
|
||||
this.eventBus.emit(CoreEvents.SYNC_FAILED, {
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
} finally {
|
||||
this.isSyncing = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a single audit entry
|
||||
* Sends to backend API and marks as synced
|
||||
*/
|
||||
private async processAuditEntry(audit: IAuditEntry): Promise<void> {
|
||||
const retryCount = this.retryCounts.get(audit.id) || 0;
|
||||
|
||||
// Check if max retries exceeded
|
||||
if (retryCount >= this.maxRetries) {
|
||||
console.error(`SyncManager: Max retries exceeded for audit ${audit.id}`);
|
||||
await this.auditService.markAsError(audit.id);
|
||||
this.retryCounts.delete(audit.id);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Send audit entry to backend
|
||||
await this.auditApiRepository.sendCreate(audit);
|
||||
|
||||
// Success - mark as synced and clear retry count
|
||||
await this.auditService.markAsSynced(audit.id);
|
||||
this.retryCounts.delete(audit.id);
|
||||
|
||||
console.log(`SyncManager: Successfully synced audit ${audit.id} (${audit.entityType}:${audit.operation})`);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`SyncManager: Failed to sync audit ${audit.id}:`, error);
|
||||
|
||||
// Increment retry count
|
||||
this.retryCounts.set(audit.id, retryCount + 1);
|
||||
|
||||
// Calculate backoff delay
|
||||
const backoffDelay = this.calculateBackoff(retryCount + 1);
|
||||
|
||||
this.eventBus.emit(CoreEvents.SYNC_RETRY, {
|
||||
auditId: audit.id,
|
||||
retryCount: retryCount + 1,
|
||||
nextRetryIn: backoffDelay
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate exponential backoff delay
|
||||
* @param retryCount Current retry count
|
||||
* @returns Delay in milliseconds
|
||||
*/
|
||||
private calculateBackoff(retryCount: number): number {
|
||||
// Exponential backoff: 2^retryCount * 1000ms
|
||||
// Retry 1: 2s, Retry 2: 4s, Retry 3: 8s, Retry 4: 16s, Retry 5: 32s
|
||||
const baseDelay = 1000;
|
||||
const exponentialDelay = Math.pow(2, retryCount) * baseDelay;
|
||||
const maxDelay = 60000; // Max 1 minute
|
||||
return Math.min(exponentialDelay, maxDelay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually trigger sync (for testing or manual sync button)
|
||||
*/
|
||||
public async triggerManualSync(): Promise<void> {
|
||||
console.log('SyncManager: Manual sync triggered');
|
||||
await this.processPendingAudits();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current sync status
|
||||
*/
|
||||
public getSyncStatus(): {
|
||||
isOnline: boolean;
|
||||
isSyncing: boolean;
|
||||
isRunning: boolean;
|
||||
} {
|
||||
return {
|
||||
isOnline: this.isOnline,
|
||||
isSyncing: this.isSyncing,
|
||||
isRunning: this.intervalId !== null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup - stop sync and remove listeners
|
||||
*/
|
||||
public destroy(): void {
|
||||
this.stopSync();
|
||||
this.retryCounts.clear();
|
||||
// Note: We don't remove window event listeners as they're global
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue