Rene Fichtmueller 1026787318 feat: add proxy network, image backfill, and scraper improvements
- Add TIP Proxy Network (packages/proxy-agent): SOCKS5 proxy agent
  for residential IP bypass of CloudFront WAF blocks
- Add /api/proxy/* routes: node registration, heartbeat, load balancing
- Add image extraction to Flexoptix catalog scraper (GraphQL small_image)
- Add image extraction to Optcore scraper (Playwright gallery img)
- Fix Fluxlight price scraping (BigCommerce HTML structure: data-product-price-without-tax)
- Add SmartOptics scraper (8 DWDM/coherent products, og:image extraction)
- Fix findOrCreateScrapedTransceiver to update image_url for existing records
- Add image backfill script (backfill-images.ts): 178 Flexoptix images added
- Fix DB connection pool: max 5, idleTimeoutMillis 10s (was unlimited, caused >100 connections)
- Add proxy.ts utility for scraper proxy rotation
2026-04-03 21:13:03 +02:00

83 lines
2.2 KiB
TypeScript

/**
* Heartbeat — sends periodic status updates to TIP routing server.
* POST /api/proxy/heartbeat every 30s.
*/
import * as https from "https";
import * as http from "http";
export interface HeartbeatPayload {
token: string;
ip?: string;
port: number;
bytesProxied: number;
requestsProxied: number;
latencyMs?: number;
version: string;
}
export interface HeartbeatResult {
ok: boolean;
latencyMs: number;
error?: string;
}
function postJson(url: string, body: unknown): Promise<{ status: number; latencyMs: number }> {
return new Promise((resolve, reject) => {
const start = Date.now();
const json = JSON.stringify(body);
const parsed = new URL(url);
const mod = parsed.protocol === "https:" ? https : http;
const req = mod.request(
{
hostname: parsed.hostname,
port: parsed.port || (parsed.protocol === "https:" ? 443 : 80),
path: parsed.pathname + parsed.search,
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(json),
"User-Agent": "TIP-Agent/1.0.0",
},
timeout: 10_000,
},
(res) => {
res.resume();
resolve({ status: res.statusCode ?? 0, latencyMs: Date.now() - start });
}
);
req.on("timeout", () => { req.destroy(); reject(new Error("timeout")); });
req.on("error", reject);
req.write(json);
req.end();
});
}
export async function sendHeartbeat(
serverUrl: string,
payload: HeartbeatPayload
): Promise<HeartbeatResult> {
const url = `${serverUrl}/api/proxy/heartbeat`;
try {
const { status, latencyMs } = await postJson(url, payload);
return { ok: status >= 200 && status < 300, latencyMs };
} catch (err) {
return { ok: false, latencyMs: 0, error: String(err) };
}
}
export function startHeartbeatLoop(
serverUrl: string,
getPayload: () => HeartbeatPayload,
onResult?: (r: HeartbeatResult) => void,
intervalMs = 30_000
): NodeJS.Timeout {
const tick = async () => {
const result = await sendHeartbeat(serverUrl, getPayload());
onResult?.(result);
};
tick();
return setInterval(tick, intervalMs);
}