Commit c5855911 authored by Vitaly Lipatov's avatar Vitaly Lipatov

web-api: instant first SSE event, resolve/routes/whois stream async

Send initial "check" event immediately (just domain name), then resolve IPs, find routes, fetch whois and gateway checks all run in parallel. Each result streams via its own SSE event: - resolve: IPs + routes - gateway: per-gateway availability - whois: domain registration info - throttle: slow download detection Eliminates all delay before first visible response. Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent a2d006a2
...@@ -1112,39 +1112,11 @@ function renderCheck(data) { ...@@ -1112,39 +1112,11 @@ function renderCheck(data) {
const container = $('check-gateways'); const container = $('check-gateways');
container.textContent = ''; container.textContent = '';
// Two-column row: left = gateways (filled by SSE), right = info // Two-column row: left = gateways (filled by SSE), right = info (filled by SSE)
const cols = mkDiv('check-columns'); const cols = mkDiv('check-columns');
const colGw = mkDiv('check-col'); const colGw = mkDiv('check-col');
colGw.id = 'check-col-gw'; colGw.id = 'check-col-gw';
const colInfo = mkDiv('check-col'); const colInfo = mkDiv('check-col');
// Right column: IPs + routes (whois arrives via SSE)
if (data.ips && data.ips.length) {
const sec = mkDiv('check-section');
sec.appendChild(mkDiv('check-section-title', 'IP-\\u0430\\u0434\\u0440\\u0435\\u0441\\u0430'));
sec.appendChild(mkDiv('check-ips', data.ips.join(', ')));
colInfo.appendChild(sec);
}
const rsec = mkDiv('check-section');
rsec.appendChild(mkDiv('check-section-title', '\\u0412 \\u0441\\u043f\\u0438\\u0441\\u043a\\u0430\\u0445 \\u043c\\u0430\\u0440\\u0448\\u0440\\u0443\\u0442\\u043e\\u0432'));
if (data.routes && data.routes.length) {
const byList = {};
data.routes.forEach(r => {
const key = r.group + '/' + r.list;
if (!byList[key]) byList[key] = [];
byList[key].push(r.ip ? r.ip + ' \\u2208 ' + r.entry : r.entry);
});
for (const [key, items] of Object.entries(byList)) {
const unique = [...new Set(items)];
const el = mkDiv('check-route', key + ': ' + unique.join(', '));
rsec.appendChild(el);
}
} else {
rsec.appendChild(mkDiv('check-no-route', '\\u041d\\u0435 \\u043d\\u0430\\u0439\\u0434\\u0435\\u043d\\u043e \\u0432 \\u0441\\u043f\\u0438\\u0441\\u043a\\u0430\\u0445 \\u043c\\u0430\\u0440\\u0448\\u0440\\u0443\\u0442\\u043e\\u0432'));
}
colInfo.appendChild(rsec);
cols.appendChild(colGw); cols.appendChild(colGw);
cols.appendChild(colInfo); cols.appendChild(colInfo);
container.appendChild(cols); container.appendChild(cols);
...@@ -1245,6 +1217,34 @@ async function checkDomain() { ...@@ -1245,6 +1217,34 @@ async function checkDomain() {
el.textContent = gw + ': ' + info.speed_str; el.textContent = gw + ': ' + info.speed_str;
} }
gwWrap.appendChild(el); gwWrap.appendChild(el);
} else if (evType === 'resolve') {
// Add IPs + routes to right column
const colInfo1 = document.querySelector('#check-gateways .check-col:last-child');
if (colInfo1) {
if (parsed.ips && parsed.ips.length) {
const sec = mkDiv('check-section');
sec.appendChild(mkDiv('check-section-title', 'IP-\\u0430\\u0434\\u0440\\u0435\\u0441\\u0430'));
sec.appendChild(mkDiv('check-ips', parsed.ips.join(', ')));
colInfo1.appendChild(sec);
}
const rsec = mkDiv('check-section');
rsec.appendChild(mkDiv('check-section-title', '\\u0412 \\u0441\\u043f\\u0438\\u0441\\u043a\\u0430\\u0445 \\u043c\\u0430\\u0440\\u0448\\u0440\\u0443\\u0442\\u043e\\u0432'));
if (parsed.routes && parsed.routes.length) {
const byList = {};
parsed.routes.forEach(r => {
const key = r.group + '/' + r.list;
if (!byList[key]) byList[key] = [];
byList[key].push(r.ip ? r.ip + ' \\u2208 ' + r.entry : r.entry);
});
for (const [key, items] of Object.entries(byList)) {
const unique = [...new Set(items)];
rsec.appendChild(mkDiv('check-route', key + ': ' + unique.join(', ')));
}
} else {
rsec.appendChild(mkDiv('check-no-route', '\\u041d\\u0435 \\u043d\\u0430\\u0439\\u0434\\u0435\\u043d\\u043e \\u0432 \\u0441\\u043f\\u0438\\u0441\\u043a\\u0430\\u0445 \\u043c\\u0430\\u0440\\u0448\\u0440\\u0443\\u0442\\u043e\\u0432'));
}
colInfo1.appendChild(rsec);
}
} else if (evType === 'whois') { } else if (evType === 'whois') {
// Add whois to right column // Add whois to right column
const colInfo = document.querySelector('#check-gateways .check-col:last-child'); const colInfo = document.querySelector('#check-gateways .check-col:last-child');
...@@ -1736,24 +1736,34 @@ class RouteHandler(http.server.BaseHTTPRequestHandler): ...@@ -1736,24 +1736,34 @@ class RouteHandler(http.server.BaseHTTPRequestHandler):
self.send_header("Cache-Control", "no-cache") self.send_header("Cache-Control", "no-cache")
self.end_headers() self.end_headers()
# Phase 0: quick data (IPs, routes) — instant # Send check event immediately — no blocking
ips = resolve_domain(domain) base = {"domain": domain}
routes = find_in_routes(domain, ips)
base = {"domain": domain, "ips": ips, "routes": routes}
if file_url: if file_url:
base["file_url"] = file_url base["file_url"] = file_url
self.send_sse("check", base) self.send_sse("check", base)
if file_url: if file_url:
# File URL mode: whois + speed test # File URL mode: resolve + whois + speed test
whois_info = get_whois(domain) def _resolve_and_routes():
if whois_info: ips = resolve_domain(domain)
self.send_sse("whois", {"whois": whois_info}) routes = find_in_routes(domain, ips)
return ips, routes
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as pool:
resolve_future = pool.submit(_resolve_and_routes)
whois_future = pool.submit(get_whois, domain)
ips, routes = resolve_future.result()
self.send_sse("resolve", {"ips": ips, "routes": routes})
whois_info = whois_future.result()
if whois_info:
self.send_sse("whois", {"whois": whois_info})
def on_speed(gw_name, res): def on_speed(gw_name, res):
self.send_sse("speed", {"gateway": gw_name, "result": res}) self.send_sse("speed", {"gateway": gw_name, "result": res})
run_speed_test(file_url, on_speed) run_speed_test(file_url, on_speed)
else: else:
# Domain mode: stream gateway checks # Domain mode: resolve + gateways + whois all in parallel
url = "https://%s/" % domain url = "https://%s/" % domain
has_v6 = False has_v6 = False
try: try:
...@@ -1762,8 +1772,14 @@ class RouteHandler(http.server.BaseHTTPRequestHandler): ...@@ -1762,8 +1772,14 @@ class RouteHandler(http.server.BaseHTTPRequestHandler):
except socket.gaierror: except socket.gaierror:
pass pass
workers = len(CHECK_GATEWAYS) * 2 + 2 def _resolve_and_routes():
ips = resolve_domain(domain)
routes = find_in_routes(domain, ips)
return ips, routes
workers = len(CHECK_GATEWAYS) * 2 + 4
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as pool: with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as pool:
resolve_future = pool.submit(_resolve_and_routes)
gw_futures = {} gw_futures = {}
for name, proxy_v4, proxy_v6 in CHECK_GATEWAYS: for name, proxy_v4, proxy_v6 in CHECK_GATEWAYS:
f4 = pool.submit(_check_one, name, proxy_v4, url, "-4") f4 = pool.submit(_check_one, name, proxy_v4, url, "-4")
...@@ -1774,6 +1790,10 @@ class RouteHandler(http.server.BaseHTTPRequestHandler): ...@@ -1774,6 +1790,10 @@ class RouteHandler(http.server.BaseHTTPRequestHandler):
whois_future = pool.submit(get_whois, domain) whois_future = pool.submit(get_whois, domain)
assets_future = pool.submit(_find_assets, CHECK_GATEWAYS[0][1], url) assets_future = pool.submit(_find_assets, CHECK_GATEWAYS[0][1], url)
# Send resolve as soon as ready
ips, routes = resolve_future.result()
self.send_sse("resolve", {"ips": ips, "routes": routes})
for future in concurrent.futures.as_completed(gw_futures): for future in concurrent.futures.as_completed(gw_futures):
name, ipver, status, code = future.result() name, ipver, status, code = future.result()
self.send_sse("gateway", { self.send_sse("gateway", {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment