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.html as 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.