9.1 KiB
9.1 KiB
Improved Calendar Initialization Strategy
Current Problems
- Race Conditions: Managers try DOM operations before DOM is ready
- Sequential Blocking: All initialization happens sequentially
- Poor Error Handling: No timeouts or retry mechanisms
- Late Data Loading: Data only loads after all managers are created
Recommended New Architecture
Phase 1: Early Parallel Startup
// 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
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
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
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
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
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
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
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
- 🚀 Parallel Operations: Data loading starts immediately while managers are being created
- 🛡️ Race Condition Prevention: DOM readiness verified before operations
- ⚡ Better Performance: Critical path optimized, non-critical operations parallelized
- 🔧 Better Error Handling: Timeouts and retry mechanisms
- 📊 Predictable Timing: Clear phases with guaranteed completion order
- 🐛 Easier Debugging: Clear lifecycle events and logging
Implementation Strategy
- Phase 1: Create BaseManager class and update existing managers
- Phase 2: Implement CalendarInitializer with parallel setup
- Phase 3: Add DOM readiness verification throughout
- Phase 4: Implement data preloading strategy
- Phase 5: Add comprehensive error handling and timeouts
This architecture ensures reliable, fast, and maintainable calendar initialization.