// HTTP рендер-сервис на виртуалке (топология Б, §5.6). POST /render {url,type} → {data}. // Портал (SelfRenderClient) зовёт как xf4. Ни базы, ни Redis, ни денег — только рисует. // Секрет портал↔виртуалка — заголовок X-Render-Key (env RENDER_KEY). Параллельность контекстов — // env MAX_CONCURRENCY (по памяти виртуалки, §9). Прокси — env PROXIES. Всё из окружения, не из гита. import http from 'node:http'; import { render, closeBrowser } from './browser.mjs'; import { createProxyPool } from './proxy-pool.mjs'; const PORT = Number(process.env.PORT || 8790); const KEY = String(process.env.RENDER_KEY || ''); const MAX = Math.max(1, Number(process.env.MAX_CONCURRENCY || 4)); const proxies = createProxyPool(process.env.PROXIES); // Семафор: не больше MAX браузер-контекстов одновременно (память). Лишние ждут очереди. let active = 0; const waiters = []; function acquire() { if (active < MAX) { active++; return Promise.resolve(); } return new Promise((res) => waiters.push(res)); } function release() { active--; const next = waiters.shift(); if (next) { active++; next(); } } function readBody(req) { return new Promise((resolve) => { let buf = ''; req.on('data', (c) => { buf += c; if (buf.length > 1_000_000) req.destroy(); }); req.on('end', () => resolve(buf)); }); } const server = http.createServer(async (req, res) => { const json = (code, obj) => { res.writeHead(code, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(obj)); }; if (req.method === 'GET' && req.url === '/health') return json(200, { ok: true, active, max: MAX, proxies: proxies.size }); if (req.method !== 'POST' || req.url !== '/render') return json(404, { error: 'not found' }); if (KEY === '' || req.headers['x-render-key'] !== KEY) return json(401, { error: 'unauthorized' }); let payload; try { payload = JSON.parse(await readBody(req)); } catch { return json(400, { error: 'bad json' }); } const url = String(payload?.url || ''); const allowed = ['initialState', 'html', 'yandex-list']; const type = allowed.includes(payload?.type) ? payload.type : 'html'; const scrolls = Math.max(1, Math.min(30, Number(payload?.scrolls) || 6)); if (!/^https?:\/\//i.test(url)) return json(400, { error: 'bad url' }); await acquire(); try { const data = await render(url, type, proxies.next(), scrolls); json(200, { data }); } catch (e) { json(200, { data: '', error: String(e).slice(0, 200) }); // не роняем поток портала — пустые данные } finally { release(); } }); server.listen(PORT, () => console.log(`[render-service] :${PORT} max=${MAX} proxies=${proxies.size}`)); for (const sig of ['SIGINT', 'SIGTERM']) { process.on(sig, async () => { await closeBrowser(); server.close(() => process.exit(0)); }); }