Skip to content

Commit 1560418

Browse files
SashaMITcursoragent
andcommitted
perf: add compression, cache headers, and connection pooling to web gateway
- Gzip compression for text-based proxy responses (HTML, CSS, JS, JSON, SVG, XML). Skips already-encoded, binary, and sub-1KB responses. Typically reduces transfer size by 50-70%. - Cache-Control headers for static assets: 7d for images/fonts, 1d for JS/CSS. Only supplements missing headers -- node-set policies preserved. - HTTP keep-alive agent for WireGuard/direct proxy targets. Reuses TCP connections instead of opening one per request, eliminating repeated handshake latency. NOT applied to Boson ActiveProxy (requires close). All changes are transparent to the PC2 node and end user. WebSocket upgrades, API routes, and Boson relay paths are unaffected. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 701a85d commit 1560418

1 file changed

Lines changed: 77 additions & 2 deletions

File tree

deploy/web-gateway/index.js

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,34 @@ import http from "http";
1414
import https from "https";
1515
import path from "path";
1616
import net from "net";
17+
import zlib from "zlib";
1718
import { execSync } from "child_process";
1819
import httpProxy from "http-proxy";
1920
const { createProxyServer } = httpProxy;
2021

22+
// ============================================================================
23+
// Performance: Keep-alive agent for WireGuard / direct HTTP proxy targets.
24+
// Reuses TCP connections to PC2 nodes instead of opening one per request.
25+
// NOT used for Boson ActiveProxy (requires Connection: close).
26+
// ============================================================================
27+
const keepAliveAgent = new http.Agent({
28+
keepAlive: true,
29+
maxSockets: 64,
30+
maxFreeSockets: 16,
31+
timeout: 60_000,
32+
});
33+
34+
// ============================================================================
35+
// Performance: Compression + cache header configuration
36+
// ============================================================================
37+
const COMPRESSIBLE_TYPES = /^text\/|\/json|\/javascript|\/xml|\+xml|\/svg/;
38+
const MIN_COMPRESS_SIZE = 1024; // Don't compress responses under 1KB
39+
40+
const STATIC_CACHE_RULES = {
41+
long: { maxAge: 604800, extensions: /\.(png|jpe?g|gif|webp|ico|woff2?|ttf|eot)$/i },
42+
medium: { maxAge: 86400, extensions: /\.(js|css|svg|map)$/i },
43+
};
44+
2145
// Configuration (supports environment variables for multi-gateway deployment)
2246
const CONFIG = {
2347
// Gateway identity for multi-instance deployments
@@ -1367,6 +1391,56 @@ proxy.on("error", (err, req, res) => {
13671391
}
13681392
});
13691393

1394+
// ============================================================================
1395+
// Performance: Intercept proxy responses to add compression + cache headers.
1396+
// Operates transparently -- the PC2 node receives identical requests and its
1397+
// response headers are preserved. We only supplement missing Cache-Control and
1398+
// compress text-based responses that the browser can accept compressed.
1399+
// ============================================================================
1400+
proxy.on("proxyRes", (proxyRes, req, res) => {
1401+
const reqUrl = req.url || '';
1402+
const contentType = proxyRes.headers['content-type'] || '';
1403+
1404+
// --- Cache headers for static assets (only if node didn't set its own) ---
1405+
if (!proxyRes.headers['cache-control']) {
1406+
for (const rule of Object.values(STATIC_CACHE_RULES)) {
1407+
if (rule.extensions.test(reqUrl)) {
1408+
proxyRes.headers['cache-control'] = `public, max-age=${rule.maxAge}`;
1409+
break;
1410+
}
1411+
}
1412+
}
1413+
1414+
// --- Gzip compression for text-based responses ---
1415+
const acceptEncoding = req.headers['accept-encoding'] || '';
1416+
const alreadyEncoded = !!proxyRes.headers['content-encoding'];
1417+
const isCompressible = COMPRESSIBLE_TYPES.test(contentType);
1418+
const contentLength = parseInt(proxyRes.headers['content-length'] || '0', 10);
1419+
const tooSmall = contentLength > 0 && contentLength < MIN_COMPRESS_SIZE;
1420+
const supportsGzip = acceptEncoding.includes('gzip');
1421+
1422+
if (!alreadyEncoded && isCompressible && !tooSmall && supportsGzip) {
1423+
// Remove content-length (unknown after compression) and set encoding
1424+
delete proxyRes.headers['content-length'];
1425+
proxyRes.headers['content-encoding'] = 'gzip';
1426+
proxyRes.headers['vary'] = 'Accept-Encoding';
1427+
1428+
// Pipe through gzip -- override res.write/end to compress on the fly
1429+
const gzipStream = zlib.createGzip({ level: 6 });
1430+
const originalWrite = res.write.bind(res);
1431+
const originalEnd = res.end.bind(res);
1432+
1433+
gzipStream.on('data', (chunk) => originalWrite(chunk));
1434+
gzipStream.on('end', () => originalEnd());
1435+
1436+
res.write = (chunk) => gzipStream.write(chunk);
1437+
res.end = (chunk) => {
1438+
if (chunk) gzipStream.write(chunk);
1439+
gzipStream.end();
1440+
};
1441+
}
1442+
});
1443+
13701444
// Extract username from hostname
13711445
function extractUsername(hostname) {
13721446
if (!hostname) return null;
@@ -1458,10 +1532,11 @@ async function handleRequest(req, res) {
14581532
return;
14591533
}
14601534

1461-
// Direct HTTP proxy (includes WireGuard tunnel endpoints at 10.100.x.x)
1535+
// Direct HTTP proxy (includes WireGuard tunnel endpoints at 10.100.x.x).
1536+
// Uses keep-alive agent to reuse TCP connections to the PC2 node.
14621537
const viaWG = isWireGuardIP(nodeInfo.endpoint);
14631538
console.log(`[Gateway] Proxying ${username} -> ${nodeInfo.endpoint}${viaWG ? ' (WireGuard tunnel)' : ''}`);
1464-
proxy.web(req, res, { target: nodeInfo.endpoint });
1539+
proxy.web(req, res, { target: nodeInfo.endpoint, agent: keepAliveAgent });
14651540
}
14661541

14671542
// API request handler

0 commit comments

Comments
 (0)