Fixing a Race Condition in the SailJADA Booking Calendar: Synchronous State Management for Availability Data
What Was Done
We identified and resolved a critical race condition in the booking flow on staging.sailjada.com that allowed users to select unavailable time slots. The issue occurred because the booking modal became interactive before the availability calendar data finished loading from external APIs. This post details the technical approach, infrastructure changes, and deployment strategy.
The Problem: Asynchronous Loading Without Gates
The booking system used a function called jadaOpenBook() defined in /Users/cb/Documents/repos/sites/sailjada.com/index.html (and replicated across 22+ page templates) that opened a modal booking interface. The critical issue:
- The modal opened immediately via DOM manipulation targeting
jada-modal-overlayelements - Availability data loaded asynchronously via
fetch()calls to external booking providers (GetMyBoat, Viator, and internal calendar APIs) - The calendar UI became clickable during the fetch phase, before availability data populated
- Users could book slots that were already reserved
The root cause: no synchronization mechanism between modal opening and data availability. The code pattern looked conceptually like:
function jadaOpenBook() {
// Show modal immediately
document.querySelector('.jada-modal-overlay').style.display = 'block';
// Fetch happens asynchronously - modal already visible!
fetch('/api/availability')
.then(response => response.json())
.then(data => renderCalendar(data));
}
Technical Solution: Synchronous State Gating
We implemented a state management pattern using a jadaBookingState object that gates modal interactivity until data loads. The fix involved three components:
1. State Management Object
Added a booking state tracker to prevent user interaction before readiness:
const jadaBookingState = {
isLoading: true,
dataReady: false,
availabilityData: null,
setReady: function(data) {
this.availabilityData = data;
this.dataReady = true;
this.isLoading = false;
},
isInteractable: function() {
return this.dataReady && !this.isLoading;
}
};
2. Modified jadaOpenBook() Function
Updated the function to block modal interactivity until data arrives:
async function jadaOpenBook() {
const modal = document.querySelector('.jada-modal-overlay');
const calendar = document.querySelector('[data-jada-calendar]');
// Show loading state
modal.style.display = 'block';
calendar.setAttribute('aria-busy', 'true');
calendar.style.pointerEvents = 'none';
try {
const availability = await Promise.all([
fetch('/api/availability').then(r => r.json()),
fetch('/api/pricing').then(r => r.json())
]);
jadaBookingState.setReady(availability);
calendar.style.pointerEvents = 'auto';
calendar.setAttribute('aria-busy', 'false');
renderCalendar(availability);
} catch (error) {
console.error('Booking load failed:', error);
modal.style.display = 'none';
showErrorMessage('Unable to load booking calendar');
}
}
3. Click Handler Protection
Added validation to slot selection handlers:
document.addEventListener('click', function(e) {
if (e.target.matches('[data-jada-slot]')) {
if (!jadaBookingState.isInteractable()) {
e.preventDefault();
e.stopPropagation();
return false;
}
// Process slot selection
handleSlotSelection(e.target.dataset.jadaSlot);
}
});
File Changes and Deployment
The race condition existed in templated pages across the SailJADA site. We identified affected files using recursive grep patterns:
grep -l "jadaOpenBook" /Users/cb/Documents/repos/sites/sailjada.com/**/*.html
This revealed the pattern affected:
/index.html(homepage with embedded booking widget)/about/index.html/contact/index.html/sd-sailing-calendar/index.html- 20+ additional charter and service pages
We created a Python automation script to apply the fix consistently across all 22 files:
#!/usr/bin/env python3
import os
import glob
import re
files = glob.glob('/Users/cb/Documents/repos/sites/sailjada.com/**/*.html', recursive=True)
for filepath in files:
with open(filepath, 'r') as f:
content = f.read()
if 'jadaOpenBook' in content and 'jadaBookingState' not in content:
# Inject state management before first jadaOpenBook call
state_code = ''''''
content = state_code + content
with open(filepath, 'w') as f:
f.write(content)
print(f"✓ Fixed {filepath}")
Infrastructure and Deployment Strategy
Staging Deployment
Per the staging validation rule, we deployed to s3://queenofsandiego.com/_staging/sailjada/ for review before production:
aws s3 sync /Users/cb/Documents/repos/sites/sailjada.com \
s3://queenofsandiego.com/_staging/sailjada/ \
--exclude ".git*" \
--exclude "*.map" \
--cache-control "max-age=300"
This created a parallel staging environment at https://queenofsandiego.com/_staging/sailjada/ for validation testing.
Production Deployment
The production site is hosted on s3://sailjada-prod with CloudFront distribution caching. Key infrastructure:
- S3 Origin Bucket:
sailjada-prod - CloudFront Distribution ID: (environment-specific, sourced from
.secrets/repos.env) - Route53 DNS:
sailjada.comALIAS record pointing to CloudFront - Cache Invalidation: Automatic for all
*.htmlfiles post-deploy
Deployment command pattern (credentials sourced from secrets):
set -a; source /Users/cb/Documents/repos/.secrets/repos.env; set +a
aws s3 sync /Users/cb/Documents/repos/sites/sailjada.com \
s3://sailjada-prod \
--delete \
--exclude ".git*" \
--