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,266 +1,181 @@
|
|||
import { ISync, EntityType, SyncStatus, IEventBus } from '../types/CalendarTypes';
|
||||
import { IEntityService } from './IEntityService';
|
||||
import { SyncPlugin } from './SyncPlugin';
|
||||
import { IndexedDBContext } from './IndexedDBContext';
|
||||
import { CoreEvents } from '../constants/CoreEvents';
|
||||
import { diff } from 'json-diff-ts';
|
||||
import { IEntitySavedPayload, IEntityDeletedPayload } from '../types/EventTypes';
|
||||
|
||||
/**
|
||||
* BaseEntityService<T extends ISync> - Abstract base class for all entity services
|
||||
*
|
||||
* HYBRID PATTERN: Inheritance + Composition
|
||||
* - Services EXTEND this base class (inheritance for structure)
|
||||
* - Sync logic is COMPOSED via SyncPlugin (pluggable)
|
||||
*
|
||||
* PROVIDES:
|
||||
* - Generic CRUD operations (get, getAll, save, delete)
|
||||
* - Sync status management (delegates to SyncPlugin)
|
||||
* - Serialization hooks (override in subclass if needed)
|
||||
* - Lazy database access via IndexedDBContext
|
||||
*
|
||||
* SUBCLASSES MUST IMPLEMENT:
|
||||
* - storeName: string (IndexedDB object store name)
|
||||
* - entityType: EntityType (for runtime routing)
|
||||
*
|
||||
* SUBCLASSES MAY OVERRIDE:
|
||||
* - serialize(entity: T): any (default: no serialization)
|
||||
* - deserialize(data: any): T (default: no deserialization)
|
||||
*
|
||||
* BENEFITS:
|
||||
* - DRY: Single source of truth for CRUD logic
|
||||
* - Type safety: Generic T ensures compile-time checking
|
||||
* - Pluggable: SyncPlugin can be swapped for testing/different implementations
|
||||
* - Open/Closed: New entities just extend this class
|
||||
* - Lazy database access: db requested when needed, not at construction time
|
||||
*/
|
||||
export abstract class BaseEntityService<T extends ISync> implements IEntityService<T> {
|
||||
// Abstract properties - must be implemented by subclasses
|
||||
abstract readonly storeName: string;
|
||||
abstract readonly entityType: EntityType;
|
||||
|
||||
// Internal composition - sync functionality
|
||||
private syncPlugin: SyncPlugin<T>;
|
||||
|
||||
// IndexedDB context - provides database connection
|
||||
private context: IndexedDBContext;
|
||||
|
||||
// EventBus for emitting entity events
|
||||
protected eventBus: IEventBus;
|
||||
|
||||
/**
|
||||
* @param context - IndexedDBContext instance (injected dependency)
|
||||
* @param eventBus - EventBus for emitting entity events
|
||||
*/
|
||||
constructor(context: IndexedDBContext, eventBus: IEventBus) {
|
||||
this.context = context;
|
||||
this.eventBus = eventBus;
|
||||
this.syncPlugin = new SyncPlugin<T>(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get IDBDatabase instance (lazy access)
|
||||
* Protected getter accessible to subclasses and methods in this class
|
||||
*/
|
||||
protected get db(): IDBDatabase {
|
||||
return this.context.getDatabase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize entity before storing in IndexedDB
|
||||
* Override in subclass if entity has Date fields or needs transformation
|
||||
*
|
||||
* @param entity - Entity to serialize
|
||||
* @returns Serialized data (default: entity itself)
|
||||
*/
|
||||
protected serialize(entity: T): any {
|
||||
return entity; // Default: no serialization
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize data from IndexedDB back to entity
|
||||
* Override in subclass if entity has Date fields or needs transformation
|
||||
*
|
||||
* @param data - Raw data from IndexedDB
|
||||
* @returns Deserialized entity (default: data itself)
|
||||
*/
|
||||
protected deserialize(data: any): T {
|
||||
return data as T; // Default: no deserialization
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single entity by ID
|
||||
*
|
||||
* @param id - Entity ID
|
||||
* @returns Entity or null if not found
|
||||
*/
|
||||
async get(id: string): Promise<T | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.storeName], 'readonly');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const request = store.get(id);
|
||||
|
||||
request.onsuccess = () => {
|
||||
const data = request.result;
|
||||
if (data) {
|
||||
resolve(this.deserialize(data));
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to get ${this.entityType} ${id}: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all entities
|
||||
*
|
||||
* @returns Array of all entities
|
||||
*/
|
||||
async getAll(): Promise<T[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.storeName], 'readonly');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const request = store.getAll();
|
||||
|
||||
request.onsuccess = () => {
|
||||
const data = request.result as any[];
|
||||
const entities = data.map(item => this.deserialize(item));
|
||||
resolve(entities);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to get all ${this.entityType}s: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Save an entity (create or update)
|
||||
* Emits ENTITY_SAVED event with operation type and changes
|
||||
*
|
||||
* @param entity - Entity to save
|
||||
*/
|
||||
async save(entity: T): Promise<void> {
|
||||
const entityId = (entity as any).id;
|
||||
|
||||
// Check if entity exists to determine create vs update
|
||||
const existingEntity = await this.get(entityId);
|
||||
const isCreate = existingEntity === null;
|
||||
|
||||
// Calculate changes: full entity for create, diff for update
|
||||
let changes: any;
|
||||
if (isCreate) {
|
||||
changes = entity;
|
||||
} else {
|
||||
// Calculate diff between existing and new entity
|
||||
const existingSerialized = this.serialize(existingEntity);
|
||||
const newSerialized = this.serialize(entity);
|
||||
changes = diff(existingSerialized, newSerialized);
|
||||
}
|
||||
|
||||
const serialized = this.serialize(entity);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.storeName], 'readwrite');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const request = store.put(serialized);
|
||||
|
||||
request.onsuccess = () => {
|
||||
// Emit ENTITY_SAVED event
|
||||
const payload: IEntitySavedPayload = {
|
||||
entityType: this.entityType,
|
||||
entityId,
|
||||
operation: isCreate ? 'create' : 'update',
|
||||
changes,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
this.eventBus.emit(CoreEvents.ENTITY_SAVED, payload);
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to save ${this.entityType} ${entityId}: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an entity
|
||||
* Emits ENTITY_DELETED event
|
||||
*
|
||||
* @param id - Entity ID to delete
|
||||
*/
|
||||
async delete(id: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.storeName], 'readwrite');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const request = store.delete(id);
|
||||
|
||||
request.onsuccess = () => {
|
||||
// Emit ENTITY_DELETED event
|
||||
const payload: IEntityDeletedPayload = {
|
||||
entityType: this.entityType,
|
||||
entityId: id,
|
||||
operation: 'delete',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
this.eventBus.emit(CoreEvents.ENTITY_DELETED, payload);
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to delete ${this.entityType} ${id}: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SYNC METHODS (IEntityService implementation) - Delegates to SyncPlugin
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Mark entity as successfully synced (IEntityService implementation)
|
||||
* Delegates to SyncPlugin
|
||||
*
|
||||
* @param id - Entity ID
|
||||
*/
|
||||
async markAsSynced(id: string): Promise<void> {
|
||||
return this.syncPlugin.markAsSynced(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark entity as sync error (IEntityService implementation)
|
||||
* Delegates to SyncPlugin
|
||||
*
|
||||
* @param id - Entity ID
|
||||
*/
|
||||
async markAsError(id: string): Promise<void> {
|
||||
return this.syncPlugin.markAsError(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sync status for an entity (IEntityService implementation)
|
||||
* Delegates to SyncPlugin
|
||||
*
|
||||
* @param id - Entity ID
|
||||
* @returns SyncStatus or null if entity not found
|
||||
*/
|
||||
async getSyncStatus(id: string): Promise<SyncStatus | null> {
|
||||
return this.syncPlugin.getSyncStatus(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get entities by sync status
|
||||
* Delegates to SyncPlugin - uses IndexedDB syncStatus index
|
||||
*
|
||||
* @param syncStatus - Sync status ('synced', 'pending', 'error')
|
||||
* @returns Array of entities with this sync status
|
||||
*/
|
||||
async getBySyncStatus(syncStatus: string): Promise<T[]> {
|
||||
return this.syncPlugin.getBySyncStatus(syncStatus);
|
||||
}
|
||||
}
|
||||
import { ISync, EntityType, SyncStatus, IEventBus, IEntitySavedPayload, IEntityDeletedPayload } from '../types/CalendarTypes';
|
||||
import { IEntityService } from './IEntityService';
|
||||
import { SyncPlugin } from './SyncPlugin';
|
||||
import { IndexedDBContext } from './IndexedDBContext';
|
||||
import { CoreEvents } from '../constants/CoreEvents';
|
||||
import { diff } from 'json-diff-ts';
|
||||
|
||||
/**
|
||||
* BaseEntityService<T extends ISync> - Abstract base class for all entity services
|
||||
*
|
||||
* PROVIDES:
|
||||
* - Generic CRUD operations (get, getAll, save, delete)
|
||||
* - Sync status management (delegates to SyncPlugin)
|
||||
* - Serialization hooks (override in subclass if needed)
|
||||
*/
|
||||
export abstract class BaseEntityService<T extends ISync> implements IEntityService<T> {
|
||||
abstract readonly storeName: string;
|
||||
abstract readonly entityType: EntityType;
|
||||
|
||||
private syncPlugin: SyncPlugin<T>;
|
||||
private context: IndexedDBContext;
|
||||
protected eventBus: IEventBus;
|
||||
|
||||
constructor(context: IndexedDBContext, eventBus: IEventBus) {
|
||||
this.context = context;
|
||||
this.eventBus = eventBus;
|
||||
this.syncPlugin = new SyncPlugin<T>(this);
|
||||
}
|
||||
|
||||
protected get db(): IDBDatabase {
|
||||
return this.context.getDatabase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize entity before storing in IndexedDB
|
||||
*/
|
||||
protected serialize(entity: T): unknown {
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize data from IndexedDB back to entity
|
||||
*/
|
||||
protected deserialize(data: unknown): T {
|
||||
return data as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single entity by ID
|
||||
*/
|
||||
async get(id: string): Promise<T | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.storeName], 'readonly');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const request = store.get(id);
|
||||
|
||||
request.onsuccess = () => {
|
||||
const data = request.result;
|
||||
resolve(data ? this.deserialize(data) : null);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to get ${this.entityType} ${id}: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all entities
|
||||
*/
|
||||
async getAll(): Promise<T[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.storeName], 'readonly');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const request = store.getAll();
|
||||
|
||||
request.onsuccess = () => {
|
||||
const data = request.result as unknown[];
|
||||
const entities = data.map(item => this.deserialize(item));
|
||||
resolve(entities);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to get all ${this.entityType}s: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Save an entity (create or update)
|
||||
* Emits ENTITY_SAVED event with operation type and changes (diff for updates)
|
||||
* @param entity - Entity to save
|
||||
* @param silent - If true, skip event emission (used for seeding)
|
||||
*/
|
||||
async save(entity: T, silent = false): Promise<void> {
|
||||
const entityId = (entity as unknown as { id: string }).id;
|
||||
const existingEntity = await this.get(entityId);
|
||||
const isCreate = existingEntity === null;
|
||||
|
||||
// Calculate changes: full entity for create, diff for update
|
||||
let changes: unknown;
|
||||
if (isCreate) {
|
||||
changes = entity;
|
||||
} else {
|
||||
const existingSerialized = this.serialize(existingEntity);
|
||||
const newSerialized = this.serialize(entity);
|
||||
changes = diff(existingSerialized, newSerialized);
|
||||
}
|
||||
|
||||
const serialized = this.serialize(entity);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.storeName], 'readwrite');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const request = store.put(serialized);
|
||||
|
||||
request.onsuccess = () => {
|
||||
// Only emit event if not silent (silent used for seeding)
|
||||
if (!silent) {
|
||||
const payload: IEntitySavedPayload = {
|
||||
entityType: this.entityType,
|
||||
entityId,
|
||||
operation: isCreate ? 'create' : 'update',
|
||||
changes,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
this.eventBus.emit(CoreEvents.ENTITY_SAVED, payload);
|
||||
}
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to save ${this.entityType} ${entityId}: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an entity
|
||||
* Emits ENTITY_DELETED event
|
||||
*/
|
||||
async delete(id: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.storeName], 'readwrite');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const request = store.delete(id);
|
||||
|
||||
request.onsuccess = () => {
|
||||
const payload: IEntityDeletedPayload = {
|
||||
entityType: this.entityType,
|
||||
entityId: id,
|
||||
operation: 'delete',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
this.eventBus.emit(CoreEvents.ENTITY_DELETED, payload);
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to delete ${this.entityType} ${id}: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Sync methods - delegate to SyncPlugin
|
||||
async markAsSynced(id: string): Promise<void> {
|
||||
return this.syncPlugin.markAsSynced(id);
|
||||
}
|
||||
|
||||
async markAsError(id: string): Promise<void> {
|
||||
return this.syncPlugin.markAsError(id);
|
||||
}
|
||||
|
||||
async getSyncStatus(id: string): Promise<SyncStatus | null> {
|
||||
return this.syncPlugin.getSyncStatus(id);
|
||||
}
|
||||
|
||||
async getBySyncStatus(syncStatus: string): Promise<T[]> {
|
||||
return this.syncPlugin.getBySyncStatus(syncStatus);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue