Files
worldmonitor/api/_upstash-cache.js

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);
}