Skip to content

Commit 8adb320

Browse files
jeremymanningclaude
andcommitted
Improve slot prefetch: full booking window with client-side merging
Prefetch covers the entire valid booking range (min_notice to max_advance_days) in a single API call at 15-min granularity. When user selects a larger duration, client-side mergeIntoSlots() combines consecutive 15-min slots into the target duration. Cache hit serves slots instantly; cache miss falls back to fresh API call. Typical user flow: page loads → prefetch fires → user spends ~5s on Steps 1-2 → Step 3 loads instantly from cache. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent fc6263c commit 8adb320

2 files changed

Lines changed: 85 additions & 29 deletions

File tree

js/api-client.js

Lines changed: 78 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,36 +5,96 @@
55

66
const ApiClient = (function () {
77
let _baseUrl = null;
8-
var _slotCache = {}; // key: "startISO|endISO|duration" → Promise<result>
8+
var _prefetchPromise = null; // Promise<{slots: [{start,end}]}> for full date range
9+
var _prefetchRange = null; // {start: ISO, end: ISO}
910

1011
function init(appsScriptUrl) {
1112
_baseUrl = appsScriptUrl;
1213
}
1314

1415
/**
15-
* Prefetch slots for a date range at 15-min granularity.
16-
* Called early (page load) so data is ready when user reaches Step 3.
16+
* Prefetch slots for the full booking window at 15-min granularity.
17+
* Called on page load so data is ready when user reaches Step 3.
1718
*/
1819
function prefetchSlots(startDate, endDate) {
19-
var key = startDate + '|' + endDate + '|15';
20-
if (_slotCache[key]) return; // already prefetching
21-
_slotCache[key] = apiCall('getAvailableSlots', {
20+
if (_prefetchPromise) return;
21+
_prefetchRange = { start: startDate, end: endDate };
22+
_prefetchPromise = apiCall('getAvailableSlots', {
2223
startDate: startDate,
2324
endDate: endDate,
2425
durationMinutes: 15,
25-
}).catch(function () { delete _slotCache[key]; });
26+
}).catch(function () {
27+
_prefetchPromise = null;
28+
_prefetchRange = null;
29+
});
2630
}
2731

2832
/**
29-
* Get the cache key for a slot request. Returns cached promise if available.
33+
* Try to serve a slot request from the prefetched data.
34+
* The prefetch uses 15-min slots; for larger durations we filter
35+
* to only include slots whose time boundaries align with the requested duration.
3036
*/
31-
function getCachedSlots(startDate, endDate, durationMinutes) {
32-
var key = startDate + '|' + endDate + '|' + durationMinutes;
33-
return _slotCache[key] || null;
37+
function tryServePrefetch(startDate, endDate, durationMinutes) {
38+
if (!_prefetchPromise || !_prefetchRange) return null;
39+
// Check if requested range is within prefetched range
40+
if (new Date(startDate) < new Date(_prefetchRange.start) ||
41+
new Date(endDate) > new Date(_prefetchRange.end)) {
42+
return null;
43+
}
44+
var reqStart = new Date(startDate).getTime();
45+
var reqEnd = new Date(endDate).getTime();
46+
var durationMs = durationMinutes * 60 * 1000;
47+
48+
return _prefetchPromise.then(function (result) {
49+
if (!result || !result.slots) return result;
50+
// Filter slots to the requested date range and duration
51+
var filtered = result.slots.filter(function (slot) {
52+
var s = new Date(slot.start).getTime();
53+
var e = new Date(slot.end).getTime();
54+
return s >= reqStart && e <= reqEnd;
55+
});
56+
// For durations > 15 min, merge consecutive 15-min slots into
57+
// larger slots of the requested duration
58+
if (durationMinutes > 15) {
59+
filtered = mergeIntoSlots(filtered, durationMs);
60+
}
61+
return { success: true, slots: filtered };
62+
});
3463
}
3564

36-
function cacheSlots(startDate, endDate, durationMinutes, promise) {
37-
_slotCache[startDate + '|' + endDate + '|' + durationMinutes] = promise;
65+
/**
66+
* Merge consecutive 15-min slots into slots of the target duration.
67+
* E.g., four consecutive 15-min slots → one 60-min slot.
68+
*/
69+
function mergeIntoSlots(fifteenMinSlots, durationMs) {
70+
if (fifteenMinSlots.length === 0) return [];
71+
var slots = [];
72+
// Sort by start time
73+
fifteenMinSlots.sort(function (a, b) {
74+
return new Date(a.start).getTime() - new Date(b.start).getTime();
75+
});
76+
// Slide window: for each 15-min slot, check if there are enough
77+
// consecutive slots to fill the target duration
78+
for (var i = 0; i < fifteenMinSlots.length; i++) {
79+
var candidateStart = new Date(fifteenMinSlots[i].start).getTime();
80+
var candidateEnd = candidateStart + durationMs;
81+
// Check that all 15-min slots needed are present and consecutive
82+
var covered = candidateStart;
83+
var valid = true;
84+
for (var j = i; j < fifteenMinSlots.length && covered < candidateEnd; j++) {
85+
var slotStart = new Date(fifteenMinSlots[j].start).getTime();
86+
var slotEnd = new Date(fifteenMinSlots[j].end).getTime();
87+
if (slotStart !== covered) { valid = false; break; }
88+
covered = slotEnd;
89+
}
90+
if (valid && covered >= candidateEnd) {
91+
slots.push({
92+
start: fifteenMinSlots[i].start,
93+
end: new Date(candidateEnd).toISOString(),
94+
});
95+
}
96+
}
97+
return slots;
3898
}
3999

40100
async function apiCall(action, data) {
@@ -77,17 +137,16 @@ const ApiClient = (function () {
77137
}
78138

79139
async function getAvailableSlots(startDate, endDate, durationMinutes) {
80-
// Check cache first
81-
var cached = getCachedSlots(startDate, endDate, durationMinutes);
82-
if (cached) return cached;
140+
// Try to serve from prefetched data (covers full booking window)
141+
var fromPrefetch = tryServePrefetch(startDate, endDate, durationMinutes);
142+
if (fromPrefetch) return fromPrefetch;
83143

84-
var promise = apiCall('getAvailableSlots', {
144+
// Fallback: fresh API call
145+
return apiCall('getAvailableSlots', {
85146
startDate: startDate,
86147
endDate: endDate,
87148
durationMinutes: durationMinutes,
88149
});
89-
cacheSlots(startDate, endDate, durationMinutes, promise);
90-
return promise;
91150
}
92151

93152
async function createBooking(bookingData) {

js/app.js

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,13 @@ var App = (function () {
2929
// Initialize API client
3030
ApiClient.init(config.settings.apps_script_url);
3131

32-
// Prefetch slots for current week so data is ready when user reaches Step 3.
33-
// Uses 15-min granularity; backend returns the same availability windows
34-
// regardless of duration, just sliced differently.
35-
var now = new Date();
36-
var weekStart = new Date(now);
37-
weekStart.setDate(now.getDate() - now.getDay());
38-
weekStart.setHours(0, 0, 0, 0);
39-
var weekEnd = new Date(weekStart);
40-
weekEnd.setDate(weekStart.getDate() + 7);
41-
ApiClient.prefetchSlots(weekStart.toISOString(), weekEnd.toISOString());
32+
// Prefetch slots for the full booking window so data is ready when user
33+
// reaches Step 3. One API call covers all navigable weeks.
34+
var minHours = config.settings.min_notice_hours || 12;
35+
var maxDays = config.settings.max_advance_days || 90;
36+
var prefetchStart = new Date(Date.now() + minHours * 60 * 60 * 1000);
37+
var prefetchEnd = new Date(Date.now() + maxDays * 24 * 60 * 60 * 1000);
38+
ApiClient.prefetchSlots(prefetchStart.toISOString(), prefetchEnd.toISOString());
4239

4340
// Render meeting types
4441
renderMeetingTypes(config.meetingTypes);

0 commit comments

Comments
 (0)