Fixing a Race Condition in the SailJada Booking Calendar: Synchronizing State Before User Interaction
What Was Done
We identified and fixed a critical race condition in the SailJada booking flow where the booking calendar modal became interactive before availability data finished loading from external APIs. This allowed users to potentially book time slots that were already reserved, creating double-bookings and availability conflicts. The fix involved adding explicit state synchronization to the jadaOpenBook() function across 22 HTML pages, ensuring the fetch operation completes before enabling user interaction with the calendar UI.
The Problem: A Classic Race Condition
The issue manifested in the following sequence:
- User clicks "Book Now" button on any page containing the booking flow
jadaOpenBook()is invoked, which opens a modal overlay containing the calendar- The modal becomes visually interactive (clickable) while an asynchronous
fetch()call to load availability data is still in-flight - User can select dates and times before the availability data arrives from the backend
- When availability data finally loads, it reveals conflicts or unavailable slots the user already selected
This is a textbook example of the async/await anti-pattern: starting a long-running operation without blocking dependent code. The booking calendar UI was rendering synchronously while data loading was asynchronous, creating a window of vulnerability.
Technical Implementation
The fix was applied to the jadaOpenBook() function definition in /Users/cb/Documents/repos/sites/sailjada.com/index.html and all child pages containing booking functionality:
// BEFORE: Race condition - modal is interactive immediately
function jadaOpenBook() {
document.getElementById('jada-modal-overlay').style.display = 'block';
// This fetch happens in the background - modal is already clickable!
fetch('/api/availability')
.then(response => response.json())
.then(data => {
// Populate calendar weeks later
jadaCalendar.loadAvailability(data);
});
}
// AFTER: State is synchronized - modal only becomes interactive after fetch completes
async function jadaOpenBook() {
// Show loading state
document.getElementById('jada-modal-overlay').style.display = 'block';
document.getElementById('jada-loading-spinner').style.display = 'block';
document.getElementById('jada-calendar-container').style.pointerEvents = 'none';
try {
// Wait for availability data to load
const response = await fetch('/api/availability');
const data = await response.json();
// Populate calendar with data
jadaCalendar.loadAvailability(data);
// Only NOW enable user interaction
document.getElementById('jada-loading-spinner').style.display = 'none';
document.getElementById('jada-calendar-container').style.pointerEvents = 'auto';
} catch (error) {
console.error('Failed to load availability:', error);
document.getElementById('jada-error-message').textContent =
'Unable to load calendar. Please try again.';
document.getElementById('jada-loading-spinner').style.display = 'none';
}
}
The key changes:
- async/await syntax: Made the function
asyncto useawaitfor the fetch operation, blocking execution until data arrives - UI state management: Added a loading spinner and disabled pointer events on the calendar container until data loads
- Error handling: Wrapped the fetch in try/catch to gracefully handle network failures instead of leaving the modal in an inconsistent state
- Visual feedback: Users now see a spinner during the load, making it clear the system is working
Deployment to All Affected Pages
We discovered the jadaBookingState and jadaOpenBook references across the entire site using grep:
grep -l "jadaOpenBook" /Users/cb/Documents/repos/sites/sailjada.com/**/*.html | sort
This identified 22 HTML files requiring the fix. Rather than manually editing each file, we created an automated Python script to consistently apply the transformation:
# Identify all affected files
find /Users/cb/Documents/repos/sites/sailjada.com -name "*.html" -type f \
-exec grep -l "jadaOpenBook\|jadaBookingState" {} \;
Files updated included:
/index.html(main landing page)/about/index.html(company page)/contact/index.html(contact/booking page)/sd-sailing-calendar/index.html(calendar integration page)- Various charter and service description pages
Infrastructure and Deployment Strategy
To minimize risk while fixing production, we used a staged rollout approach:
Staging Environment
First, all changes were deployed to the staging environment at s3://queenofsandiego.com/_staging/sailjada/. This is a separate S3 bucket that mirrors production structure but allows testing before going live:
# Deploy to staging for review
aws s3 sync /Users/cb/Documents/repos/sites/sailjada.com \
s3://queenofsandiego.com/_staging/sailjada/ \
--exclude ".git/*" \
--exclude "node_modules/*" \
--delete
Changes were then reviewed at https://queenofsandiego.com/_staging/sailjada/ before proceeding to production.
Production Deployment
Once validated on staging, the same sync was applied to the production S3 bucket:
# Deploy to production (sailjada.com)
aws s3 sync /Users/cb/Documents/repos/sites/sailjada.com \
s3://sail-jada-production/ \
--exclude ".git/*" \
--exclude "node_modules/*" \
--delete
After S3 sync, the CloudFront distribution cache was invalidated to ensure edge nodes served fresh content:
# Invalidate CloudFront to force cache refresh
aws cloudfront create-invalidation \
--distribution-id EXAMPLE1234567 \
--paths "/*"
Versioning and Rollback
To ensure we could quickly rollback if issues arose, we tagged the git commit with the deployment version:
cd /Users/cb/Documents/repos/sites/sailjada.com
# Git workflow
git add -A
git commit -m "fix: race condition in jadaOpenBook - block modal interaction until availability loads"
git tag -a v1.2.3-booking-fix -m "Production deployment of booking calendar race condition fix"
git push origin main --tags
This creates an auditable trail and allows reverting to the previous version if needed:
git checkout v1.2.2 # Rollback if necessary
Why This Approach
Synchronous blocking: Rather than using promises or callbacks, async/await provides cleaner, more readable code that explicitly shows the