mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
186 lines
4.5 KiB
JavaScript
186 lines
4.5 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 persistInFlight = false;
|
|
let persistQueued = false;
|
|
let loaded = false;
|
|
const MAX_PERSIST_ENTRIES = Math.max(100, Number(process.env.LOCAL_API_CACHE_PERSIST_MAX || 5000));
|
|
|
|
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 buildPersistSnapshot() {
|
|
const now = Date.now();
|
|
const payload = Object.create(null);
|
|
let kept = 0;
|
|
|
|
for (const [key, entry] of mem) {
|
|
if (!entry || entry.expiresAt <= now) continue;
|
|
payload[key] = entry;
|
|
kept += 1;
|
|
if (kept >= MAX_PERSIST_ENTRIES) break;
|
|
}
|
|
|
|
return payload;
|
|
}
|
|
|
|
async function persistToDisk() {
|
|
if (!persistPath) return;
|
|
if (persistInFlight) {
|
|
persistQueued = true;
|
|
return;
|
|
}
|
|
|
|
persistInFlight = true;
|
|
try {
|
|
const snapshot = buildPersistSnapshot();
|
|
const json = JSON.stringify(snapshot);
|
|
const { writeFile, rename } = await import('node:fs/promises');
|
|
const tmp = persistPath + '.tmp';
|
|
await writeFile(tmp, json, 'utf8');
|
|
await rename(tmp, persistPath);
|
|
} catch (err) {
|
|
console.warn('[Cache] Persist error:', err.message);
|
|
} finally {
|
|
persistInFlight = false;
|
|
if (persistQueued) {
|
|
persistQueued = false;
|
|
void persistToDisk();
|
|
}
|
|
}
|
|
}
|
|
|
|
function debouncedPersist() {
|
|
if (!persistPath) return;
|
|
clearTimeout(persistTimer);
|
|
persistTimer = setTimeout(() => {
|
|
void persistToDisk();
|
|
}, 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);
|
|
}
|