```html

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 await on the availability fetch before showing the modal
  • Introduced window.jadaBookingState global object that iframe can check via contentDocument
  • 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 === true before 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