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
9.7 KiB
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:
<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
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
<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
export class SwpEventElement extends BaseSwpEventElement {
private static indexedDB: IndexedDBService;
static setIndexedDB(db: IndexedDBService): void {
SwpEventElement.indexedDB = db;
}
}
1.3 Implement Async Data Loading
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
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)
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)
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)
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)
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)
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:
// 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)
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
// After IndexedDB initialization
const indexedDB = new IndexedDBService();
await indexedDB.initialize();
// Set reference in SwpEventElement
SwpEventElement.setIndexedDB(indexedDB);
Data Flow
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
- Async Complexity: Use
async/awaitconsistently throughout - Performance: IndexedDB is fast enough for our use case
- Drag Smoothness: Store
data-original-durationduring drag to avoid lookup
Files to Modify
- ✏️
src/elements/SwpEventElement.ts- Main refactoring - ✏️
src/managers/DragDropManager.ts- Update to use async lookups - ✏️
src/index.ts- Initialize IndexedDB reference - ✏️
src/renderers/EventRenderer.ts- May need async updates - ✏️
src/managers/AllDayManager.ts- May need async updates
Testing Strategy
- Test event rendering with only ID in DOM
- Test drag & drop with async data loading
- Test resize with async data loading
- Test performance with many events
- Test offline functionality
- Test sync after reconnection
Next Steps
- Review this plan
- Discuss any concerns or modifications
- Switch to Code mode for implementation
- Implement phase by phase
- Test thoroughly after each phase
Note: This is a significant architectural change. We should implement it carefully and test thoroughly at each phase.