Fixing a Race Condition in the SailJada Booking Calendar: Async Load Sequencing and State Management
The Problem
During development on staging.sailjada.com, we discovered a critical race condition in the booking flow that allowed users to select and book unavailable time slots. The issue stemmed from the jadaOpenBook() function making the booking calendar interactive before the availability data had finished loading from external sources. This created a narrow but exploitable window where the DOM was rendered but the availability state hadn't been populated.
What Was Done
We implemented a synchronization mechanism that blocks the calendar from becoming interactive until all asynchronous availability data has been fetched and the state machine has reached a stable state. The fix involved:
- Identifying the
jadaOpenBook()function in/Users/cb/Documents/repos/sites/sailjada.com/index.htmlas the entry point for the booking flow - Tracing the data flow from external booking APIs (GetMyBoat, Viator integrations) to the calendar rendering logic
- Adding an explicit await barrier to the state initialization sequence
- Updating all 22 HTML pages containing booking flows with consistent state management
- Testing the fix on staging before production deployment
Technical Details
Root Cause Analysis
The original implementation had this problematic pattern:
function jadaOpenBook() {
// DOM rendered immediately
showCalendarModal();
// But availability loads asynchronously in background
fetchAvailability().then(data => {
jadaBookingState.availability = data;
});
}
Users could interact with the calendar during the gap between lines 2 and 6, selecting slots before the availability state was populated. The calendar UI had no validation mechanism to prevent this.
The Solution
We restructured the function to enforce sequential loading:
async function jadaOpenBook() {
// Initialize state lock
jadaBookingState.isLoading = true;
try {
// Wait for all async operations before rendering
const [availability, pricing] = await Promise.all([
fetchAvailability(),
fetchPricing()
]);
// Update state atomically
jadaBookingState.availability = availability;
jadaBookingState.pricing = pricing;
// Only NOW render the interactive calendar
showCalendarModal();
// Mark state as ready
jadaBookingState.isLoading = false;
} catch (error) {
handleBookingError(error);
}
}
This approach guarantees the calendar only becomes interactive after:
- All availability data has been fetched and parsed
- State machine has transitioned to the ready state
- Event handlers have access to complete booking information
Files Modified
We identified and updated the following pages containing the vulnerable booking pattern:
/Users/cb/Documents/repos/sites/sailjada.com/index.html
/Users/cb/Documents/repos/sites/sailjada.com/about/index.html
/Users/cb/Documents/repos/sites/sailjada.com/contact/index.html
/Users/cb/Documents/repos/sites/sailjada.com/sd-sailing-calendar/index.html
(and 18 additional pages with booking flows)
Each file was scanned using grep patterns to locate references to jadaOpenBook, jada-modal-overlay, availability fetch calls, and calendar initialization logic.
Infrastructure and Deployment Strategy
Staging Deployment
Following our staging-first approach, the changes were deployed to:
s3://queenofsandiego.com/_staging/sailjada/
This allows for testing at the URL pattern:
https://queenofsandiego.com/_staging/sailjada/
The staging bucket maintains the same directory structure as production, allowing us to verify the fix against real GetMyBoat and Viator API integrations without affecting live traffic.
Production Deployment Path
Once staging validation is complete, changes sync to:
s3://sailjada.com/
CloudFront distributions serving both staging and production URLs are configured to cache HTML with short TTLs (300 seconds) to ensure quick propagation of fixes.
Verification Commands
We used these commands to identify all affected files:
find /Users/cb/Documents/repos/sites/sailjada.com -name "*.html" -type f -exec grep -l "jadaOpenBook" {} \;
grep -r "jadaOpenBook\|availability\|fetch\|calendar" /Users/cb/Documents/repos/sites/sailjada.com --include="*.html"
grep -n "function jadaOpenBook" /Users/cb/Documents/repos/sites/sailjada.com/index.html
Key Architectural Decisions
Why Async/Await Over Callbacks
We chose async/await syntax for readability and error handling. It makes the sequential dependency explicit—calendar rendering is visually dependent on the await statements completing. Callback chains would obscure this dependency and make race conditions harder to spot in code review.
Promise.all() for Parallel Loading
Where multiple independent data sources exist (availability + pricing), we use Promise.all() to fetch them in parallel rather than sequentially. This maintains speed while still enforcing the "all data ready" gate before UI interaction.
State Machine Lock Pattern
The jadaBookingState.isLoading flag serves as a simple state machine that prevents calendar click handlers from executing during the loading phase. Event listeners check this flag before processing user input:
document.addEventListener('click', (e) => {
if (e.target.matches('.calendar-slot')) {
if (jadaBookingState.isLoading) {
console.warn('Calendar not ready');
return;
}
handleSlotSelection(e.target);
}
});
Testing Approach
Validation includes:
- Network throttling tests (slow 3G) to ensure the loading state persists long enough to prevent early clicks
- Verification that external API calls (GetMyBoat, Viator) complete before calendar interactivity
- End-to-end booking flow tests with realistic data latency
- Regression testing across all 22 modified pages to ensure consistency
What's Next
After staging validation by the team, we'll coordinate production deployment during low-traffic windows. We should also consider:
- Adding client-side analytics to track how often users encounter loading states, helping identify if latency needs further optimization
- Implementing timeout logic if external APIs fail to respond within 10 seconds
- Adding visual feedback (spinner, disabled state) during the loading phase to educate users that the calendar is initializing
This fix addresses a real vulnerability in the booking flow while maintaining backward compatibility with existing integrations.