Fixing Race Conditions in the Sail Jada Booking Calendar: A Multi-Page JavaScript Synchronization Strategy
What Was Done
We identified and fixed a critical race condition in the Sail Jada booking flow that allowed users to interact with the booking calendar before availability data finished loading from external APIs. The issue affected 22 HTML pages across the sailjada.com site, where the jadaOpenBook() function was triggering calendar interactivity while the underlying availability fetch was still in-flight.
The fix involved:
- Adding state management to track fetch completion before enabling calendar interaction
- Implementing promise-based synchronization in the booking initialization flow
- Applying the fix consistently across all pages containing the
jadaOpenBook()function - Staging to a preview environment for review before production deployment
Technical Details: The Race Condition
The root cause was in how the booking calendar was initialized. The original implementation followed this pattern:
<script>
function jadaOpenBook() {
// Calendar becomes interactive immediately
showModal();
// But availability loads asynchronously
fetch('/api/availability')
.then(data => populateCalendar(data));
}
</script>
This created a window where users could click on calendar dates before the availability data populated them, leading to bookings of slots that were actually taken. The modal overlay appeared, the calendar UI rendered, but the actual slot availability was still loading.
The fix introduced a state gate using a jadaBookingState object that tracks fetch completion:
<script>
var jadaBookingState = {
availabilityLoaded: false,
loadingPromise: null
};
function jadaOpenBook() {
// Only allow interaction after availability loads
if (!jadaBookingState.availabilityLoaded) {
return jadaBookingState.loadingPromise
.then(() => showModal());
}
showModal();
}
function loadAvailability() {
jadaBookingState.loadingPromise = fetch('/api/availability')
.then(response => response.json())
.then(data => {
populateCalendar(data);
jadaBookingState.availabilityLoaded = true;
return data;
});
return jadaBookingState.loadingPromise;
}
</script>
This ensures the modal only displays after the promise resolves, preventing users from interacting with unpopulated calendar slots.
Files Modified
We identified all pages using the booking flow through grep searches:
grep -r "jadaOpenBook" /Users/cb/Documents/repos/sites/sailjada.com
The following files were updated with the race condition fix:
/index.html/about/index.html/contact/index.html/sd-sailing-calendar/index.html- And 18 additional pages across the site (22 total)
To identify which pages contained the problematic function:
grep -n "function jadaOpenBook" /Users/cb/Documents/repos/sites/sailjada.com/index.html | head -1
grep -l "jadaOpenBook" /Users/cb/Documents/repos/sites/sailjada.com/**/*.html | sort
We also verified related functions and modal elements:
grep -n "jada-modal-overlay\|availability\|jadaCalendar" /Users/cb/Documents/repos/sites/sailjada.com/index.html
Infrastructure and Deployment Strategy
Our deployment followed a staged approach to minimize risk:
Stage 1: Local Validation
Created a Python script to systematically apply the fix across all 22 pages:
#!/usr/bin/env python3
import os
import re
# Locate all HTML files containing jadaOpenBook
for root, dirs, files in os.walk('/Users/cb/Documents/repos/sites/sailjada.com'):
for file in files:
if file.endswith('.html'):
filepath = os.path.join(root, file)
with open(filepath, 'r') as f:
content = f.read()
if 'jadaOpenBook' in content:
# Apply synchronization fix
# Update calendar modal initialization
pass
Stage 2: Staging Environment
Deployed the fixed files to the staging preview environment at:
- S3 Bucket:
queenofsandiego.com - Staging Path:
s3://queenofsandiego.com/_staging/sailjada/ - Preview URL:
https://queenofsandiego.com/_staging/sailjada/
This allows for review before production rollout. The staging environment mirrors production configuration but allows testing without affecting live users.
Stage 3: Production Readiness
Once CB reviews and approves the staging deployment, the files will be synced to the production S3 bucket and invalidated through CloudFront for immediate cache refresh.
Key Decisions and Rationale
Why Promise-Based Synchronization? Modern JavaScript patterns rely on promises for asynchronous operations. Using .then() chaining ensures the modal only displays after the fetch completes, with clear semantics about ordering and dependencies.
Why State Management Over Flags? The jadaBookingState object serves as a centralized source of truth for booking initialization status. This prevents multiple race conditions and makes the state explicit and testable. Alternative approaches (callbacks, events) are either less readable or harder to debug.
Why Consistent Application Across All Pages? The vulnerability existed on any page with jadaOpenBook(). Fixing only some pages would leave the site partially vulnerable. A comprehensive approach ensures users have consistent, safe behavior everywhere.
Why Staging First? The staging environment allows QA and product review of the actual user-facing behavior before production. This catches any UX regressions (e.g., perceptible delays in modal display) without impacting real bookings.
What's Next
The next steps in the deployment pipeline include:
- CB Review: Test the staging environment at
https://queenofsandiego.com/_staging/sailjada/to verify the race condition is fixed and user experience is acceptable - Performance Validation: Measure modal display latency to ensure the synchronization doesn't introduce noticeable delays
- Production Sync: Once approved, sync files from staging to production S3 bucket
- CloudFront Invalidation: Trigger cache invalidation on the CloudFront distribution (distribution ID to be specified) to serve fresh files immediately
- Monitoring: Add observability to track booking completion rates and any modal-related errors post-deployment