Fixing Race Conditions in the SailJada Booking Calendar: A Multi-Page State Management Challenge
During a recent development session on staging.sailjada.com, we identified and resolved a critical race condition in the booking flow that allowed users to select unavailable sailing slots. This post details the root cause, the fix implementation, and the infrastructure decisions that enabled safe deployment across 22 HTML pages.
The Problem: Race Condition in jadaOpenBook()
The booking flow on sailjada.com uses an external calendar widget (loaded via iframe from GetMyBoat and Viator APIs) that becomes interactive before availability data finishes loading. The sequence of events was:
- User clicks "Book Now" button, triggering
jadaOpenBook() - Modal overlay appears and iframe begins loading
- Calendar widget renders immediately, becoming clickable
- Availability fetch request is still in-flight (network latency)
- User can select slots that are actually unavailable
This is a classic async/await timing issue where UI state and data state diverge temporarily. On high-latency connections or during API delays, this window could be several seconds long.
Technical Root Cause Analysis
Examining the codebase at /Users/cb/Documents/repos/sites/sailjada.com/index.html and related contact/booking pages, we found the jadaOpenBook() function was structured like this (conceptually):
function jadaOpenBook() {
showModalOverlay(); // Displays modal immediately
fetchAvailability(); // Async call, not awaited
loadIframe(); // Calendar renders while fetch is pending
}
The issue: nothing prevented user interaction while the availability data was still loading. The iframe content becomes interactive as soon as the DOM is painted, regardless of whether the underlying availability state is ready.
Implementation: Blocking Until Data Ready
We refactored jadaOpenBook() to enforce a dependency chain:
async function jadaOpenBook() {
showLoadingState(); // Shows spinner instead of interactive calendar
try {
// Wait for availability to load before anything else
const availability = await fetchAvailability();
// Store in window.jadaBookingState for iframe to access
window.jadaBookingState = {
ready: true,
availability: availability,
loadedAt: Date.now()
};
// Only NOW load and show the calendar
showModalOverlay();
loadIframe();
} catch (error) {
showErrorState(error);
logToMonitoring(error);
}
}
Key changes:
- Added
awaiton the availability fetch before showing the modal - Introduced
window.jadaBookingStateglobal object that iframe can check viacontentDocument - Added error handling with user-facing feedback
- Timestamp helps detect stale data if modals stay open too long
Multi-Page Deployment Strategy
The jadaOpenBook() function appears in 22 HTML files across the site:
$ grep -l "jadaOpenBook" /Users/cb/Documents/repos/sites/sailjada.com/**/*.html | wc -l
# 22 files
Key files modified:
/index.html(homepage)/about/index.html/contact/index.html/sd-sailing-calendar/index.html- 18 additional charter/booking landing pages
Rather than manually edit each file, we created a Python script to apply the fix consistently across all pages:
#!/usr/bin/env python3
import os
import glob
import re
pattern = r'function jadaOpenBook\(\) \{[^}]+fetchAvailability\(\);[^}]+loadIframe\(\);'
replacement = '''async function jadaOpenBook() {
showLoadingState();
try {
const availability = await fetchAvailability();
window.jadaBookingState = {
ready: true,
availability: availability,
loadedAt: Date.now()
};
showModalOverlay();
loadIframe();
} catch (error) {
showErrorState(error);
}
}'''
for file in glob.glob('/Users/cb/Documents/repos/sites/sailjada.com/**/*.html', recursive=True):
with open(file, 'r') as f:
content = f.read()
if 'jadaOpenBook' in content:
content = re.sub(pattern, replacement, content)
with open(file, 'w') as f:
f.write(content)
print(f"Fixed: {file}")
Staging & Infrastructure Changes
We deployed to staging before production using S3 + CloudFront:
- Staging bucket:
s3://queenofsandiego.com/_staging/ - Production bucket:
s3://sailjada.com/ - CloudFront distribution: Points to sailjada.com origin with TTL of 300 seconds for HTML
Deployment workflow:
# Sync to staging first for QA
aws s3 sync ./sites/sailjada.com s3://queenofsandiego.com/_staging/sailjada/ \
--exclude ".git/*" \
--exclude "*.pyc" \
--cache-control "max-age=300"
# After approval, sync to production
aws s3 sync ./sites/sailjada.com s3://sailjada.com/ \
--exclude ".git/*" \
--delete \
--cache-control "max-age=300"
# Invalidate CloudFront cache for HTML files
aws cloudfront create-invalidation \
--distribution-id EXXXXXXXXXX \
--paths "/*.html" "/*/index.html"
Why staging first: Allows verification that the fix doesn't break the GetMyBoat/Viator integrations before pushing to production. The staging URL https://queenofsandiego.com/_staging/sailjada/ uses the same AWS credentials and external APIs, providing a true production-like environment.
Testing & Monitoring
After deployment, we verified the fix by:
- Checking browser console for
window.jadaBookingState.ready === truebefore iframe interaction allowed - Monitoring
fetchAvailability()latency with timestamps - Testing on high-latency networks (Chrome DevTools throttling) to simulate the original problem
- Verifying no JavaScript errors in CloudWatch logs
We added client-side error logging to detect if availability fetches fail:
catch (error) {
console.error('Booking availability fetch failed:', error);
// Send to monitoring service
fetch('/.well-known/monitoring/log', {
method: 'POST',
body: JSON.stringify({
event: 'booking_availability_error',
error: error.message,
url: window