Files
worldmonitor/api/ucdp-events.js
Elie Habib c353cf2070 Reduce egress costs, add PWA support, fix Polymarket and Railway relay
Egress optimization:
- Add s-maxage + stale-while-revalidate to all API endpoints for Vercel CDN caching
- Add vercel.json with immutable caching for hashed assets
- Add gzip compression to sidecar responses >1KB
- Add gzip to Railway RSS responses (4 paths previously uncompressed)
- Increase polling intervals: markets/crypto 60s→120s, ETF/macro/stablecoins 60s→180s
- Remove hardcoded Railway URL from theater-posture.js (now env-var only)

PWA / Service Worker:
- Add vite-plugin-pwa with autoUpdate strategy
- Cache map tiles (CacheFirst), fonts (StaleWhileRevalidate), static assets
- NetworkOnly for all /api/* routes (real-time data must be fresh)
- Manual SW registration (web only, skip Tauri)
- Add offline fallback page
- Replace manual manifest with plugin-generated manifest

Polymarket fix:
- Route dev proxy through production Vercel (bypasses JA3 blocking)
- Add 4th fallback tier: production URL as absolute fallback

Desktop/Sidecar:
- Dual-backend cache (_upstash-cache.js): Redis cloud + in-memory+file desktop
- Settings window OK/Cancel redesign
- Runtime config and secret injection improvements
2026-02-14 19:53:04 +04:00

238 lines
7.4 KiB
JavaScript

import { getCorsHeaders, isDisallowedOrigin } from './_cors.js';
import { getCachedJson, setCachedJson } from './_upstash-cache.js';
import { recordCacheTelemetry } from './_cache-telemetry.js';
import { createIpRateLimiter } from './_ip-rate-limit.js';
export const config = { runtime: 'edge' };
const CACHE_KEY = 'ucdp:gedevents:v2';
const CACHE_TTL_SECONDS = 6 * 60 * 60;
const CACHE_TTL_MS = CACHE_TTL_SECONDS * 1000;
const UCDP_PAGE_SIZE = 1000;
const MAX_PAGES = 12;
const TRAILING_WINDOW_MS = 365 * 24 * 60 * 60 * 1000;
let fallbackCache = { data: null, timestamp: 0 };
const rateLimiter = createIpRateLimiter({
limit: 15,
windowMs: 60 * 1000,
maxEntries: 5000,
});
function getClientIp(req) {
return req.headers.get('x-forwarded-for')?.split(',')[0] ||
req.headers.get('x-real-ip') ||
'unknown';
}
function toErrorMessage(error) {
if (error instanceof Error) return error.message;
return String(error || 'unknown error');
}
function isValidResult(data) {
return Boolean(data && typeof data === 'object' && Array.isArray(data.data));
}
const VIOLENCE_TYPE_MAP = {
1: 'state-based',
2: 'non-state',
3: 'one-sided',
};
function parseDateMs(value) {
if (!value) return NaN;
return Date.parse(String(value));
}
function getMaxDateMs(events) {
let maxMs = NaN;
for (const event of events) {
const ms = parseDateMs(event?.date_start);
if (!Number.isFinite(ms)) continue;
if (!Number.isFinite(maxMs) || ms > maxMs) {
maxMs = ms;
}
}
return maxMs;
}
function buildVersionCandidates() {
const year = new Date().getFullYear() - 2000;
return Array.from(new Set([
`${year}.1`,
`${year - 1}.1`,
'25.1',
'24.1',
]));
}
async function fetchGedPage(version, page) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8000);
try {
const response = await fetch(
`https://ucdpapi.pcr.uu.se/api/gedevents/${version}?pagesize=${UCDP_PAGE_SIZE}&page=${page}`,
{ headers: { Accept: 'application/json' }, signal: controller.signal }
);
if (!response.ok) {
throw new Error(`UCDP GED API error (${version}, page ${page}): ${response.status}`);
}
return response.json();
} finally {
clearTimeout(timeout);
}
}
async function discoverGedVersion() {
const candidates = buildVersionCandidates();
for (const version of candidates) {
try {
const page0 = await fetchGedPage(version, 0);
if (Array.isArray(page0?.Result)) {
return { version, page0 };
}
} catch {
// Try the next version candidate.
}
}
throw new Error('Unable to fetch UCDP GED metadata from known API versions');
}
export default async function handler(req) {
const corsHeaders = getCorsHeaders(req, 'GET, OPTIONS');
if (req.method === 'OPTIONS') {
if (isDisallowedOrigin(req)) {
return new Response(null, { status: 403, headers: corsHeaders });
}
return new Response(null, { status: 204, headers: corsHeaders });
}
if (req.method !== 'GET') {
return Response.json({ error: 'Method not allowed', data: [] }, {
status: 405, headers: corsHeaders,
});
}
if (isDisallowedOrigin(req)) {
return Response.json({ error: 'Origin not allowed', data: [] }, {
status: 403, headers: corsHeaders,
});
}
const ip = getClientIp(req);
if (!rateLimiter.check(ip)) {
return Response.json({ error: 'Rate limited', data: [] }, {
status: 429,
headers: { ...corsHeaders, 'Retry-After': '60' },
});
}
const now = Date.now();
const cached = await getCachedJson(CACHE_KEY);
if (isValidResult(cached)) {
recordCacheTelemetry('/api/ucdp-events', 'REDIS-HIT');
return Response.json(cached, {
status: 200,
headers: { ...corsHeaders, 'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600', 'X-Cache': 'REDIS-HIT' },
});
}
if (isValidResult(fallbackCache.data) && now - fallbackCache.timestamp < CACHE_TTL_MS) {
recordCacheTelemetry('/api/ucdp-events', 'MEMORY-HIT');
return Response.json(fallbackCache.data, {
status: 200,
headers: { ...corsHeaders, 'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600', 'X-Cache': 'MEMORY-HIT' },
});
}
try {
const { version, page0 } = await discoverGedVersion();
const totalPages = Math.max(1, Number(page0?.TotalPages) || 1);
const newestPage = totalPages - 1;
let allEvents = [];
let latestDatasetMs = NaN;
for (let offset = 0; offset < MAX_PAGES && (newestPage - offset) >= 0; offset++) {
const page = newestPage - offset;
const rawData = page === 0 ? page0 : await fetchGedPage(version, page);
const events = Array.isArray(rawData?.Result) ? rawData.Result : [];
allEvents = allEvents.concat(events);
const pageMaxMs = getMaxDateMs(events);
if (!Number.isFinite(latestDatasetMs) && Number.isFinite(pageMaxMs)) {
latestDatasetMs = pageMaxMs;
}
// Pages are ordered oldest->newest; once we are fully outside trailing window, stop.
if (Number.isFinite(latestDatasetMs) && Number.isFinite(pageMaxMs)) {
const cutoffMs = latestDatasetMs - TRAILING_WINDOW_MS;
if (pageMaxMs < cutoffMs) {
break;
}
}
}
const sanitized = allEvents
.filter((event) => {
if (!Number.isFinite(latestDatasetMs)) return true;
const eventMs = parseDateMs(event?.date_start);
if (!Number.isFinite(eventMs)) return false;
return eventMs >= (latestDatasetMs - TRAILING_WINDOW_MS);
})
.map(e => ({
id: String(e.id || ''),
date_start: e.date_start || '',
date_end: e.date_end || '',
latitude: Number(e.latitude) || 0,
longitude: Number(e.longitude) || 0,
country: e.country || '',
side_a: (e.side_a || '').substring(0, 200),
side_b: (e.side_b || '').substring(0, 200),
deaths_best: Number(e.best) || 0,
deaths_low: Number(e.low) || 0,
deaths_high: Number(e.high) || 0,
type_of_violence: VIOLENCE_TYPE_MAP[e.type_of_violence] || 'state-based',
source_original: (e.source_original || '').substring(0, 300),
}))
.sort((a, b) => {
const bMs = parseDateMs(b.date_start);
const aMs = parseDateMs(a.date_start);
return (Number.isFinite(bMs) ? bMs : 0) - (Number.isFinite(aMs) ? aMs : 0);
});
const result = {
success: true,
count: sanitized.length,
data: sanitized,
version,
cached_at: new Date().toISOString(),
};
fallbackCache = { data: result, timestamp: now };
void setCachedJson(CACHE_KEY, result, CACHE_TTL_SECONDS);
recordCacheTelemetry('/api/ucdp-events', 'MISS');
return Response.json(result, {
status: 200,
headers: { ...corsHeaders, 'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600', 'X-Cache': 'MISS' },
});
} catch (error) {
if (isValidResult(fallbackCache.data)) {
recordCacheTelemetry('/api/ucdp-events', 'STALE');
return Response.json(fallbackCache.data, {
status: 200,
headers: { ...corsHeaders, 'Cache-Control': 'public, max-age=600, s-maxage=600, stale-while-revalidate=120', 'X-Cache': 'STALE' },
});
}
recordCacheTelemetry('/api/ucdp-events', 'ERROR');
return Response.json({ error: `Fetch failed: ${toErrorMessage(error)}`, data: [] }, {
status: 500, headers: corsHeaders,
});
}
}