Files
worldmonitor/api/_upstash-json.js
Elie Habib ee8ca345cb refactor: consolidate Upstash helpers and extract DeckGL color config (#2465)
* refactor: consolidate Upstash helpers and extract DeckGL color config

Part 1 — api/_upstash-json.js: add getRedisCredentials, redisPipeline,
setCachedData exports. Migrate _oauth-token.js, reverse-geocode.js,
health.js, bootstrap.js, seed-health.js, and cache-purge.js off their
inline credential/pipeline boilerplate.

Part 2 — DeckGLMap.ts: extract getBaseColor, mineralColor, windColor,
TC_WIND_COLORS, CII_LEVEL_COLORS into src/config/ files so panels and
tests can reuse them without importing DeckGLMap.

Surfaced by reviewing nichm/worldmonitor-private fork.

* fix(mcp): restore throw-on-Redis-error in fetchOAuthToken; fix health error message

_oauth-token.js: readJsonFromUpstash returns null for HTTP errors, but
mcp.ts:702 relies on a throw to return 503 (retryable) vs null→401
(re-authenticate). Restore the explicit fetch that throws on !resp.ok,
using getRedisCredentials() for credential extraction.

health.js: the single null guard produced "Redis not configured" for
both missing creds and HTTP failures. Split into two checks so the
503 body correctly distinguishes env config problems from service
outages.

* fix(upstash): remove dead try/catch in reverse-geocode; atomic SET EX in setCachedData
2026-03-29 10:38:43 +04:00

67 lines
2.0 KiB
JavaScript

export async function readJsonFromUpstash(key, timeoutMs = 3_000) {
const url = process.env.UPSTASH_REDIS_REST_URL;
const token = process.env.UPSTASH_REDIS_REST_TOKEN;
if (!url || !token) return null;
const resp = await fetch(`${url}/get/${encodeURIComponent(key)}`, {
headers: { Authorization: `Bearer ${token}` },
signal: AbortSignal.timeout(timeoutMs),
});
if (!resp.ok) return null;
const data = await resp.json();
if (!data.result) return null;
try {
return JSON.parse(data.result);
} catch {
return null;
}
}
/** Returns Redis credentials or null if not configured. */
export function getRedisCredentials() {
const url = process.env.UPSTASH_REDIS_REST_URL;
const token = process.env.UPSTASH_REDIS_REST_TOKEN;
if (!url || !token) return null;
return { url, token };
}
/**
* Execute a batch of Redis commands via the Upstash pipeline endpoint.
* Returns null on missing credentials, HTTP error, or timeout.
* @param {Array<string[]>} commands - e.g. [['GET', 'key'], ['EXPIRE', 'key', '60']]
* @param {number} [timeoutMs=5000]
* @returns {Promise<Array<{ result: unknown }> | null>}
*/
export async function redisPipeline(commands, timeoutMs = 5_000) {
const creds = getRedisCredentials();
if (!creds) return null;
try {
const resp = await fetch(`${creds.url}/pipeline`, {
method: 'POST',
headers: { Authorization: `Bearer ${creds.token}`, 'Content-Type': 'application/json' },
body: JSON.stringify(commands),
signal: AbortSignal.timeout(timeoutMs),
});
if (!resp.ok) return null;
return await resp.json();
} catch {
return null;
}
}
/**
* Write a JSON value to Redis with a TTL (SET + EXPIRE as pipeline).
* @param {string} key
* @param {unknown} value - will be JSON.stringify'd
* @param {number} ttlSeconds
* @returns {Promise<boolean>} true on success
*/
export async function setCachedData(key, value, ttlSeconds) {
const results = await redisPipeline([
['SET', key, JSON.stringify(value), 'EX', String(ttlSeconds)],
]);
return results !== null;
}