Calendar/coding-sessions/2025-11-22-audit-trail-event-driven-sync.md

532 lines
16 KiB
Markdown
Raw Permalink Normal View History

# Audit Trail & Event-Driven Sync Architecture
**Date:** 2025-11-22
**Duration:** ~4 hours
**Initial Scope:** Understand existing sync logic
**Actual Scope:** Complete audit-based sync architecture with event-driven design
---
## Executive Summary
Discovered that existing sync infrastructure (SyncManager, SyncPlugin, OperationQueue) was completely disconnected - nothing was wired together. Redesigned from scratch using audit-based architecture where all entity changes are logged to an audit store, and SyncManager listens for AUDIT_LOGGED events to sync to backend.
**Key Achievements:**
- ✅ Designed event-driven audit trail architecture
- ✅ Created AuditTypes, AuditStore, AuditService
- ✅ Updated BaseEntityService with JSON diff calculation (json-diff-ts)
- ✅ Implemented event emission on save/delete (ENTITY_SAVED, ENTITY_DELETED)
- ✅ Refactored SyncManager to listen for AUDIT_LOGGED events
- ✅ Fixed EventBus injection (required, not optional)
- ✅ Added typed Payload interfaces for all events
**Critical Discovery:** The "working" sync infrastructure was actually dead code - SyncManager was commented out, queue was never populated, and no events were being emitted.
---
## Context: Starting Point
### Previous Work (Nov 20, 2025)
Repository Layer Elimination session established:
- BaseEntityService<T> with generic CRUD operations
- IndexedDBContext for database connection
- Direct service usage pattern (no repository wrapper)
- DataSeeder for initial data loading
### The Gap
After repository elimination, we had:
- ✅ Services working (BaseEntityService + SyncPlugin)
- ✅ IndexedDB storing data
- ❌ SyncManager commented out
- ❌ OperationQueue never populated
- ❌ No events emitted on entity changes
- ❌ No audit trail for compliance
---
## Session Evolution: Major Architectural Decisions
### Phase 1: Discovery - Nothing Was Connected 🚨
**User Question:** *"What synchronization logic do we have for the server database?"*
**Investigation Findings:**
- SyncManager exists but was commented out in index.ts
- OperationQueue exists but never receives operations
- BaseEntityService.save() just saves - no events, no queue
- SyncPlugin provides sync status methods but nothing triggers them
**The Truth:** Entire sync infrastructure was scaffolding with no actual wiring.
---
### Phase 2: Architecture Discussion - Queue vs Audit
**User's Initial Mental Model:**
```
IEntityService.save() → saves to IndexedDB → emits event
SyncManager listens → reads pending from IndexedDB → syncs to backend
```
**Problem Identified:** "Who fills the queue?"
**Options Discussed:**
1. Service writes to queue
2. EventManager writes to queue
3. SyncManager reads pending from IndexedDB
**User Insight:** *"I need an audit trail for all changes."*
**Decision:** Drop OperationQueue concept, use Audit store instead.
---
### Phase 3: Audit Architecture Design ✅
**Requirements:**
- All entity changes must be logged (compliance)
- Changes should store JSON diff (not full entity)
- userId required (hardcoded GUID for now)
- Audit entries never deleted
**Designed Event Chain:**
```
Entity change
→ BaseEntityService.save()
→ emit ENTITY_SAVED (with diff)
→ AuditService listens
→ creates audit entry
→ emit AUDIT_LOGGED
→ SyncManager listens
→ syncs to backend
```
**Why Chained Events?**
User caught race condition: *"If both AuditService and SyncManager listen to ENTITY_SAVED, SyncManager could execute before the audit entry is created."*
Solution: AuditService emits AUDIT_LOGGED after saving, SyncManager only listens to AUDIT_LOGGED.
---
### Phase 4: Implementation - AuditTypes & AuditStore ✅
**Created src/types/AuditTypes.ts:**
```typescript
export interface IAuditEntry extends ISync {
id: string;
entityType: EntityType;
entityId: string;
operation: 'create' | 'update' | 'delete';
userId: string;
timestamp: number;
changes: any; // JSON diff result
synced: boolean;
syncStatus: 'synced' | 'pending' | 'error';
}
```
**Created src/storage/audit/AuditStore.ts:**
```typescript
export class AuditStore implements IStore {
readonly storeName = 'audit';
create(db: IDBDatabase): void {
const store = db.createObjectStore(this.storeName, { keyPath: 'id' });
store.createIndex('syncStatus', 'syncStatus', { unique: false });
store.createIndex('synced', 'synced', { unique: false });
store.createIndex('entityId', 'entityId', { unique: false });
store.createIndex('timestamp', 'timestamp', { unique: false });
}
}
```
---
### Phase 5: BaseEntityService - Diff Calculation & Event Emission ✅
**Added json-diff-ts dependency:**
```bash
npm install json-diff-ts
```
**Updated save() method:**
```typescript
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 {
const existingSerialized = this.serialize(existingEntity);
const newSerialized = this.serialize(entity);
changes = diff(existingSerialized, newSerialized);
}
// ... save to IndexedDB ...
// 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);
}
```
---
### Phase 6: AuditService - Override Pattern ✅
**Key Design Decision:** AuditService overrides save() to NOT emit ENTITY_SAVED.
**Why:** If AuditService.save() emitted ENTITY_SAVED, it would trigger AuditService again → infinite loop.
**Created src/storage/audit/AuditService.ts:**
```typescript
export class AuditService extends BaseEntityService<IAuditEntry> {
readonly storeName = 'audit';
readonly entityType: EntityType = 'Audit';
constructor(context: IndexedDBContext, eventBus: IEventBus) {
super(context, eventBus);
this.setupEventListeners();
}
private setupEventListeners(): void {
this.eventBus.on(CoreEvents.ENTITY_SAVED, (event: Event) => {
const detail = (event as CustomEvent).detail;
this.handleEntitySaved(detail);
});
this.eventBus.on(CoreEvents.ENTITY_DELETED, (event: Event) => {
const detail = (event as CustomEvent).detail;
this.handleEntityDeleted(detail);
});
}
// Override save to emit AUDIT_LOGGED instead of ENTITY_SAVED
async save(entity: IAuditEntry): Promise<void> {
// ... save to IndexedDB ...
// Emit AUDIT_LOGGED (not ENTITY_SAVED)
const payload: IAuditLoggedPayload = {
auditId: entity.id,
entityType: entity.entityType,
entityId: entity.entityId,
operation: entity.operation,
timestamp: entity.timestamp
};
this.eventBus.emit(CoreEvents.AUDIT_LOGGED, payload);
}
// Audit entries cannot be deleted (compliance)
async delete(_id: string): Promise<void> {
throw new Error('Audit entries cannot be deleted (compliance requirement)');
}
}
```
---
### Phase 7: SyncManager Refactoring ✅
**Before:** Used OperationQueue (never populated)
**After:** Listens to AUDIT_LOGGED, syncs audit entries
**Key Changes:**
```typescript
export class SyncManager {
constructor(
eventBus: IEventBus,
auditService: AuditService,
auditApiRepository: IApiRepository<IAuditEntry>
) {
this.setupAuditListener();
this.startSync();
}
private setupAuditListener(): void {
this.eventBus.on(CoreEvents.AUDIT_LOGGED, () => {
if (this.isOnline && !this.isSyncing) {
this.processPendingAudits();
}
});
}
private async processPendingAudits(): Promise<void> {
const pendingAudits = await this.auditService.getPendingAudits();
for (const audit of pendingAudits) {
await this.auditApiRepository.sendCreate(audit);
await this.auditService.markAsSynced(audit.id);
}
}
}
```
---
### Phase 8: EventBus Injection Problem 🐛
**Discovery:** Entity services had no EventBus!
**User Observation:** *"There's no EventBus being passed. Why are you using super(...arguments) when there's an empty constructor?"*
**Problem:**
- BaseEntityService had optional eventBus parameter
- Entity services (EventService, BookingService, etc.) had no constructors
- EventBus was never passed → events never emitted
**User Directive:** *"Remove the null check you added in BaseEntityService - it doesn't make sense."*
**Fix:**
1. Made eventBus required in BaseEntityService
2. Added constructors to all entity services:
```typescript
// EventService.ts
constructor(context: IndexedDBContext, eventBus: IEventBus) {
super(context, eventBus);
}
// BookingService.ts, CustomerService.ts, ResourceService.ts - same pattern
```
---
### Phase 9: Typed Payload Interfaces ✅
**User Observation:** *"The events you've created use anonymous types. I'd prefer typed interfaces following the existing Payload suffix convention."*
**Added to src/types/EventTypes.ts:**
```typescript
export interface IEntitySavedPayload {
entityType: EntityType;
entityId: string;
operation: 'create' | 'update';
changes: any;
timestamp: number;
}
export interface IEntityDeletedPayload {
entityType: EntityType;
entityId: string;
operation: 'delete';
timestamp: number;
}
export interface IAuditLoggedPayload {
auditId: string;
entityType: EntityType;
entityId: string;
operation: 'create' | 'update' | 'delete';
timestamp: number;
}
```
---
## Files Changed Summary
### Files Created (4)
1. **src/types/AuditTypes.ts** - IAuditEntry interface
2. **src/storage/audit/AuditStore.ts** - IndexedDB store for audit entries
3. **src/storage/audit/AuditService.ts** - Event-driven audit service
4. **src/repositories/MockAuditRepository.ts** - Mock API for audit sync
### Files Deleted (1)
5. **src/storage/OperationQueue.ts** - Replaced by audit-based approach
### Files Modified (9)
6. **src/types/CalendarTypes.ts** - Added 'Audit' to EntityType
7. **src/constants/CoreEvents.ts** - Added ENTITY_SAVED, ENTITY_DELETED, AUDIT_LOGGED
8. **src/types/EventTypes.ts** - Added IEntitySavedPayload, IEntityDeletedPayload, IAuditLoggedPayload
9. **src/storage/BaseEntityService.ts** - Added diff calculation, event emission, required eventBus
10. **src/storage/events/EventService.ts** - Added constructor with eventBus
11. **src/storage/bookings/BookingService.ts** - Added constructor with eventBus
12. **src/storage/customers/CustomerService.ts** - Added constructor with eventBus
13. **src/storage/resources/ResourceService.ts** - Added constructor with eventBus
14. **src/workers/SyncManager.ts** - Refactored to use AuditService
15. **src/storage/IndexedDBContext.ts** - Bumped DB version to 3
16. **src/index.ts** - Updated DI registrations
---
## Architecture Evolution Diagram
**BEFORE (Disconnected):**
```
EventManager
EventService.save()
IndexedDB (saved)
[OperationQueue - never filled]
[SyncManager - commented out]
```
**AFTER (Event-Driven):**
```
EventManager
EventService.save()
BaseEntityService.save()
├── IndexedDB (saved)
└── emit ENTITY_SAVED (with diff)
AuditService listens
├── Creates audit entry
└── emit AUDIT_LOGGED
SyncManager listens
└── Syncs to backend
```
---
## Key Design Decisions
### 1. Audit-Based Instead of Queue-Based
**Why:** Audit serves dual purpose - compliance trail AND sync source.
**Benefit:** Single source of truth for all changes.
### 2. Chained Events (ENTITY_SAVED → AUDIT_LOGGED)
**Why:** Prevents race condition where SyncManager runs before audit is saved.
**Benefit:** Guaranteed order of operations.
### 3. JSON Diff for Changes
**Why:** Only store what changed, not full entity.
**Benefit:** Smaller audit entries, easier to see what changed.
### 4. Override Pattern for AuditService
**Why:** Prevent infinite loop (audit → event → audit → event...).
**Benefit:** Clean separation without special flags.
### 5. Required EventBus (Not Optional)
**Why:** Events are core to architecture, not optional.
**Benefit:** No null checks, guaranteed behavior.
---
## Discussion Topics (Not Implemented)
### IndexedDB for Logging/Traces
User observation: *"This IndexedDB approach is quite interesting - we could extend it to handle logging, traces, and exceptions as well."*
**Potential Extension:**
- LogStore - Application logs
- TraceStore - Performance traces
- ExceptionStore - Caught/uncaught errors
**Console Interception Pattern:**
```typescript
const originalLog = console.log;
console.log = (...args) => {
logService.save({ level: 'info', message: args, timestamp: Date.now() });
if (isDevelopment) originalLog.apply(console, args);
};
```
**Cleanup Strategy:**
- 7-day retention
- Or max 10,000 entries with FIFO
**Decision:** Not implemented this session, but architecture supports it.
---
## Lessons Learned
### 1. Verify Existing Code Actually Works
The sync infrastructure looked complete but was completely disconnected.
**Lesson:** Don't assume existing code works - trace the actual flow.
### 2. Audit Trail Serves Multiple Purposes
Audit is not just for compliance - it's also the perfect sync source.
**Lesson:** Look for dual-purpose designs.
### 3. Event Ordering Matters
Race conditions between listeners are real.
**Lesson:** Use chained events when order matters.
### 4. Optional Dependencies Create Hidden Bugs
Optional eventBus meant events silently didn't fire.
**Lesson:** Make core dependencies required.
### 5. Type Consistency Matters
Anonymous types in events vs Payload interfaces elsewhere.
**Lesson:** Follow existing patterns in codebase.
---
## Current State & Next Steps
### ✅ Build Status: Successful
```
[NovaDI] Performance Summary:
- Files in TypeScript Program: 80
- Files actually transformed: 58
- Total: 1467.93ms
```
### ✅ Architecture State
- **Event-driven audit trail:** Complete
- **AuditService:** Listens for entity events, creates audit entries
- **SyncManager:** Listens for AUDIT_LOGGED, syncs to backend
- **BaseEntityService:** Emits events on save/delete with JSON diff
### ⚠️ Not Yet Tested
- Runtime behavior (does AuditService receive events?)
- Diff calculation accuracy
- SyncManager sync flow
- IndexedDB version upgrade (v2 → v3)
### 📋 Next Steps
**Immediate:**
1. Test in browser - verify events fire correctly
2. Check IndexedDB for audit entries after save
3. Verify SyncManager logs sync attempts
**Future:**
1. Real backend API for audit sync
2. Logging/traces extension (console interception)
3. Cleanup strategy for old audit entries
---
## Conclusion
**Initial Goal:** Understand sync logic
**Actual Work:** Complete architecture redesign
**What We Found:**
- Sync infrastructure was dead code
- OperationQueue never populated
- SyncManager commented out
- No events being emitted
**What We Built:**
- Audit-based sync architecture
- Event-driven design with chained events
- JSON diff for change tracking
- Typed payload interfaces
**Key Insight:** Sometimes "understanding existing code" reveals there's nothing to understand - just scaffolding that needs to be replaced with actual implementation.
---
**Session Complete:** 2025-11-22
**Documentation Quality:** High
**Ready for:** Runtime testing