Major refactorering to get a hold on all these events

This commit is contained in:
Janus Knudsen 2025-08-09 00:31:44 +02:00
parent 2a766cf685
commit 59b3c64c55
18 changed files with 1901 additions and 357 deletions

View file

@ -0,0 +1,237 @@
# Calendar Plantempus - Date Mode Initialization Sequence
## Overview
This document shows the complete initialization sequence and event flow for Date Mode in Calendar Plantempus, including when data is loaded and ready for rendering.
## Sequence Diagram
```mermaid
sequenceDiagram
participant Browser as Browser
participant Index as index.ts
participant Config as CalendarConfig
participant Factory as CalendarTypeFactory
participant CM as CalendarManager
participant EM as EventManager
participant GM as GridManager
participant NM as NavigationManager
participant VM as ViewManager
participant ER as EventRenderer
participant SM as ScrollManager
participant EB as EventBus
participant DOM as DOM
Note over Browser: Page loads calendar application
Browser->>Index: Load application
Note over Index: PHASE 0: Pre-initialization Setup
Index->>Config: new CalendarConfig()
Config->>Config: loadCalendarType() - Read URL ?type=date
Config->>Config: loadFromDOM() - Read data attributes
Config->>Config: Set mode='date', period='week'
Index->>Factory: CalendarTypeFactory.initialize()
Factory->>Factory: Create DateHeaderRenderer
Factory->>Factory: Create DateColumnRenderer
Factory->>Factory: Create DateEventRenderer
Note over Factory: Strategy Pattern renderers ready
Note over Index: PHASE 1: Core Managers Construction
Index->>CM: new CalendarManager(eventBus, config)
CM->>EB: Subscribe to VIEW_CHANGE_REQUESTED
CM->>EB: Subscribe to NAV_PREV, NAV_NEXT
Index->>NM: new NavigationManager(eventBus)
NM->>EB: Subscribe to CALENDAR_INITIALIZED
Note over NM: Will wait to call updateWeekInfo()
Index->>VM: new ViewManager(eventBus)
VM->>EB: Subscribe to CALENDAR_INITIALIZED
Note over Index: PHASE 2: Data & Rendering Managers
Index->>EM: new EventManager(eventBus)
EM->>EB: Subscribe to CALENDAR_INITIALIZED
Note over EM: Will wait to load data
Index->>ER: new EventRenderer(eventBus)
ER->>EB: Subscribe to EVENTS_LOADED
ER->>EB: Subscribe to GRID_RENDERED
Note over ER: Needs BOTH events before rendering
Note over Index: PHASE 3: Layout Managers (Order Critical!)
Index->>SM: new ScrollManager()
SM->>EB: Subscribe to GRID_RENDERED
Note over SM: Must subscribe BEFORE GridManager renders
Index->>GM: new GridManager()
GM->>EB: Subscribe to CALENDAR_INITIALIZED
GM->>EB: Subscribe to CALENDAR_DATA_LOADED
GM->>GM: Set currentWeek = getWeekStart(new Date())
Note over GM: Ready to render, but waiting
Note over Index: PHASE 4: Coordinated Initialization
Index->>CM: initialize()
CM->>EB: emit(CALENDAR_INITIALIZING)
CM->>CM: setView('week'), setCurrentDate()
CM->>EB: emit(CALENDAR_INITIALIZED) ⭐
Note over EB: 🚀 CALENDAR_INITIALIZED triggers all managers
par EventManager Data Loading
EB->>EM: CALENDAR_INITIALIZED
EM->>EM: loadMockData() for date mode
EM->>EM: fetch('/src/data/mock-events.json')
Note over EM: Loading date-specific mock data
EM->>EM: Process events for current week
EM->>EB: emit(CALENDAR_DATA_LOADED, {calendarType: 'date', data})
EM->>EB: emit(EVENTS_LOADED, {events: [...])
and GridManager Initial Rendering
EB->>GM: CALENDAR_INITIALIZED
GM->>GM: render()
GM->>GM: updateGridStyles() - Set --grid-columns: 7
GM->>GM: createHeaderSpacer()
GM->>GM: createTimeAxis(dayStartHour, dayEndHour)
GM->>GM: createGridContainer()
Note over GM: Strategy Pattern - Date Mode Rendering
GM->>Factory: getHeaderRenderer('date') → DateHeaderRenderer
GM->>GM: renderCalendarHeader() - Create day headers
GM->>DOM: Create 7 swp-day-column elements
GM->>Factory: getColumnRenderer('date') → DateColumnRenderer
GM->>GM: renderColumnContainer() - Date columns
GM->>EB: emit(GRID_RENDERED) ⭐
and NavigationManager UI
EB->>NM: CALENDAR_INITIALIZED
NM->>NM: updateWeekInfo()
NM->>DOM: Update week display in navigation
NM->>EB: emit(WEEK_INFO_UPDATED)
and ViewManager Setup
EB->>VM: CALENDAR_INITIALIZED
VM->>VM: initializeView()
VM->>EB: emit(VIEW_RENDERED)
end
Note over GM: GridManager receives its own data event
EB->>GM: CALENDAR_DATA_LOADED
GM->>GM: updateGridStyles() - Recalculate columns if needed
Note over GM: Grid already rendered, just update styles
Note over ER: 🎯 Critical Synchronization Point
EB->>ER: EVENTS_LOADED
ER->>ER: pendingEvents = events (store, don't render yet)
Note over ER: Waiting for grid to be ready...
EB->>ER: GRID_RENDERED
ER->>DOM: querySelectorAll('swp-day-column') - Check if ready
DOM-->>ER: Return 7 day columns (ready!)
Note over ER: Both events loaded AND grid ready → Render!
ER->>Factory: getEventRenderer('date') → DateEventRenderer
ER->>ER: renderEvents(pendingEvents) using DateEventRenderer
ER->>DOM: Position events in day columns
ER->>ER: Clear pendingEvents
ER->>EB: emit(EVENT_RENDERED)
Note over SM: ScrollManager sets up after grid is complete
EB->>SM: GRID_RENDERED
SM->>DOM: querySelector('swp-scrollable-content')
SM->>SM: setupScrolling()
SM->>SM: applyScrollbarStyling()
SM->>SM: setupScrollSynchronization()
Note over Index: 🎊 Date Mode Initialization Complete!
Note over Index: Ready for user interaction
```
## Key Initialization Phases
### Phase 0: Pre-initialization Setup
- **CalendarConfig**: Loads URL parameters (`?type=date`) and DOM attributes
- **CalendarTypeFactory**: Creates strategy pattern renderers for date mode
### Phase 1: Core Managers Construction
- **CalendarManager**: Central coordinator
- **NavigationManager**: Week navigation controls
- **ViewManager**: View state management
### Phase 2: Data & Rendering Managers
- **EventManager**: Handles data loading
- **EventRenderer**: Manages event display with synchronization
### Phase 3: Layout Managers (Order Critical!)
- **ScrollManager**: Must subscribe before GridManager renders
- **GridManager**: Main grid rendering
### Phase 4: Coordinated Initialization
- **CalendarManager.initialize()**: Triggers `CALENDAR_INITIALIZED` event
- All managers respond simultaneously but safely
## Critical Synchronization Points
### 1. Event-Grid Synchronization
```typescript
// EventRenderer waits for BOTH events
if (this.pendingEvents.length > 0) {
const columns = document.querySelectorAll('swp-day-column'); // DATE MODE
if (columns.length > 0) { // Grid must exist first
this.renderEvents(this.pendingEvents);
}
}
```
### 2. Scroll-Grid Dependency
```typescript
// ScrollManager only sets up after grid is rendered
eventBus.on(EventTypes.GRID_RENDERED, () => {
this.setupScrolling(); // Safe to access DOM now
});
```
### 3. Manager Construction Order
```typescript
// Critical order: ScrollManager subscribes BEFORE GridManager renders
const scrollManager = new ScrollManager();
const gridManager = new GridManager();
```
## Date Mode Specifics
### Data Loading
- Uses `/src/data/mock-events.json`
- Processes events for current week
- Emits `CALENDAR_DATA_LOADED` with `calendarType: 'date'`
### Grid Rendering
- Creates 7 `swp-day-column` elements (weekDays: 7)
- Uses `DateHeaderRenderer` strategy
- Uses `DateColumnRenderer` strategy
- Sets `--grid-columns: 7` CSS variable
### Event Rendering
- Uses `DateEventRenderer` strategy
- Positions events in day columns based on start/end time
- Calculates pixel positions using `PositionUtils`
## Race Condition Prevention
1. **Subscription Before Action**: All managers subscribe during construction, act on `CALENDAR_INITIALIZED`
2. **DOM Existence Checks**: Managers verify DOM elements exist before manipulation
3. **Event Ordering**: `GRID_RENDERED` always fires before event rendering attempts
4. **Pending States**: EventRenderer stores pending events until grid is ready
5. **Coordinated Start**: Single `CALENDAR_INITIALIZED` event starts all processes
## Debugging Points
Key events to monitor during initialization:
- `CALENDAR_INITIALIZED` - Start of coordinated setup
- `CALENDAR_DATA_LOADED` - Date data ready
- `GRID_RENDERED` - Grid structure complete
- `EVENTS_LOADED` - Event data ready
- `EVENT_RENDERED` - Events positioned in grid
This sequence ensures deterministic, race-condition-free initialization with comprehensive logging for debugging.

