/** * Safari AJAX Handler with Timeout and Retry Logic * Provides robust AJAX handling for Safari browsers with automatic retry on failure * * @package HVAC_Community_Events * @since 2.0.0 */ var SafariAjaxHandler = (function() { 'use strict'; /** * Default configuration */ var defaults = { timeout: 30000, // 30 seconds maxRetries: 3, // Maximum retry attempts retryDelay: 1000, // Initial retry delay (exponential backoff) chunkSize: 100, // Items per chunk for large datasets progressCallback: null // Optional progress callback }; /** * Make AJAX request with retry logic * * @param {string} action - WordPress AJAX action * @param {object} data - Request data * @param {object} options - Optional configuration * @returns {jQuery.Deferred} Promise object */ function request(action, data, options) { var settings = jQuery.extend({}, defaults, options || {}); var attemptCount = 0; var deferred = jQuery.Deferred(); function makeRequest() { attemptCount++; console.log('[Safari AJAX] Attempt ' + attemptCount + ' for action: ' + action); // Add Safari-specific headers var ajaxOptions = { url: typeof hvac_find_trainer !== 'undefined' ? hvac_find_trainer.ajax_url : ajaxurl, type: 'POST', timeout: settings.timeout, data: jQuery.extend({ action: action, nonce: typeof hvac_find_trainer !== 'undefined' ? hvac_find_trainer.nonce : '', _safari_request: true, _attempt: attemptCount }, data), xhrFields: { withCredentials: true // Safari ITP compatibility }, beforeSend: function(xhr) { // Add custom headers for Safari xhr.setRequestHeader('X-Safari-Request', 'true'); xhr.setRequestHeader('X-Request-Attempt', attemptCount.toString()); } }; jQuery.ajax(ajaxOptions) .done(function(response) { console.log('[Safari AJAX] Success for action: ' + action); deferred.resolve(response); }) .fail(function(xhr, status, error) { console.error('[Safari AJAX] Failed attempt ' + attemptCount + ':', status, error); // Check if we should retry if (status === 'timeout' && attemptCount < settings.maxRetries) { // Calculate exponential backoff delay var delay = settings.retryDelay * Math.pow(2, attemptCount - 1); console.log('[Safari AJAX] Retrying after ' + delay + 'ms...'); // Update progress if callback provided if (typeof settings.progressCallback === 'function') { settings.progressCallback({ status: 'retrying', attempt: attemptCount, maxAttempts: settings.maxRetries, delay: delay }); } // Retry after delay setTimeout(function() { makeRequest(); }, delay); } else { // Max retries reached or non-timeout error var errorMsg = 'Request failed after ' + attemptCount + ' attempts: ' + error; console.error('[Safari AJAX] ' + errorMsg); // Update progress if callback provided if (typeof settings.progressCallback === 'function') { settings.progressCallback({ status: 'failed', error: errorMsg, attempts: attemptCount }); } deferred.reject(xhr, status, error); } }); } // Start the first request makeRequest(); return deferred.promise(); } /** * Process large datasets in chunks * * @param {string} action - WordPress AJAX action * @param {array} items - Array of items to process * @param {object} options - Optional configuration * @returns {jQuery.Deferred} Promise object */ function processChunked(action, items, options) { var settings = jQuery.extend({}, defaults, options || {}); var deferred = jQuery.Deferred(); var results = []; var chunks = []; // Split items into chunks for (var i = 0; i < items.length; i += settings.chunkSize) { chunks.push(items.slice(i, i + settings.chunkSize)); } console.log('[Safari AJAX] Processing ' + chunks.length + ' chunks of ' + settings.chunkSize + ' items'); var currentChunk = 0; function processNextChunk() { if (currentChunk >= chunks.length) { // All chunks processed console.log('[Safari AJAX] All chunks processed successfully'); deferred.resolve(results); return; } var chunk = chunks[currentChunk]; // Update progress if callback provided if (typeof settings.progressCallback === 'function') { settings.progressCallback({ status: 'processing', current: currentChunk + 1, total: chunks.length, percentage: Math.round(((currentChunk + 1) / chunks.length) * 100) }); } // Process current chunk request(action, { items: chunk }, settings) .done(function(response) { results.push(response); currentChunk++; // Process next chunk with small delay to prevent overload setTimeout(processNextChunk, 100); }) .fail(function(xhr, status, error) { console.error('[Safari AJAX] Chunk processing failed:', error); deferred.reject(xhr, status, error); }); } // Start processing processNextChunk(); return deferred.promise(); } /** * Cancel all pending requests */ function cancelAll() { // This would need to track active XHR objects console.log('[Safari AJAX] Cancelling all pending requests'); // Implementation would track and abort active XHR requests } /** * Check if Safari browser */ function isSafari() { return navigator.userAgent.indexOf('Safari') !== -1 && navigator.userAgent.indexOf('Chrome') === -1 && navigator.userAgent.indexOf('Chromium') === -1; } /** * Initialize Safari AJAX handler */ function init() { if (!isSafari()) { console.log('[Safari AJAX] Not Safari browser, handler not needed'); return false; } console.log('[Safari AJAX] Handler initialized for Safari browser'); // Override jQuery.ajax for Safari if needed if (window.hvac_safari_ajax_override) { var originalAjax = jQuery.ajax; jQuery.ajax = function(options) { // Add Safari-specific defaults if (!options.timeout) { options.timeout = defaults.timeout; } if (!options.xhrFields) { options.xhrFields = {}; } options.xhrFields.withCredentials = true; return originalAjax.call(this, options); }; console.log('[Safari AJAX] jQuery.ajax override applied'); } return true; } // Public API return { request: request, processChunked: processChunked, cancelAll: cancelAll, isSafari: isSafari, init: init, defaults: defaults }; })(); // Initialize on document ready jQuery(document).ready(function() { SafariAjaxHandler.init(); // Make globally available window.SafariAjaxHandler = SafariAjaxHandler; });