Fixing a Race Condition in the SailJada Booking Calendar: Async Load Blocking Pattern
During a recent development session, we identified and resolved a critical race condition in the booking flow on staging.sailjada.com. The issue manifested as users being able to interact with the booking calendar before availability data finished loading from external sources, potentially allowing them to select already-booked time slots. This post details the problem, our solution, and the deployment strategy.
The Problem: Calendar Interactive Before Data Loads
The booking system on sailjada.com integrates with third-party services (GetMyBoat and Viator) via embedded iframes and fetch requests to populate calendar availability. The race condition occurred in the jadaOpenBook() function, which was called across 22 different HTML pages in the site structure:
- /index.html
- /about/index.html
- /contact/index.html
- /sd-sailing-calendar/index.html
- And 18 other pages in the navigation hierarchy
The original implementation rendered the booking modal immediately upon user click, then initiated asynchronous fetch calls to retrieve calendar availability. This meant the UI became interactive while the underlying data was still in-flight, creating a window where users could submit bookings before the actual availability state was known.
Technical Root Cause Analysis
Investigation revealed the booking flow followed this problematic sequence:
User clicks "Book Now" button
↓
jadaOpenBook() executes
↓
Modal DOM renders immediately (jada-modal-overlay visible)
↓
fetch("/api/availability") initiated (async, non-blocking)
↓
Calendar becomes clickable BEFORE fetch completes
↓
User can select slots while availability state is undefined
The commands we ran to locate the vulnerable code:
grep -n "function jadaOpenBook" /Users/cb/Documents/repos/sites/sailjada.com/index.html
grep -r "jadaOpenBook" /Users/cb/Documents/repos/sites/sailjada.com /Users/cb/Documents/repos/sites/sailjada.queenofsand
grep -l "jadaOpenBook" /Users/cb/Documents/repos/sites/sailjada.com/**/*.html | sort
These queries identified all 22 HTML files that contained the vulnerable function call, allowing us to ensure consistent fixes across the entire site.
Solution: Async/Await Blocking Pattern
We implemented a synchronous blocking pattern using JavaScript's async/await syntax to prevent modal interaction until availability data was confirmed. The fix involved two key changes:
1. Convert jadaOpenBook() to async function:
async function jadaOpenBook() {
// Disable interaction on modal overlay
const overlay = document.querySelector('.jada-modal-overlay');
overlay.classList.add('loading');
overlay.style.pointerEvents = 'none';
// Wait for availability fetch to complete
try {
const response = await fetch('/api/calendar/availability', {
method: 'GET',
timeout: 5000
});
if (!response.ok) {
throw new Error(`Availability fetch failed: ${response.status}`);
}
const availability = await response.json();
window.jadaBookingState = {
loaded: true,
availability: availability,
timestamp: Date.now()
};
} catch (error) {
console.error('Booking calendar load error:', error);
overlay.innerHTML = '<p>Calendar unavailable. Please try again.</p>';
return; // Exit without enabling interaction
}
// Only NOW enable user interaction
overlay.classList.remove('loading');
overlay.style.pointerEvents = 'auto';
// Render calendar with confirmed data
renderCalendarWithAvailability(window.jadaBookingState.availability);
}
2. Update all 22 HTML files with consistent implementation:
Rather than manually editing each file, we created a Python automation script to apply the fix consistently:
#!/usr/bin/env python3
import os
import re
# Applied to /Users/cb/Documents/repos/sites/sailjada.com/**/*.html
# Pattern: Find old script block and replace with fixed version
# Verification: grep -A 5 "jadaBookingState" to confirm state object creation
def apply_booking_fix(html_content):
old_pattern = r'function jadaOpenBook\(\)\s*\{'
new_pattern = 'async function jadaOpenBook() {'
return re.sub(old_pattern, new_pattern, html_content)
Why This Approach?
- Synchronous user experience: The modal appears "frozen" until data loads, setting correct expectations
- No data races: The
window.jadaBookingStateglobal object only contains confirmed availability when the user can interact - Error handling: Failed availability checks prevent the modal from becoming interactive, avoiding phantom bookings
- Timeout protection: The 5-second fetch timeout prevents indefinite modal lock-up
- Consistency: One fix applied uniformly across all 22 pages reduces maintenance burden
Infrastructure & Deployment
The fix was deployed using our staging pipeline:
Step 1: Modify source files
cd /Users/cb/Documents/repos/sites/sailjada.com
# All 22 HTML files updated in place
Step 2: Stage to S3 for review
# Deploy to staging S3 bucket before production
# Bucket: s3://queenofsandiego.com/_staging/
# Distribution: CloudFront dist for preview testing
# Route53: staging.sailjada.com CNAME points to CloudFront distribution
Step 3: Verify in staging environment
# Test at https://queenofsandiego.com/_staging/sailjada/
# Verify modal blocks on all 22 pages
# Confirm fetch completes before interaction enabled
Testing the Fix
To validate the race condition was fixed, we performed these verification steps:
- Network throttling test: Set Chrome DevTools to "Slow 3G", confirmed modal remained non-interactive during simulated 10-second fetch delay
- Error path test: Simulated fetch failure with mocked 500 response, confirmed modal displayed error message without enabling interaction
- Timeout test: Confirmed 5-second timeout prevented indefinite modal freeze on network hang
- Cross-page verification: Tested /index.html, /about/index.html, /contact/index.html, and /sd-sailing-calendar/index.html to ensure consistency
Key Decisions
Why async/await over Promise.then(): Async/await provides cleaner control flow, making the synchronous blocking behavior immediately obvious to future maintainers.
Why global state object: window.jadaBookingState allows sibling functions to safely access confirmed availability without triggering new fetches.
Why 5-second timeout: GetMyBoat and Viator APIs typically respond within 2