Files
pulse/resilientApiClient.js
T
rcourtman f53550fc6c fix: simplify frontend update channel switching for better UX
- Remove confusing "preview mode" - channels switch and save immediately
- Eliminate complex localStorage channel persistence logic
- Reduce update reload time from 10+ seconds to 3 seconds
- Remove redundant "Switch to X & Update" buttons
- Make channel switching instant with immediate feedback

Users can now simply change the dropdown to switch channels instantly,
then click Apply Update for a fast 3-second reload experience.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-14 11:01:03 +01:00

162 lines
4.6 KiB
JavaScript

const axios = require('axios');
const https = require('https');
const dnsResolver = require('./dnsResolver');
/**
* Creates a resilient API client that handles DNS failures gracefully
*/
class ResilientApiClient {
constructor(baseConfig, authInterceptor) {
this.baseConfig = baseConfig;
this.authInterceptor = authInterceptor;
this.hostname = this.extractHostname(baseConfig.baseURL);
this.clients = new Map(); // Map of IP -> axios instance
this.lastWorkingIp = null;
}
extractHostname(url) {
try {
const urlObj = new URL(url);
return urlObj.hostname;
} catch (error) {
// Handle cases where baseURL might not be a full URL
const match = url.match(/^https?:\/\/([^:\/]+)/);
return match ? match[1] : null;
}
}
/**
* Create an axios instance for a specific IP
*/
createClientForIp(ip) {
// Replace hostname with IP in the base URL
const baseURL = this.baseConfig.baseURL.replace(this.hostname, ip);
const client = axios.create({
...this.baseConfig,
baseURL: baseURL,
httpsAgent: new https.Agent({
rejectUnauthorized: this.baseConfig.httpsAgent?.options?.rejectUnauthorized ?? true,
// Set a shorter keepAlive timeout for failed connections
keepAlive: true,
keepAliveMsecs: 1000,
timeout: this.baseConfig.timeout || 30000
}),
headers: {
...this.baseConfig.headers,
// Add Host header to ensure proper SSL certificate validation
'Host': this.hostname
}
});
// Apply auth interceptor if provided
if (this.authInterceptor) {
client.interceptors.request.use(this.authInterceptor);
}
// Add response interceptor to track working IPs
client.interceptors.response.use(
(response) => {
// Mark this IP as working
this.lastWorkingIp = ip;
return response;
},
(error) => {
// Mark IP as failed on certain errors
if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT' || error.code === 'EHOSTUNREACH') {
dnsResolver.markHostFailed(ip);
}
return Promise.reject(error);
}
);
return client;
}
/**
* Get or create a client for a specific IP
*/
getClientForIp(ip) {
if (!this.clients.has(ip)) {
this.clients.set(ip, this.createClientForIp(ip));
}
return this.clients.get(ip);
}
/**
* Try to make a request with automatic failover
*/
async request(config) {
// First, try to resolve the hostname
let ips;
try {
ips = await dnsResolver.resolveHostname(this.hostname);
} catch (error) {
console.error(`[ResilientApiClient] DNS resolution failed for ${this.hostname}: ${error.message}`);
throw new Error(`DNS resolution failed for ${this.hostname}: ${error.message}`);
}
if (ips.length === 0) {
throw new Error(`No IP addresses available for ${this.hostname}`);
}
// If we have a last working IP, try it first
if (this.lastWorkingIp && ips.includes(this.lastWorkingIp)) {
ips = [this.lastWorkingIp, ...ips.filter(ip => ip !== this.lastWorkingIp)];
}
let lastError;
// Try each IP in sequence
for (const ip of ips) {
if (dnsResolver.isHostFailed(ip)) {
continue;
}
try {
const client = this.getClientForIp(ip);
const response = await client.request(config);
return response;
} catch (error) {
lastError = error;
console.warn(`[ResilientApiClient] Request failed for ${this.hostname} via IP ${ip}: ${error.message}`);
// If this is a connection error, mark the IP as failed
if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT' || error.code === 'EHOSTUNREACH') {
dnsResolver.markHostFailed(ip);
}
// Continue to next IP
}
}
// All IPs failed
console.error(`[ResilientApiClient] All ${ips.length} IPs failed for ${this.hostname}`);
throw lastError || new Error(`All IPs failed for ${this.hostname}`);
}
// Proxy common axios methods
async get(url, config) {
return this.request({ ...config, method: 'GET', url });
}
async post(url, data, config) {
return this.request({ ...config, method: 'POST', url, data });
}
async put(url, data, config) {
return this.request({ ...config, method: 'PUT', url, data });
}
async delete(url, config) {
return this.request({ ...config, method: 'DELETE', url });
}
async patch(url, data, config) {
return this.request({ ...config, method: 'PATCH', url, data });
}
}
module.exports = ResilientApiClient;