Fixing the Booking Calendar Race Condition on SailJada: A Multi-Page Synchronization Fix
What Was Done
We identified and resolved a critical race condition in the booking flow across the SailJada website where users could attempt to book sailing slots that were already unavailable. The issue occurred because the booking calendar became interactive before availability data finished loading from external sources. We implemented a synchronization mechanism in the jadaOpenBook() function that blocks calendar interaction until all required data is loaded, then deployed the fix across all 22 HTML pages in the site.
The Problem: Race Condition in Booking Flow
The SailJada website integrates with multiple external booking systems (GetMyBoat, Viator, and internal calendar APIs) to display real-time availability. The original booking flow had this sequence:
- User clicks "Book Now" button, triggering
jadaOpenBook() - Function immediately renders the booking modal with the interactive calendar UI
- Asynchronous fetch requests for availability data begin in the background
- User can interact with the calendar before availability data returns
- User selects a date that may already be booked
This created a window of vulnerability where user selections weren't validated against actual availability—a classic race condition where concurrent async operations created unpredictable state.
Technical Implementation
Root Cause Analysis
We located the vulnerable code in /Users/cb/Documents/repos/sites/sailjada.com/index.html and related pages. The jadaOpenBook() function was rendering the modal DOM element synchronously while firing asynchronous fetch requests. The modal's event listeners were immediately active, but the underlying data they depended on wasn't guaranteed to be loaded.
The Fix: Promise-Based Synchronization
We modified the jadaOpenBook() function to implement explicit promise-based waiting:
function jadaOpenBook() {
// Show loading state immediately
const modalOverlay = document.querySelector('.jada-modal-overlay');
modalOverlay.classList.add('jada-loading');
// Create promises for each data dependency
const availabilityPromise = fetch('/api/availability')
.then(r => r.json())
.then(data => {
window.jadaBookingState.availability = data;
return data;
});
const calendarPromise = fetch('/api/calendar-slots')
.then(r => r.json())
.then(data => {
window.jadaBookingState.calendar = data;
return data;
});
// Wait for ALL data before enabling interaction
Promise.all([availabilityPromise, calendarPromise])
.then(() => {
// Now render the calendar with validated data
renderBookingCalendar(window.jadaBookingState);
modalOverlay.classList.remove('jada-loading');
modalOverlay.classList.add('jada-ready');
})
.catch(error => {
modalOverlay.classList.add('jada-error');
console.error('Booking data load failed:', error);
showErrorMessage('Unable to load availability. Please try again.');
});
}
Key improvements:
- Explicit Promise.all(): Uses
Promise.all()to wait for all async operations simultaneously, with proper error handling - Loading State Management: DOM classes (
jada-loading,jada-ready,jada-error) indicate UI state, with CSS rules preventing interaction during loading - Centralized State: All booking state stored in
window.jadaBookingStateobject, eliminating implicit dependencies - Validation Before Render: Calendar only renders after all data is loaded and validated
CSS Guard Rails
We added CSS to prevent user interaction during loading:
.jada-modal-overlay.jada-loading {
pointer-events: none;
opacity: 0.6;
}
.jada-modal-overlay.jada-loading .jadaCalendar {
cursor: not-allowed;
}
.jada-modal-overlay.jada-ready {
pointer-events: auto;
opacity: 1;
}
Multi-Page Deployment Strategy
The fix needed to be applied across all pages in the site. We identified files containing booking integration:
/index.html(main homepage)/about/index.html/contact/index.html/sd-sailing-calendar/index.html- 18 additional pages with booking CTAs
Rather than manually editing each file, we created a Python script to apply the transformation consistently across all 22 HTML files:
#!/usr/bin/env python3
import os
import re
import glob
pattern = r''
replacement = '''
'''
for filepath in glob.glob('/Users/cb/Documents/repos/sites/sailjada.com/**/*.html', recursive=True):
with open(filepath, 'r') as f:
content = f.read()
if 'jadaOpenBook' in content:
updated = re.sub(pattern, replacement, content, flags=re.DOTALL)
with open(filepath, 'w') as f:
f.write(updated)
print(f'Updated: {filepath}')
This approach:
- Extracted booking logic into
/assets/booking-fix.js(single source of truth) - Injected initialization on all pages consistently
- Ensured version control tracked exact changes across all files
Staging Deployment
Following the staging rule, we deployed to the staging bucket before production:
aws s3 sync /Users/cb/Documents/repos/sites/sailjada.com \
s3://queenofsandiego.com/_staging/sailjada/ \
--delete \
--exclude ".git/*" \
--exclude "node_modules/*"
# CloudFront invalidation for staging
aws cloudfront create-invalidation \
--distribution-id E1ABC2DEF3GHIJ \
--paths "/_staging/sailjada/*"
Staging URL: https://queenofsandiego.com/_staging/sailjada/
Why This Approach?
- Promise-Based: Modern async/await would be cleaner, but Promise.all() maintains compatibility with existing codebase
- CSS State Classes: Leverages cascade to prevent user interaction—more reliable than JavaScript-only guards
- Centralized State Object: Easier to debug than scattered variables; facilitates future analytics and error tracking
- Script Extraction: Single source of truth in