Add request queue to prevent map/filter race conditions
- Add RequestQueue module with 200ms debounce and request cancellation - Queue cluster requests to abort pending when new viewport/filter changes - Queue property list requests to prevent stale data rendering - Track request IDs to discard responses from cancelled requests - Error handler ignores aborted requests (intentional cancellation) Fixes issue where changing filters then zooming map before load complete would render pins from previous filter state. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+1
-1
File diff suppressed because one or more lines are too long
@@ -9,6 +9,126 @@
|
||||
(function($) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Request Queue Manager
|
||||
* Handles debouncing and cancellation of AJAX requests to prevent race conditions.
|
||||
* When filters, zoom, or viewport change, pending requests are cancelled and
|
||||
* only the final requested state is fetched.
|
||||
*/
|
||||
var RequestQueue = {
|
||||
// Pending XHR objects by type
|
||||
pending: {
|
||||
clusters: null,
|
||||
properties: null
|
||||
},
|
||||
// Debounce timeouts by type
|
||||
timeouts: {
|
||||
clusters: null,
|
||||
properties: null
|
||||
},
|
||||
// Request IDs to track staleness
|
||||
requestIds: {
|
||||
clusters: 0,
|
||||
properties: 0
|
||||
},
|
||||
// Debounce delay in ms
|
||||
debounceDelay: 200,
|
||||
|
||||
/**
|
||||
* Queue a request with debouncing and cancellation
|
||||
* @param {string} type - 'clusters' or 'properties'
|
||||
* @param {function} requestFn - Function that returns jqXHR (receives requestId)
|
||||
* @param {function} successFn - Success callback (receives response, requestId)
|
||||
* @param {function} completeFn - Complete callback (optional)
|
||||
* @param {function} errorFn - Error callback (optional, receives jqXHR, textStatus, errorThrown)
|
||||
*/
|
||||
queue: function(type, requestFn, successFn, completeFn, errorFn) {
|
||||
var self = this;
|
||||
|
||||
// Cancel any pending debounce timeout
|
||||
if (this.timeouts[type]) {
|
||||
clearTimeout(this.timeouts[type]);
|
||||
this.timeouts[type] = null;
|
||||
}
|
||||
|
||||
// Abort any in-flight request
|
||||
if (this.pending[type]) {
|
||||
this.pending[type].abort();
|
||||
this.pending[type] = null;
|
||||
}
|
||||
|
||||
// Increment request ID
|
||||
this.requestIds[type]++;
|
||||
var requestId = this.requestIds[type];
|
||||
|
||||
// Schedule the request with debounce
|
||||
this.timeouts[type] = setTimeout(function() {
|
||||
self.timeouts[type] = null;
|
||||
|
||||
// Execute the request
|
||||
var xhr = requestFn(requestId);
|
||||
|
||||
if (xhr && xhr.then) {
|
||||
self.pending[type] = xhr;
|
||||
|
||||
xhr.done(function(response) {
|
||||
// Only process if this is still the latest request
|
||||
if (requestId === self.requestIds[type]) {
|
||||
successFn(response, requestId);
|
||||
}
|
||||
});
|
||||
|
||||
xhr.fail(function(jqXHR, textStatus, errorThrown) {
|
||||
// Only call error handler if this is still the latest request
|
||||
// and it wasn't aborted (which is intentional)
|
||||
if (errorFn && requestId === self.requestIds[type] && textStatus !== 'abort') {
|
||||
errorFn(jqXHR, textStatus, errorThrown);
|
||||
}
|
||||
});
|
||||
|
||||
xhr.always(function() {
|
||||
// Clear pending reference if this is the current request
|
||||
if (self.pending[type] === xhr) {
|
||||
self.pending[type] = null;
|
||||
}
|
||||
if (completeFn && requestId === self.requestIds[type]) {
|
||||
completeFn();
|
||||
}
|
||||
});
|
||||
}
|
||||
}, this.debounceDelay);
|
||||
},
|
||||
|
||||
/**
|
||||
* Cancel all pending requests of a specific type
|
||||
* @param {string} type - 'clusters' or 'properties' (optional, cancels all if omitted)
|
||||
*/
|
||||
cancel: function(type) {
|
||||
var types = type ? [type] : ['clusters', 'properties'];
|
||||
var self = this;
|
||||
|
||||
types.forEach(function(t) {
|
||||
if (self.timeouts[t]) {
|
||||
clearTimeout(self.timeouts[t]);
|
||||
self.timeouts[t] = null;
|
||||
}
|
||||
if (self.pending[t]) {
|
||||
self.pending[t].abort();
|
||||
self.pending[t] = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a request type is currently loading
|
||||
* @param {string} type - 'clusters' or 'properties'
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isLoading: function(type) {
|
||||
return !!(this.pending[type] || this.timeouts[type]);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Property Map Manager
|
||||
* Uses server-side clustering for efficient rendering of 30k+ properties
|
||||
@@ -23,8 +143,6 @@
|
||||
hoveredPropertyId: null,
|
||||
baseZIndex: 400,
|
||||
currentFilters: {},
|
||||
isLoading: false,
|
||||
loadTimeout: null,
|
||||
currentMode: null, // Track current visualization mode
|
||||
|
||||
/**
|
||||
@@ -95,29 +213,16 @@
|
||||
|
||||
/**
|
||||
* Load clusters/markers from server based on viewport
|
||||
* Uses RequestQueue for debouncing and cancellation
|
||||
*/
|
||||
loadClusters: function() {
|
||||
if (!this.map) return;
|
||||
|
||||
var self = this;
|
||||
|
||||
// Debounce rapid requests
|
||||
clearTimeout(this.loadTimeout);
|
||||
this.loadTimeout = setTimeout(function() {
|
||||
self._doLoadClusters();
|
||||
}, 150);
|
||||
},
|
||||
|
||||
_doLoadClusters: function() {
|
||||
if (this.isLoading) return;
|
||||
|
||||
var self = this;
|
||||
var bounds = this.map.getBounds();
|
||||
var center = this.map.getCenter();
|
||||
var zoom = this.map.getZoom();
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
// Bounds array for both map clusters and property list
|
||||
var boundsArray = [
|
||||
bounds.getSouthWest().lat,
|
||||
@@ -139,14 +244,20 @@
|
||||
min_beds: this.currentFilters.min_beds || ''
|
||||
};
|
||||
|
||||
// Also update the property list with the same viewport
|
||||
// Also update the property list with the same viewport (queued separately)
|
||||
PropertyFilters.updateFromMap(boundsArray, centerArray);
|
||||
|
||||
$.ajax({
|
||||
// Queue the cluster request with debounce and cancellation
|
||||
RequestQueue.queue(
|
||||
'clusters',
|
||||
function(requestId) {
|
||||
return $.ajax({
|
||||
url: homeprozMapData.clusterEndpoint,
|
||||
type: 'GET',
|
||||
data: requestData,
|
||||
success: function(response) {
|
||||
data: requestData
|
||||
});
|
||||
},
|
||||
function(response, requestId) {
|
||||
if (response.success) {
|
||||
var data = response.data;
|
||||
self.currentMode = data.type;
|
||||
@@ -163,11 +274,8 @@
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
complete: function() {
|
||||
self.isLoading = false;
|
||||
}
|
||||
});
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -525,7 +633,6 @@
|
||||
|
||||
// State
|
||||
isFirstLoad: true,
|
||||
isLoading: false,
|
||||
mapBounds: null, // Current map viewport bounds
|
||||
mapCenter: null, // Current map center for distance sorting
|
||||
isMapUpdate: false, // Flag to prevent map->filter->map loop
|
||||
@@ -646,20 +753,16 @@
|
||||
|
||||
/**
|
||||
* Filter properties via AJAX
|
||||
* Uses RequestQueue for debouncing and cancellation to prevent race conditions
|
||||
*/
|
||||
filterProperties: function(page, updateHistory) {
|
||||
if (this.isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateHistory = updateHistory !== false;
|
||||
page = page || 1;
|
||||
|
||||
var self = this;
|
||||
var formData = this.getFormData();
|
||||
|
||||
// Show loading state
|
||||
this.isLoading = true;
|
||||
// Show loading state immediately
|
||||
this.$filters.addClass('is-loading');
|
||||
|
||||
// Show spinner only on first load
|
||||
@@ -688,20 +791,29 @@
|
||||
requestData.center = this.mapCenter;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
// Capture isMapUpdate state before it might change
|
||||
var wasMapUpdate = this.isMapUpdate;
|
||||
this.isMapUpdate = false;
|
||||
|
||||
// Queue the property list request with debounce and cancellation
|
||||
RequestQueue.queue(
|
||||
'properties',
|
||||
function(requestId) {
|
||||
return $.ajax({
|
||||
url: homeprozAjax.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: requestData,
|
||||
success: function(response) {
|
||||
data: requestData
|
||||
});
|
||||
},
|
||||
function(response, requestId) {
|
||||
if (response.success) {
|
||||
self.$results.html(response.data.html);
|
||||
self.isFirstLoad = false;
|
||||
|
||||
// Update map with new filter params (but not if this was triggered by map move)
|
||||
if (response.data.filters && !self.isMapUpdate) {
|
||||
if (response.data.filters && !wasMapUpdate) {
|
||||
PropertyMap.updateFilters(response.data.filters);
|
||||
}
|
||||
self.isMapUpdate = false;
|
||||
|
||||
// Recalculate layout after content update
|
||||
if (typeof LayoutCalculator !== 'undefined') {
|
||||
@@ -730,14 +842,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
self.$results.html('<div class="no-properties"><h3>Error</h3><p>Something went wrong. Please try again.</p></div>');
|
||||
},
|
||||
complete: function() {
|
||||
self.isLoading = false;
|
||||
function() {
|
||||
// Complete callback - remove loading state
|
||||
self.$filters.removeClass('is-loading');
|
||||
},
|
||||
function() {
|
||||
// Error callback - show error message
|
||||
self.$results.html('<div class="no-properties"><h3>Error</h3><p>Something went wrong. Please try again.</p></div>');
|
||||
}
|
||||
});
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -1486,13 +1599,16 @@
|
||||
* Reset infinite scroll (called on filter/map change)
|
||||
*/
|
||||
reset: function() {
|
||||
// Cancel all pending requests
|
||||
// Cancel all pending infinite scroll requests
|
||||
for (var page in InfiniteScrollState.pendingRequests) {
|
||||
if (InfiniteScrollState.pendingRequests[page]) {
|
||||
InfiniteScrollState.pendingRequests[page].abort();
|
||||
}
|
||||
}
|
||||
|
||||
// Also cancel pending property and cluster requests via RequestQueue
|
||||
RequestQueue.cancel();
|
||||
|
||||
// Clear state
|
||||
this.resetState();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user