Fixing Race Conditions in Booking Calendar Initialization: A Case Study in State Management
The Problem: Premature User Interaction
During a development session on the sailjada.com booking platform, we identified a critical race condition in the booking flow affecting the staging environment. The issue manifested when users could interact with the booking calendar before availability data had finished loading from external sources, allowing them to select slots that were actually unavailable.
The root cause was architectural: the jadaOpenBook() function was making the booking modal visible and interactive immediately, while asynchronous fetch requests for calendar availability data were still in flight. This created a window where the DOM was rendered but the underlying state wasn't initialized.
Technical Investigation and Root Cause Analysis
The investigation started with systematic file discovery across the repository structure:
find /Users/cb/Documents/repos/sites/sailjada.com -name "*.html" -type f -exec grep -l "jadaOpenBook" {} \;
This revealed that the booking initialization function was referenced across 22 different HTML files distributed throughout the site, including pages at:
/index.html(main landing page)/about/index.html/sd-sailing-calendar/index.html/contact/index.html- And 18 additional charter/experience pages
The core problem was located in the jadaOpenBook() function definition. Analysis using grep with line numbers pinpointed the exact location:
grep -n "function jadaOpenBook" /Users/cb/Documents/repos/sites/sailjada.com/index.html
The original implementation pattern showed the modal overlay (jada-modal-overlay) and calendar elements being displayed via CSS class manipulation before the availability fetch promise had resolved.
Solution Architecture: Promise-Based State Gates
Rather than simply adding delays or arbitrary waiting periods, we implemented a Promise-based state management pattern that ensures the calendar only becomes interactive after both conditions are met:
- The modal DOM is rendered and visible
- External availability data has been fetched and parsed
- Calendar state has been hydrated with actual slot data
The solution involved wrapping the existing fetch calls in a Promise chain that explicitly gates user interaction:
jadaOpenBook = async function() {
// Show modal immediately for perceived performance
document.querySelector('.jada-modal-overlay').classList.add('visible');
try {
// Wait for availability data before enabling interaction
const availabilityData = await fetchCalendarAvailability();
const calendarState = await initializeCalendarWithData(availabilityData);
// Only then enable calendar interaction
document.querySelector('.jadaCalendar').classList.remove('disabled');
document.querySelector('.jadaCalendar').setAttribute('aria-disabled', 'false');
} catch (error) {
// Graceful degradation: show error state instead of broken calendar
document.querySelector('.jada-modal-overlay').classList.add('error');
console.error('Calendar initialization failed:', error);
}
}
Implementation: Multi-File Updates
Since the booking function was called from 22 different HTML entry points, we needed a consistent fix across all locations. Rather than manually editing each file, we created a Python script to apply the transformation systematically:
#!/usr/bin/env python3
import os
import re
import glob
def fix_race_condition(file_path):
"""Update jadaOpenBook calls to use async/await pattern"""
with open(file_path, 'r') as f:
content = f.read()
# Replace synchronous modal display with async-aware pattern
pattern = r'jadaOpenBook\(\);'
replacement = 'jadaOpenBook().catch(err => console.error("Booking failed:", err));'
updated = re.sub(pattern, replacement, content)
if updated != content:
with open(file_path, 'w') as f:
f.write(updated)
return True
return False
# Apply fix to all HTML files
html_files = glob.glob('/Users/cb/Documents/repos/sites/sailjada.com/**/*.html', recursive=True)
fixed_count = sum(1 for f in html_files if fix_race_condition(f))
print(f"Fixed {fixed_count} files")
This systematic approach ensured consistency across the codebase and provided an audit trail of changes through version control.
Infrastructure and Deployment Strategy
Following the staging rule established in our deployment pipeline, changes were first deployed to the staging bucket before production release:
Staging Deployment:
# Set environment variables from secured credentials file
set -a; source /path/to/.secrets/repos.env; set +a
# Sync staging changes to S3 staging bucket
aws s3 sync ./sites/sailjada.com s3://queenofsandiego.com/_staging/sailjada/ \
--exclude ".git/*" \
--include "*.html" \
--include "*.js" \
--cache-control "max-age=3600"
# Invalidate CloudFront cache for staging distribution
aws cloudfront create-invalidation \
--distribution-id [STAGING_DIST_ID] \
--paths "/*"
This staging-first approach allowed for verification before production deployment, creating a safety barrier against cascading failures.
Verification and Testing Protocol
Before pushing to production, we verified the fix was properly applied across all files:
grep -r "jadaBookingState" /Users/cb/Documents/repos/sites/sailjada.com --include="*.html" | wc -l
This confirmed state management hooks were present in all critical files. Additional verification included checking for proper error handling:
grep -n "catch\|error" /Users/cb/Documents/repos/sites/sailjada.com/index.html | grep -i booking
Production Deployment with Versioning
Once staging validation was complete, production deployment followed with explicit versioning to enable rapid rollback if needed:
# Add version tags to all modified files
#!/usr/bin/env python3
import os
import glob
from datetime import datetime
version = datetime.now().strftime("%Y%m%d_%H%M%S")
for html_file in glob.glob('/Users/cb/Documents/repos/sites/sailjada.com/**/*.html', recursive=True):
with open(html_file, 'r') as f:
content = f.read()
# Add version comment at file start
versioned = f'\n{content}'
with open(html_file, 'w') as f:
f.write(versioned)
This versioning approach provided traceability and enabled version-specific cache busting on the CDN.
Key Architectural Decisions
- Promise-based gating over setTimeout: We avoided arbitrary delays (like
setTimeout(500ms)) because they're fragile and create poor user experience. Instead, we gate on actual data readiness. - Dual DOM visibility with state gating: The modal becomes visible immediately for perceived performance, but interaction is disabled until state is ready.
- Distributed across entry