View file

@ -0,0 +1,270 @@
# Improved Calendar Initialization Strategy
## Current Problems
1. **Race Conditions**: Managers try DOM operations before DOM is ready
2. **Sequential Blocking**: All initialization happens sequentially
3. **Poor Error Handling**: No timeouts or retry mechanisms
4. **Late Data Loading**: Data only loads after all managers are created
## Recommended New Architecture
### Phase 1: Early Parallel Startup
```typescript
// index.ts - Improved initialization
export class CalendarInitializer {
async initialize(): Promise<void> {
console.log('📋 Starting Calendar initialization...');
// PHASE 1: Early parallel setup
const setupPromises = [
this.initializeConfig(), // Load URL params, DOM attrs
this.initializeFactory(), // Setup strategy patterns
this.preloadCalendarData(), // Start data loading early
this.waitForDOMReady() // Ensure basic DOM exists
];
await Promise.all(setupPromises);
console.log('✅ Phase 1 complete: Config, Factory, Data preloading started');
// PHASE 2: Manager creation with dependencies
await this.createManagersWithDependencies();
// PHASE 3: Coordinated activation
await this.activateAllManagers();
console.log('🎊 Calendar fully initialized!');
}
}
```
### Phase 2: Dependency-Aware Manager Creation
```typescript
private async createManagersWithDependencies(): Promise<void> {
const managers = new Map<string, any>();
// Core managers (no DOM dependencies)
managers.set('config', calendarConfig);
managers.set('eventBus', eventBus);
managers.set('calendarManager', new CalendarManager(eventBus, calendarConfig));
// DOM-dependent managers (wait for DOM readiness)
await this.waitForRequiredDOM(['swp-calendar', 'swp-calendar-nav']);
managers.set('navigationManager', new NavigationManager(eventBus));
managers.set('viewManager', new ViewManager(eventBus));
// Data managers (can work with preloaded data)
managers.set('eventManager', new EventManager(eventBus));
managers.set('dataManager', new DataManager());
// Layout managers (need DOM structure + other managers)
await this.waitForRequiredDOM(['swp-calendar-container']);
// CRITICAL ORDER: ScrollManager subscribes before GridManager renders
managers.set('scrollManager', new ScrollManager());
managers.set('gridManager', new GridManager());
// Rendering managers (need grid structure)
managers.set('eventRenderer', new EventRenderer(eventBus));
this.managers = managers;
}
```
### Phase 3: Coordinated Activation
```typescript
private async activateAllManagers(): Promise<void> {
// All managers created and subscribed, now activate in coordinated fashion
const calendarManager = this.managers.get('calendarManager');
// This triggers CALENDAR_INITIALIZED, but now all managers are ready
await calendarManager.initialize();
// Wait for critical initialization events
await Promise.all([
this.waitForEvent('CALENDAR_DATA_LOADED', 10000),
this.waitForEvent('GRID_RENDERED', 5000),
this.waitForEvent('EVENTS_LOADED', 10000)
]);
// Ensure event rendering completes
await this.waitForEvent('EVENT_RENDERED', 3000);
}
```
## Specific Timing Improvements
### 1. Early Data Preloading
```typescript
private async preloadCalendarData(): Promise<void> {
const currentDate = new Date();
const mode = calendarConfig.getCalendarMode();
// Start loading data for current period immediately
const dataManager = new DataManager();
const currentPeriod = this.getCurrentPeriod(currentDate, mode);
// Don't await - let this run in background
const dataPromise = dataManager.fetchEventsForPeriod(currentPeriod);
// Also preload adjacent periods
const prevPeriod = this.getPreviousPeriod(currentDate, mode);
const nextPeriod = this.getNextPeriod(currentDate, mode);
// Store promises for later use
this.preloadPromises = {
current: dataPromise,
previous: dataManager.fetchEventsForPeriod(prevPeriod),
next: dataManager.fetchEventsForPeriod(nextPeriod)
};
console.log('📊 Data preloading started for current, previous, and next periods');
}
```
### 2. DOM Readiness Verification
```typescript
private async waitForRequiredDOM(selectors: string[]): Promise<void> {
const maxWait = 5000; // 5 seconds max
const checkInterval = 100; // Check every 100ms
const startTime = Date.now();
while (Date.now() - startTime < maxWait) {
const missing = selectors.filter(selector => !document.querySelector(selector));
if (missing.length === 0) {
console.log(`✅ Required DOM elements found: ${selectors.join(', ')}`);
return;
}
await new Promise(resolve => setTimeout(resolve, checkInterval));
}
throw new Error(`❌ Timeout waiting for DOM elements: ${selectors.join(', ')}`);
}
```
### 3. Manager Base Class with Proper Lifecycle
```typescript
export abstract class BaseManager {
protected isInitialized = false;
protected requiredDOMSelectors: string[] = [];
constructor() {
// Don't call init() immediately in constructor!
console.log(`${this.constructor.name}: Created but not initialized`);
}
async initialize(): Promise<void> {
if (this.isInitialized) {
console.log(`${this.constructor.name}: Already initialized, skipping`);
return;
}
// Wait for required DOM elements
if (this.requiredDOMSelectors.length > 0) {
await this.waitForDOM(this.requiredDOMSelectors);
}
// Perform manager-specific initialization
await this.performInitialization();
this.isInitialized = true;
console.log(`${this.constructor.name}: Initialization complete`);
}
protected abstract performInitialization(): Promise<void>;
private async waitForDOM(selectors: string[]): Promise<void> {
// Same DOM waiting logic as above
}
}
```
### 4. Enhanced GridManager
```typescript
export class GridManager extends BaseManager {
protected requiredDOMSelectors = ['swp-calendar-container'];
constructor() {
super(); // Don't call this.init()!
this.currentWeek = this.getWeekStart(new Date());
}
protected async performInitialization(): Promise<void> {
// Now safe to find elements - DOM guaranteed to exist
this.findElements();
this.subscribeToEvents();
// Wait for CALENDAR_INITIALIZED before rendering
await this.waitForEvent('CALENDAR_INITIALIZED');
console.log('GridManager: Starting initial render');
this.render();
}
}
```
### 5. Enhanced EventRenderer with Better Synchronization
```typescript
export class EventRenderer extends BaseManager {
private dataReady = false;
private gridReady = false;
private pendingEvents: CalendarEvent[] = [];
protected async performInitialization(): Promise<void> {
this.subscribeToEvents();
// Wait for both data and grid in parallel
const [eventsData] = await Promise.all([
this.waitForEvent('EVENTS_LOADED'),
this.waitForEvent('GRID_RENDERED')
]);
console.log('EventRenderer: Both events and grid ready, rendering now');
this.renderEvents(eventsData.events);
}
private subscribeToEvents(): void {
this.eventBus.on(EventTypes.EVENTS_LOADED, (e: Event) => {
const detail = (e as CustomEvent).detail;
this.pendingEvents = detail.events;
this.dataReady = true;
this.tryRender();
});
this.eventBus.on(EventTypes.GRID_RENDERED, () => {
this.gridReady = true;
this.tryRender();
});
}
private tryRender(): void {
if (this.dataReady && this.gridReady && this.pendingEvents.length > 0) {
this.renderEvents(this.pendingEvents);
this.pendingEvents = [];
}
}
}
```
## Benefits of New Architecture
1. **🚀 Parallel Operations**: Data loading starts immediately while managers are being created
2. **🛡️ Race Condition Prevention**: DOM readiness verified before operations
3. **⚡ Better Performance**: Critical path optimized, non-critical operations parallelized
4. **🔧 Better Error Handling**: Timeouts and retry mechanisms
5. **📊 Predictable Timing**: Clear phases with guaranteed completion order
6. **🐛 Easier Debugging**: Clear lifecycle events and logging
## Implementation Strategy
1. **Phase 1**: Create BaseManager class and update existing managers
2. **Phase 2**: Implement CalendarInitializer with parallel setup
3. **Phase 3**: Add DOM readiness verification throughout
4. **Phase 4**: Implement data preloading strategy
5. **Phase 5**: Add comprehensive error handling and timeouts
This architecture ensures reliable, fast, and maintainable calendar initialization.