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($) {
|
(function($) {
|
||||||
'use strict';
|
'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
|
* Property Map Manager
|
||||||
* Uses server-side clustering for efficient rendering of 30k+ properties
|
* Uses server-side clustering for efficient rendering of 30k+ properties
|
||||||
@@ -23,8 +143,6 @@
|
|||||||
hoveredPropertyId: null,
|
hoveredPropertyId: null,
|
||||||
baseZIndex: 400,
|
baseZIndex: 400,
|
||||||
currentFilters: {},
|
currentFilters: {},
|
||||||
isLoading: false,
|
|
||||||
loadTimeout: null,
|
|
||||||
currentMode: null, // Track current visualization mode
|
currentMode: null, // Track current visualization mode
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -95,29 +213,16 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Load clusters/markers from server based on viewport
|
* Load clusters/markers from server based on viewport
|
||||||
|
* Uses RequestQueue for debouncing and cancellation
|
||||||
*/
|
*/
|
||||||
loadClusters: function() {
|
loadClusters: function() {
|
||||||
if (!this.map) return;
|
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 self = this;
|
||||||
var bounds = this.map.getBounds();
|
var bounds = this.map.getBounds();
|
||||||
var center = this.map.getCenter();
|
var center = this.map.getCenter();
|
||||||
var zoom = this.map.getZoom();
|
var zoom = this.map.getZoom();
|
||||||
|
|
||||||
this.isLoading = true;
|
|
||||||
|
|
||||||
// Bounds array for both map clusters and property list
|
// Bounds array for both map clusters and property list
|
||||||
var boundsArray = [
|
var boundsArray = [
|
||||||
bounds.getSouthWest().lat,
|
bounds.getSouthWest().lat,
|
||||||
@@ -139,14 +244,20 @@
|
|||||||
min_beds: this.currentFilters.min_beds || ''
|
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);
|
PropertyFilters.updateFromMap(boundsArray, centerArray);
|
||||||
|
|
||||||
$.ajax({
|
// Queue the cluster request with debounce and cancellation
|
||||||
|
RequestQueue.queue(
|
||||||
|
'clusters',
|
||||||
|
function(requestId) {
|
||||||
|
return $.ajax({
|
||||||
url: homeprozMapData.clusterEndpoint,
|
url: homeprozMapData.clusterEndpoint,
|
||||||
type: 'GET',
|
type: 'GET',
|
||||||
data: requestData,
|
data: requestData
|
||||||
success: function(response) {
|
});
|
||||||
|
},
|
||||||
|
function(response, requestId) {
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
var data = response.data;
|
var data = response.data;
|
||||||
self.currentMode = data.type;
|
self.currentMode = data.type;
|
||||||
@@ -163,11 +274,8 @@
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
complete: function() {
|
|
||||||
self.isLoading = false;
|
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -525,7 +633,6 @@
|
|||||||
|
|
||||||
// State
|
// State
|
||||||
isFirstLoad: true,
|
isFirstLoad: true,
|
||||||
isLoading: false,
|
|
||||||
mapBounds: null, // Current map viewport bounds
|
mapBounds: null, // Current map viewport bounds
|
||||||
mapCenter: null, // Current map center for distance sorting
|
mapCenter: null, // Current map center for distance sorting
|
||||||
isMapUpdate: false, // Flag to prevent map->filter->map loop
|
isMapUpdate: false, // Flag to prevent map->filter->map loop
|
||||||
@@ -646,20 +753,16 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter properties via AJAX
|
* Filter properties via AJAX
|
||||||
|
* Uses RequestQueue for debouncing and cancellation to prevent race conditions
|
||||||
*/
|
*/
|
||||||
filterProperties: function(page, updateHistory) {
|
filterProperties: function(page, updateHistory) {
|
||||||
if (this.isLoading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateHistory = updateHistory !== false;
|
updateHistory = updateHistory !== false;
|
||||||
page = page || 1;
|
page = page || 1;
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
var formData = this.getFormData();
|
var formData = this.getFormData();
|
||||||
|
|
||||||
// Show loading state
|
// Show loading state immediately
|
||||||
this.isLoading = true;
|
|
||||||
this.$filters.addClass('is-loading');
|
this.$filters.addClass('is-loading');
|
||||||
|
|
||||||
// Show spinner only on first load
|
// Show spinner only on first load
|
||||||
@@ -688,20 +791,29 @@
|
|||||||
requestData.center = this.mapCenter;
|
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,
|
url: homeprozAjax.ajaxUrl,
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
data: requestData,
|
data: requestData
|
||||||
success: function(response) {
|
});
|
||||||
|
},
|
||||||
|
function(response, requestId) {
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
self.$results.html(response.data.html);
|
self.$results.html(response.data.html);
|
||||||
self.isFirstLoad = false;
|
self.isFirstLoad = false;
|
||||||
|
|
||||||
// Update map with new filter params (but not if this was triggered by map move)
|
// 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);
|
PropertyMap.updateFilters(response.data.filters);
|
||||||
}
|
}
|
||||||
self.isMapUpdate = false;
|
|
||||||
|
|
||||||
// Recalculate layout after content update
|
// Recalculate layout after content update
|
||||||
if (typeof LayoutCalculator !== 'undefined') {
|
if (typeof LayoutCalculator !== 'undefined') {
|
||||||
@@ -730,14 +842,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: function() {
|
function() {
|
||||||
self.$results.html('<div class="no-properties"><h3>Error</h3><p>Something went wrong. Please try again.</p></div>');
|
// Complete callback - remove loading state
|
||||||
},
|
|
||||||
complete: function() {
|
|
||||||
self.isLoading = false;
|
|
||||||
self.$filters.removeClass('is-loading');
|
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 infinite scroll (called on filter/map change)
|
||||||
*/
|
*/
|
||||||
reset: function() {
|
reset: function() {
|
||||||
// Cancel all pending requests
|
// Cancel all pending infinite scroll requests
|
||||||
for (var page in InfiniteScrollState.pendingRequests) {
|
for (var page in InfiniteScrollState.pendingRequests) {
|
||||||
if (InfiniteScrollState.pendingRequests[page]) {
|
if (InfiniteScrollState.pendingRequests[page]) {
|
||||||
InfiniteScrollState.pendingRequests[page].abort();
|
InfiniteScrollState.pendingRequests[page].abort();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also cancel pending property and cluster requests via RequestQueue
|
||||||
|
RequestQueue.cancel();
|
||||||
|
|
||||||
// Clear state
|
// Clear state
|
||||||
this.resetState();
|
this.resetState();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user