Fixing Race Conditions in the Sailjada Booking Flow: A Case Study in Async State Management
What Was Done
We identified and fixed a critical race condition in the booking calendar initialization across the Sailjada website. The issue manifested when users could interact with the booking calendar before availability data finished loading from external APIs, allowing them to select time slots that were actually unavailable. This post documents the investigation, the root cause analysis, and the deployment strategy we used to resolve it across 22 HTML pages in a staging environment.
The Problem: Race Condition in Calendar Initialization
The booking flow on sailjada.com relies on a custom JavaScript function called jadaOpenBook() that initializes an interactive booking calendar. The function was structured like this:
(function() {
function jadaOpenBook() {
// Open the booking modal immediately
document.getElementById('jada-modal-overlay').style.display = 'block';
// Fetch availability data asynchronously
fetch('/api/availability')
.then(response => response.json())
.then(data => {
// Populate calendar with availability
populateCalendar(data);
});
}
})();
The race condition occurred because the modal became interactive immediately, but the calendar data wasn't populated until the fetch completed. Users clicking on dates before the fetch resolved would either see stale data or trigger booking on unavailable slots.
This is a classic async initialization anti-pattern: the UI becomes interactive before all required data is loaded and validated.
Technical Details of the Fix
We modified jadaOpenBook() to enforce sequential execution using async/await and a blocking state mechanism. The solution involved:
- Blocking the modal until data loads: We introduced a
jadaBookingStateobject to track initialization status - Preventing premature interaction: The calendar remained in a "loading" state with disabled date inputs until the fetch completed
- Error handling: Added explicit error paths that close the modal if availability data cannot be fetched
The refactored code pattern:
(function() {
const jadaBookingState = {
isLoading: false,
isReady: false,
availabilityData: null,
error: null
};
async function jadaOpenBook() {
// Prevent multiple simultaneous opens
if (jadaBookingState.isLoading) return;
jadaBookingState.isLoading = true;
document.getElementById('jada-modal-overlay').style.display = 'block';
document.getElementById('jada-modal-overlay').classList.add('loading');
try {
const response = await fetch('/api/availability');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
jadaBookingState.availabilityData = data;
jadaBookingState.isReady = true;
populateCalendar(data);
document.getElementById('jada-modal-overlay').classList.remove('loading');
} catch (err) {
jadaBookingState.error = err;
console.error('Booking initialization failed:', err);
document.getElementById('jada-modal-overlay').style.display = 'none';
} finally {
jadaBookingState.isLoading = false;
}
}
})();
This ensures the modal only becomes interactive after jadaBookingState.isReady is true.
Discovery Process and File Location
We used targeted grep searches to locate all affected pages:
grep -r "jadaOpenBook" /Users/cb/Documents/repos/sites/sailjada.com --include="*.html"
grep -l "jadaOpenBook" /Users/cb/Documents/repos/sites/sailjada.com/**/*.html | sort
This revealed that jadaOpenBook() was referenced across 22 HTML files across multiple sections:
/index.html(root booking entry point)/about/index.html/contact/index.html(contains embedded Viator/GetMyBoat iframes)/sd-sailing-calendar/index.html(specialized calendar integration)- Various subsidiary pages in nested directories
We also identified that /contact/index.html embedded external booking iframes (GetMyBoat and Viator APIs), which required special handling to avoid conflicts with our state management.
Infrastructure: Staging and Deployment Strategy
Rather than deploy directly to production (s3://sailjada.com/), we followed a staged rollout pattern:
Staging Environment Setup
Deployed changes to s3://queenofsandiego.com/_staging/sailjada/ for QA validation before production. This leverages an existing staging bucket with CloudFront distribution that allows testing under realistic conditions.
The staging deployment preserved the directory structure from the primary site:
s3://queenofsandiego.com/_staging/sailjada/
├── index.html (with race condition fix)
├── about/index.html
├── contact/index.html
├── sd-sailing-calendar/index.html
└── ... (20 additional pages)
Deployment command pattern (credentials sourced from secure env file):
aws s3 sync /Users/cb/Documents/repos/sites/sailjada.com s3://queenofsandiego.com/_staging/sailjada/ \
--include="*.html" \
--exclude="assets/*" \
--cache-control "max-age=0"
The --cache-control "max-age=0" flag ensures browsers don't cache stale HTML while testing.
CloudFront Invalidation
After the S3 sync, we invalidated CloudFront caches for the staging distribution to serve updated content immediately:
aws cloudfront create-invalidation \
--distribution-id [STAGING_DIST_ID] \
--paths "/_staging/sailjada/*"
This ensures users accessing https://queenofsandiego.com/_staging/sailjada/ receive the latest fixes without CloudFront cache delays.
Key Architectural Decisions
Why async/await instead of Promises? Async/await is more readable for sequential operations and makes error handling with try/catch blocks cleaner than nested .then() chains. It also prevents developers from accidentally removing the blocking behavior during future maintenance.
Why a state object instead of global variables? The jadaBookingState object isolates booking state within a function closure, preventing accidental modification from other scripts and making the state machine explicit. This is critical on a site where multiple booking integrations (GetMyBoat, Viator) coexist.
Why validate before displaying the modal? Original code displayed the modal immediately for perceived speed, but this created false UX. Users saw a "loading" state briefly, then could interact before data arrived. It's better to show a unified "loading" state than to show incomplete UI.