mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-05-14 11:06:21 +02:00
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
149 lines
3.7 KiB
JavaScript
149 lines
3.7 KiB
JavaScript
const isSidecar = (process.env.LOCAL_API_MODE || '').includes('sidecar');
|
|
|
|
// ── In-memory cache (desktop/sidecar) ──
|
|
const mem = new Map();
|
|
let persistPath = null;
|
|
let persistTimer = null;
|
|
let loaded = false;
|
|
|
|
async function ensureDesktopCache() {
|
|
if (loaded) return;
|
|
loaded = true;
|
|
try {
|
|
const { join } = await import('node:path');
|
|
const { readFileSync } = await import('node:fs');
|
|
const dir = process.env.LOCAL_API_RESOURCE_DIR || '.';
|
|
persistPath = join(dir, 'api-cache.json');
|
|
const data = JSON.parse(readFileSync(persistPath, 'utf8'));
|
|
const now = Date.now();
|
|
for (const [k, entry] of Object.entries(data)) {
|
|
if (entry.expiresAt > now) mem.set(k, entry);
|
|
}
|
|
console.log(`[Cache] Loaded ${mem.size} entries from disk`);
|
|
} catch {
|
|
// File doesn't exist yet
|
|
}
|
|
setInterval(() => {
|
|
const now = Date.now();
|
|
for (const [k, v] of mem) {
|
|
if (v.expiresAt <= now) mem.delete(k);
|
|
}
|
|
}, 60_000).unref?.();
|
|
}
|
|
|
|
function debouncedPersist() {
|
|
if (!persistPath) return;
|
|
clearTimeout(persistTimer);
|
|
persistTimer = setTimeout(async () => {
|
|
try {
|
|
const { writeFileSync, renameSync } = await import('node:fs');
|
|
const tmp = persistPath + '.tmp';
|
|
writeFileSync(tmp, JSON.stringify(Object.fromEntries(mem)));
|
|
renameSync(tmp, persistPath);
|
|
} catch (err) {
|
|
console.warn('[Cache] Persist error:', err.message);
|
|
}
|
|
}, 2000);
|
|
if (persistTimer?.unref) persistTimer.unref();
|
|
}
|
|
|
|
// ── Redis (cloud/Vercel) ──
|
|
let RedisClass = null;
|
|
let redis = null;
|
|
let redisInitFailed = false;
|
|
|
|
export async function getRedis() {
|
|
if (isSidecar) return null;
|
|
if (redis) return redis;
|
|
if (redisInitFailed) return null;
|
|
|
|
const url = process.env.UPSTASH_REDIS_REST_URL;
|
|
const token = process.env.UPSTASH_REDIS_REST_TOKEN;
|
|
if (!url || !token) return null;
|
|
|
|
try {
|
|
if (!RedisClass) {
|
|
const mod = await import('@upstash/redis');
|
|
RedisClass = mod.Redis;
|
|
}
|
|
redis = new RedisClass({ url, token });
|
|
return redis;
|
|
} catch (err) {
|
|
redisInitFailed = true;
|
|
console.warn('[Cache] Redis init failed:', err.message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// ── Shared API ──
|
|
|
|
export async function getCachedJson(key) {
|
|
if (isSidecar) {
|
|
await ensureDesktopCache();
|
|
const entry = mem.get(key);
|
|
if (!entry) return null;
|
|
if (entry.expiresAt <= Date.now()) {
|
|
mem.delete(key);
|
|
return null;
|
|
}
|
|
return entry.value;
|
|
}
|
|
|
|
const r = await getRedis();
|
|
if (!r) return null;
|
|
try {
|
|
return await r.get(key);
|
|
} catch (err) {
|
|
console.warn('[Cache] Read failed:', err.message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function setCachedJson(key, value, ttlSeconds) {
|
|
if (isSidecar) {
|
|
await ensureDesktopCache();
|
|
mem.set(key, { value, expiresAt: Date.now() + ttlSeconds * 1000 });
|
|
debouncedPersist();
|
|
return true;
|
|
}
|
|
|
|
const r = await getRedis();
|
|
if (!r) return false;
|
|
try {
|
|
await r.set(key, value, { ex: ttlSeconds });
|
|
return true;
|
|
} catch (err) {
|
|
console.warn('[Cache] Write failed:', err.message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export async function mget(...keys) {
|
|
if (isSidecar) {
|
|
await ensureDesktopCache();
|
|
const now = Date.now();
|
|
return keys.map(k => {
|
|
const entry = mem.get(k);
|
|
if (!entry || entry.expiresAt <= now) return null;
|
|
return entry.value;
|
|
});
|
|
}
|
|
|
|
const r = await getRedis();
|
|
if (!r) return keys.map(() => null);
|
|
try {
|
|
return await r.mget(...keys);
|
|
} catch (err) {
|
|
console.warn('[Cache] mget failed:', err.message);
|
|
return keys.map(() => null);
|
|
}
|
|
}
|
|
|
|
export function hashString(input) {
|
|
let hash = 5381;
|
|
for (let i = 0; i < input.length; i++) {
|
|
hash = ((hash << 5) + hash) + input.charCodeAt(i);
|
|
}
|
|
return (hash >>> 0).toString(36);
|
|
}
|