114 lines
3.2 KiB
TypeScript
114 lines
3.2 KiB
TypeScript
import express from 'express';
|
|
import { ScreenshotCache } from './cache.js';
|
|
import { takeScreenshot, checkWebGL, closeBrowser, initialize } from './screenshot.js';
|
|
|
|
const PORT = parseInt(process.env.PORT || '8002', 10);
|
|
const APP_URL = process.env.APP_URL;
|
|
const CACHE_DIR = process.env.CACHE_DIR;
|
|
|
|
if (!APP_URL) {
|
|
console.error('Error: APP_URL environment variable is required');
|
|
process.exit(1);
|
|
}
|
|
|
|
if (!CACHE_DIR) {
|
|
console.error('Error: CACHE_DIR environment variable is required');
|
|
process.exit(1);
|
|
}
|
|
|
|
const cache = new ScreenshotCache(CACHE_DIR);
|
|
const app = express();
|
|
|
|
app.get('/health', (_req, res) => {
|
|
res.status(200).send('ok');
|
|
});
|
|
|
|
app.get('/debug', async (_req, res) => {
|
|
try {
|
|
const info = await checkWebGL();
|
|
res.json(info);
|
|
} catch (err) {
|
|
res.status(500).json({ error: String(err) });
|
|
}
|
|
});
|
|
|
|
app.get('/screenshot', async (req, res) => {
|
|
try {
|
|
const qs = new URLSearchParams();
|
|
for (const key of ['lat', 'lon', 'zoom', 'tab', 'og']) {
|
|
const val = req.query[key];
|
|
if (typeof val === 'string' && val) {
|
|
qs.set(key, val);
|
|
}
|
|
}
|
|
// Repeated params: filter, poi
|
|
for (const key of ['filter', 'poi']) {
|
|
const val = req.query[key];
|
|
if (typeof val === 'string' && val) {
|
|
qs.append(key, val);
|
|
} else if (Array.isArray(val)) {
|
|
for (const v of val) {
|
|
if (typeof v === 'string' && v) qs.append(key, v);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Extract path param (used for non-root pages like /invite/CODE)
|
|
const pagePath = typeof req.query.path === 'string' && req.query.path ? req.query.path : '/';
|
|
if (pagePath !== '/') qs.set('path', pagePath);
|
|
|
|
const cacheKey = cache.buildKey(qs);
|
|
qs.delete('path');
|
|
|
|
// Check cache first
|
|
const cached = cache.get(cacheKey);
|
|
if (cached) {
|
|
res.setHeader('Content-Type', 'image/jpeg');
|
|
res.setHeader('Cache-Control', 'public, max-age=86400');
|
|
res.setHeader('X-Cache', 'HIT');
|
|
cached.pipe(res);
|
|
return;
|
|
}
|
|
|
|
// Build the URL for the frontend in screenshot mode
|
|
qs.set('screenshot', '1');
|
|
const url = `${APP_URL}${pagePath}?${qs}`;
|
|
|
|
console.log(`Taking screenshot: ${url}`);
|
|
const jpeg = await takeScreenshot(url);
|
|
|
|
// Cache it
|
|
cache.set(cacheKey, jpeg);
|
|
|
|
res.setHeader('Content-Type', 'image/jpeg');
|
|
res.setHeader('Cache-Control', 'public, max-age=86400');
|
|
res.setHeader('X-Cache', 'MISS');
|
|
res.send(jpeg);
|
|
} catch (err) {
|
|
console.error('Screenshot failed:', err);
|
|
res.status(500).json({ error: 'Screenshot failed' });
|
|
}
|
|
});
|
|
|
|
const server = app.listen(PORT, () => {
|
|
console.log(`Screenshot service listening on port ${PORT}`);
|
|
console.log(` APP_URL: ${APP_URL}`);
|
|
console.log(` CACHE_DIR: ${CACHE_DIR}`);
|
|
|
|
// Pre-warm browser and populate network cache in background.
|
|
// The health endpoint is available immediately; screenshot requests
|
|
// during warm-up will still work (just slower on the first call).
|
|
initialize(APP_URL).catch((err) => {
|
|
console.error('Initialization failed:', err);
|
|
});
|
|
});
|
|
|
|
// Graceful shutdown
|
|
for (const signal of ['SIGTERM', 'SIGINT']) {
|
|
process.on(signal, async () => {
|
|
console.log(`Received ${signal}, shutting down...`);
|
|
server.close();
|
|
await closeBrowser();
|
|
process.exit(0);
|
|
});
|
|
}
|