Calendar/coding-sessions/2025-11-12-indexeddb-only-dom-optimization.md
Janus C. H. Knudsen b5dfd57d9e Migrates date handling from date-fns to day.js
Replaces date-fns library with day.js to reduce bundle size and improve tree-shaking

- Centralizes all date logic in DateService
- Reduces library footprint from 576 KB to 29 KB
- Maintains 99.4% test coverage during migration
- Adds timezone and formatting plugins for day.js

Improves overall library performance and reduces dependency complexity
2025-11-12 23:51:48 +01:00

345 lines
No EOL
9.7 KiB
Markdown

# IndexedDB-Only DOM Optimization Plan
**Date:** 2025-11-12
**Status:** Planning Phase
**Goal:** Reduce DOM data-attributes to only event ID, using IndexedDB as single source of truth
## Current Problem
Events currently store all data in DOM attributes:
```html
<swp-event
data-event-id="123"
data-title="Meeting"
data-description="Long description..."
data-start="2025-11-10T10:00:00Z"
data-end="2025-11-10T11:00:00Z"
data-type="work"
data-duration="60"
/>
```
**Issues:**
- Data duplication (IndexedDB + DOM)
- Synchronization complexity
- Large DOM size with descriptions
- Memory overhead
## Proposed Solution
### Architecture Principle
**Single Source of Truth: IndexedDB**
```mermaid
graph TB
A[IndexedDB] -->|getEvent| B[SwpEventElement]
B -->|Only stores| C[data-event-id]
B -->|Renders from| D[ICalendarEvent]
A -->|Provides| D
```
### Target DOM Structure
```html
<swp-event data-event-id="123" />
```
Only 1 attribute instead of 8+.
## Implementation Plan
### Phase 1: Refactor SwpEventElement
**File:** `src/elements/SwpEventElement.ts`
#### 1.1 Remove Getters/Setters
Remove all property getters/setters except `eventId`:
- ❌ Remove: `start`, `end`, `title`, `description`, `type`
- ✅ Keep: `eventId`
#### 1.2 Add IndexedDB Reference
```typescript
export class SwpEventElement extends BaseSwpEventElement {
private static indexedDB: IndexedDBService;
static setIndexedDB(db: IndexedDBService): void {
SwpEventElement.indexedDB = db;
}
}
```
#### 1.3 Implement Async Data Loading
```typescript
async connectedCallback() {
const event = await this.loadEventData();
if (event) {
await this.renderFromEvent(event);
}
}
private async loadEventData(): Promise<ICalendarEvent | null> {
return await SwpEventElement.indexedDB.getEvent(this.eventId);
}
```
#### 1.4 Update Render Method
```typescript
private async renderFromEvent(event: ICalendarEvent): Promise<void> {
const timeRange = TimeFormatter.formatTimeRange(event.start, event.end);
const durationMinutes = (event.end.getTime() - event.start.getTime()) / (1000 * 60);
this.innerHTML = `
<swp-event-time data-duration="${durationMinutes}">${timeRange}</swp-event-time>
<swp-event-title>${event.title}</swp-event-title>
${event.description ? `<swp-event-description>${event.description}</swp-event-description>` : ''}
`;
}
```
### Phase 2: Update Factory Method
**File:** `src/elements/SwpEventElement.ts` (line 284)
```typescript
public static fromCalendarEvent(event: ICalendarEvent): SwpEventElement {
const element = document.createElement('swp-event') as SwpEventElement;
// Only set event ID - all other data comes from IndexedDB
element.dataset.eventId = event.id;
return element;
}
```
### Phase 3: Update Extract Method
**File:** `src/elements/SwpEventElement.ts` (line 303)
```typescript
public static async extractCalendarEventFromElement(element: HTMLElement): Promise<ICalendarEvent | null> {
const eventId = element.dataset.eventId;
if (!eventId) return null;
// Load from IndexedDB instead of reading from DOM
return await SwpEventElement.indexedDB.getEvent(eventId);
}
```
### Phase 4: Update Position Updates
**File:** `src/elements/SwpEventElement.ts` (line 117)
```typescript
public async updatePosition(columnDate: Date, snappedY: number): Promise<void> {
// 1. Update visual position
this.style.top = `${snappedY + 1}px`;
// 2. Load current event data from IndexedDB
const event = await this.loadEventData();
if (!event) return;
// 3. Calculate new timestamps
const { startMinutes, endMinutes } = this.calculateTimesFromPosition(snappedY, event);
// 4. Create new dates
const startDate = this.dateService.createDateAtTime(columnDate, startMinutes);
let endDate = this.dateService.createDateAtTime(columnDate, endMinutes);
// Handle cross-midnight
if (endMinutes >= 1440) {
const extraDays = Math.floor(endMinutes / 1440);
endDate = this.dateService.addDays(endDate, extraDays);
}
// 5. Update in IndexedDB
const updatedEvent = { ...event, start: startDate, end: endDate };
await SwpEventElement.indexedDB.saveEvent(updatedEvent);
// 6. Re-render from updated data
await this.renderFromEvent(updatedEvent);
}
```
### Phase 5: Update Height Updates
**File:** `src/elements/SwpEventElement.ts` (line 142)
```typescript
public async updateHeight(newHeight: number): Promise<void> {
// 1. Update visual height
this.style.height = `${newHeight}px`;
// 2. Load current event
const event = await this.loadEventData();
if (!event) return;
// 3. Calculate new end time
const gridSettings = this.config.gridSettings;
const { hourHeight, snapInterval } = gridSettings;
const rawDurationMinutes = (newHeight / hourHeight) * 60;
const snappedDurationMinutes = Math.round(rawDurationMinutes / snapInterval) * snapInterval;
const endDate = this.dateService.addMinutes(event.start, snappedDurationMinutes);
// 4. Update in IndexedDB
const updatedEvent = { ...event, end: endDate };
await SwpEventElement.indexedDB.saveEvent(updatedEvent);
// 5. Re-render
await this.renderFromEvent(updatedEvent);
}
```
### Phase 6: Update Calculate Times
**File:** `src/elements/SwpEventElement.ts` (line 255)
```typescript
private calculateTimesFromPosition(snappedY: number, event: ICalendarEvent): { startMinutes: number; endMinutes: number } {
const gridSettings = this.config.gridSettings;
const { hourHeight, dayStartHour, snapInterval } = gridSettings;
// Calculate original duration from event data
const originalDuration = (event.end.getTime() - event.start.getTime()) / (1000 * 60);
// Calculate snapped start minutes
const minutesFromGridStart = (snappedY / hourHeight) * 60;
const actualStartMinutes = (dayStartHour * 60) + minutesFromGridStart;
const snappedStartMinutes = Math.round(actualStartMinutes / snapInterval) * snapInterval;
// Calculate end minutes
const endMinutes = snappedStartMinutes + originalDuration;
return { startMinutes: snappedStartMinutes, endMinutes };
}
```
### Phase 7: Update DragDropManager
**File:** `src/managers/DragDropManager.ts`
All places reading from `element.dataset.start`, `element.dataset.end` etc. must change to:
```typescript
// Before:
const start = new Date(element.dataset.start);
const end = new Date(element.dataset.end);
// After:
const event = await SwpEventElement.extractCalendarEventFromElement(element);
if (!event) return;
const start = event.start;
const end = event.end;
```
### Phase 8: Update Clone Method
**File:** `src/elements/SwpEventElement.ts` (line 169)
```typescript
public async createClone(): Promise<SwpEventElement> {
const clone = this.cloneNode(true) as SwpEventElement;
// Apply "clone-" prefix to ID
clone.dataset.eventId = `clone-${this.eventId}`;
// Disable pointer events
clone.style.pointerEvents = 'none';
// Load event data to get duration
const event = await this.loadEventData();
if (event) {
const duration = (event.end.getTime() - event.start.getTime()) / (1000 * 60);
clone.dataset.originalDuration = duration.toString();
}
// Set height from original
clone.style.height = this.style.height || `${this.getBoundingClientRect().height}px`;
return clone;
}
```
### Phase 9: Initialize IndexedDB Reference
**File:** `src/index.ts`
```typescript
// After IndexedDB initialization
const indexedDB = new IndexedDBService();
await indexedDB.initialize();
// Set reference in SwpEventElement
SwpEventElement.setIndexedDB(indexedDB);
```
## Data Flow
```mermaid
sequenceDiagram
participant DOM as SwpEventElement
participant IDB as IndexedDBService
participant User
User->>DOM: Drag event
DOM->>IDB: getEvent(id)
IDB-->>DOM: ICalendarEvent
DOM->>DOM: Calculate new position
DOM->>IDB: saveEvent(updated)
IDB-->>DOM: Success
DOM->>DOM: renderFromEvent()
```
## Benefits
**Minimal DOM**: Only 1 attribute instead of 8+
**Single Source of Truth**: IndexedDB is authoritative
**No Duplication**: Data only in one place
**Scalability**: Large descriptions no problem
**Simpler Sync**: No DOM/IndexedDB mismatch
## Potential Challenges
⚠️ **Async Complexity**: All data operations become async
⚠️ **Performance**: More IndexedDB lookups
⚠️ **Drag Smoothness**: Async lookup during drag
## Solutions to Challenges
1. **Async Complexity**: Use `async/await` consistently throughout
2. **Performance**: IndexedDB is fast enough for our use case
3. **Drag Smoothness**: Store `data-original-duration` during drag to avoid lookup
## Files to Modify
1. ✏️ `src/elements/SwpEventElement.ts` - Main refactoring
2. ✏️ `src/managers/DragDropManager.ts` - Update to use async lookups
3. ✏️ `src/index.ts` - Initialize IndexedDB reference
4. ✏️ `src/renderers/EventRenderer.ts` - May need async updates
5. ✏️ `src/managers/AllDayManager.ts` - May need async updates
## Testing Strategy
1. Test event rendering with only ID in DOM
2. Test drag & drop with async data loading
3. Test resize with async data loading
4. Test performance with many events
5. Test offline functionality
6. Test sync after reconnection
## Next Steps
1. Review this plan
2. Discuss any concerns or modifications
3. Switch to Code mode for implementation
4. Implement phase by phase
5. Test thoroughly after each phase
---
**Note:** This is a significant architectural change. We should implement it carefully and test thoroughly at each phase.