| import { unwrapEnvelope } from './seed-envelope'; |
| import { getRpcNoStoreReasonFromPayload } from './cache-contract'; |
| import { buildUpstreamEvent, getUsageScope, sendToAxiom } from './usage'; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function parseTimeoutEnv(raw: string | undefined, defaultMs: number): number { |
| const parsed = Number.parseInt(raw ?? '', 10); |
| return parsed > 0 ? parsed : defaultMs; |
| } |
| export const REDIS_OP_TIMEOUT_MS = parseTimeoutEnv(process.env.REDIS_OP_TIMEOUT_MS, 1_500); |
| export const REDIS_PIPELINE_TIMEOUT_MS = parseTimeoutEnv(process.env.REDIS_PIPELINE_TIMEOUT_MS, 5_000); |
|
|
| function errMsg(err: unknown): string { |
| return err instanceof Error ? err.message : String(err); |
| } |
|
|
| function hasRemoteRedisConfig(): boolean { |
| return Boolean(process.env.UPSTASH_REDIS_REST_URL && process.env.UPSTASH_REDIS_REST_TOKEN); |
| } |
|
|
| |
| |
| |
| |
| export function getKeyPrefix(): string { |
| const env = process.env.VERCEL_ENV; |
| if (!env || env === 'production') return ''; |
| const sha = process.env.VERCEL_GIT_COMMIT_SHA?.slice(0, 8) || 'dev'; |
| return `${env}:${sha}:`; |
| } |
|
|
| let cachedPrefix: string | undefined; |
| function prefixKey(key: string): string { |
| if (cachedPrefix === undefined) cachedPrefix = getKeyPrefix(); |
| if (!cachedPrefix) return key; |
| return `${cachedPrefix}${key}`; |
| } |
|
|
| |
| |
| |
| export function __resetKeyPrefixCacheForTests(): void { |
| cachedPrefix = undefined; |
| } |
|
|
| export type CacheReadResult = { status: 'hit'; value: unknown } | { status: 'miss' } | { status: 'error'; error: unknown }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export async function readCachedJson(key: string, raw = false): Promise<CacheReadResult> { |
| if (process.env.LOCAL_API_MODE === 'tauri-sidecar') { |
| try { |
| const { sidecarCacheGet } = await import('./sidecar-cache'); |
| const value = sidecarCacheGet(key); |
| return value == null ? { status: 'miss' } : { status: 'hit', value }; |
| } catch (error) { |
| return { status: 'error', error }; |
| } |
| } |
|
|
| const url = process.env.UPSTASH_REDIS_REST_URL; |
| const token = process.env.UPSTASH_REDIS_REST_TOKEN; |
| if (!url || !token) return { status: 'miss' }; |
| try { |
| const finalKey = raw ? key : prefixKey(key); |
| const resp = await fetch(`${url}/get/${encodeURIComponent(finalKey)}`, { |
| headers: { Authorization: `Bearer ${token}` }, |
| signal: AbortSignal.timeout(REDIS_OP_TIMEOUT_MS), |
| }); |
| if (!resp.ok) throw new Error(`Redis HTTP ${resp.status}`); |
| const data = (await resp.json()) as { result?: string }; |
| if (!data.result) return { status: 'miss' }; |
| |
| |
| |
| return { |
| status: 'hit', |
| value: unwrapEnvelope(JSON.parse(data.result)).data, |
| }; |
| } catch (error) { |
| return { status: 'error', error }; |
| } |
| } |
|
|
| function logCacheReadError(key: string, err: unknown): void { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const isTimeout = err instanceof Error && (err.name === 'TimeoutError' || err.name === 'AbortError'); |
| if (isTimeout) { |
| console.error(`[REDIS-TIMEOUT] getCachedJson key=${key} timeoutMs=${REDIS_OP_TIMEOUT_MS}`); |
| } else { |
| console.warn('[redis] getCachedJson failed:', errMsg(err)); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| export async function getRawJson(key: string): Promise<unknown | null> { |
| if (process.env.LOCAL_API_MODE === 'tauri-sidecar') { |
| const { sidecarCacheGet } = await import('./sidecar-cache'); |
| return sidecarCacheGet(key); |
| } |
| const url = process.env.UPSTASH_REDIS_REST_URL; |
| const token = process.env.UPSTASH_REDIS_REST_TOKEN; |
| if (!url || !token) throw new Error('Redis credentials not configured'); |
| const resp = await fetch(`${url}/get/${encodeURIComponent(key)}`, { |
| headers: { Authorization: `Bearer ${token}` }, |
| signal: AbortSignal.timeout(REDIS_OP_TIMEOUT_MS), |
| }); |
| if (!resp.ok) throw new Error(`Redis HTTP ${resp.status}`); |
| const data = (await resp.json()) as { result?: string }; |
| if (!data.result) return null; |
| |
| |
| return unwrapEnvelope(JSON.parse(data.result)).data; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export async function getCachedRawString(key: string): Promise<string | null> { |
| if (process.env.LOCAL_API_MODE === 'tauri-sidecar') { |
| const { sidecarCacheGet } = await import('./sidecar-cache'); |
| const v = sidecarCacheGet(key); |
| return typeof v === 'string' ? v : null; |
| } |
| const url = process.env.UPSTASH_REDIS_REST_URL; |
| const token = process.env.UPSTASH_REDIS_REST_TOKEN; |
| if (!url || !token) return null; |
| try { |
| const resp = await fetch(`${url}/get/${encodeURIComponent(key)}`, { |
| headers: { Authorization: `Bearer ${token}` }, |
| signal: AbortSignal.timeout(REDIS_OP_TIMEOUT_MS), |
| }); |
| if (!resp.ok) return null; |
| const data = (await resp.json()) as { result?: string | null }; |
| return typeof data.result === 'string' && data.result.length > 0 ? data.result : null; |
| } catch (err) { |
| |
| |
| |
| const isTimeout = err instanceof Error && (err.name === 'TimeoutError' || err.name === 'AbortError'); |
| if (isTimeout) console.error(`[REDIS-TIMEOUT] getCachedRawString key=${key} timeoutMs=${REDIS_OP_TIMEOUT_MS}`); |
| else console.warn('[redis] getCachedRawString failed:', errMsg(err)); |
| return null; |
| } |
| } |
|
|
| export async function getCachedJson(key: string, raw = false): Promise<unknown | null> { |
| const read = await readCachedJson(key, raw); |
| if (read.status === 'hit') return read.value; |
| if (read.status === 'error') logCacheReadError(key, read.error); |
| return null; |
| } |
|
|
| export async function setCachedJson(key: string, value: unknown, ttlSeconds: number, raw = false): Promise<boolean> { |
| if (process.env.LOCAL_API_MODE === 'tauri-sidecar') { |
| const { sidecarCacheSet } = await import('./sidecar-cache'); |
| sidecarCacheSet(key, value, ttlSeconds); |
| return true; |
| } |
|
|
| const url = process.env.UPSTASH_REDIS_REST_URL; |
| const token = process.env.UPSTASH_REDIS_REST_TOKEN; |
| if (!url || !token) return false; |
| try { |
| const finalKey = raw ? key : prefixKey(key); |
| |
| |
| |
| |
| |
| |
| |
| |
| const resp = await fetch(`${url}/`, { |
| method: 'POST', |
| headers: { |
| Authorization: `Bearer ${token}`, |
| 'Content-Type': 'application/json', |
| }, |
| body: JSON.stringify(['SET', finalKey, JSON.stringify(value), 'EX', String(ttlSeconds)]), |
| signal: AbortSignal.timeout(REDIS_PIPELINE_TIMEOUT_MS), |
| }); |
| const data = (await resp.json().catch(() => null)) as { |
| result?: string; |
| error?: string; |
| } | null; |
| if (!resp.ok || data?.error) { |
| console.warn(`[redis] setCachedJson failed:`, data?.error ?? `HTTP ${resp.status}`); |
| return false; |
| } |
| return true; |
| } catch (err) { |
| console.warn('[redis] setCachedJson failed:', errMsg(err)); |
| return false; |
| } |
| } |
|
|
| const NEG_SENTINEL = '__WM_NEG__'; |
| const FETCH_ERROR_NEGATIVE_TTL_SECONDS = 30; |
| |
| const FETCH_ERROR_UNAVAILABLE_BACKOFF_SECONDS = 3; |
| const REDIS_FAILURE_POSITIVE_TTL_SECONDS = 30; |
| const LOCAL_FALLBACK_MAX_ENTRIES = 5000; |
|
|
| const localNegativeUntil = new Map<string, number>(); |
| |
| const localUnavailableUntil = new Map<string, number>(); |
| const localPositiveFallback = new Map<string, { value: unknown; expiresAt: number }>(); |
|
|
| function evictOldestLocalFallbackEntries<T>(map: Map<string, T>): void { |
| while (map.size > LOCAL_FALLBACK_MAX_ENTRIES) { |
| const oldestKey = map.keys().next().value; |
| if (oldestKey === undefined) return; |
| map.delete(oldestKey); |
| } |
| } |
|
|
| function effectiveFetchErrorNegativeTtlSeconds(negativeTtlSeconds: number): number { |
| return Math.max(1, Math.min(negativeTtlSeconds, FETCH_ERROR_NEGATIVE_TTL_SECONDS)); |
| } |
|
|
| function armLocalNegativeCooldown(key: string, ttlSeconds: number): void { |
| localNegativeUntil.set(key, Date.now() + ttlSeconds * 1000); |
| evictOldestLocalFallbackEntries(localNegativeUntil); |
| } |
|
|
| function hasLocalNegativeCooldown(key: string): boolean { |
| const expiresAt = localNegativeUntil.get(key); |
| if (expiresAt === undefined) return false; |
| if (expiresAt > Date.now()) return true; |
| localNegativeUntil.delete(key); |
| return false; |
| } |
|
|
| function armLocalUnavailableBackoff(key: string, ttlSeconds: number): void { |
| localUnavailableUntil.set(key, Date.now() + ttlSeconds * 1000); |
| evictOldestLocalFallbackEntries(localUnavailableUntil); |
| } |
|
|
| function hasLocalUnavailableBackoff(key: string): boolean { |
| const expiresAt = localUnavailableUntil.get(key); |
| if (expiresAt === undefined) return false; |
| if (expiresAt > Date.now()) return true; |
| localUnavailableUntil.delete(key); |
| return false; |
| } |
|
|
| |
| |
| export function __clearLocalUnavailableBackoffForTests(): void { |
| localUnavailableUntil.clear(); |
| } |
|
|
| function effectiveRedisFailurePositiveTtlSeconds(ttlSeconds: number): number { |
| return Math.max(1, Math.min(ttlSeconds, REDIS_FAILURE_POSITIVE_TTL_SECONDS)); |
| } |
|
|
| |
| |
| function armLocalPositiveFallback(key: string, value: unknown, ttlSeconds: number): void { |
| const effectiveTtlSeconds = effectiveRedisFailurePositiveTtlSeconds(ttlSeconds); |
| localPositiveFallback.set(key, { |
| value, |
| expiresAt: Date.now() + effectiveTtlSeconds * 1000, |
| }); |
| evictOldestLocalFallbackEntries(localPositiveFallback); |
| } |
|
|
| function readLocalPositiveFallback(key: string): unknown | undefined { |
| const cached = localPositiveFallback.get(key); |
| if (cached === undefined) return undefined; |
| if (cached.expiresAt > Date.now()) return cached.value; |
| localPositiveFallback.delete(key); |
| return undefined; |
| } |
|
|
| |
| |
| |
| |
| export async function getCachedJsonBatch(keys: string[], raw = false): Promise<Map<string, unknown>> { |
| const result = new Map<string, unknown>(); |
| if (keys.length === 0) return result; |
|
|
| if (process.env.LOCAL_API_MODE === 'tauri-sidecar') { |
| try { |
| const { sidecarCacheGet } = await import('./sidecar-cache'); |
| for (const key of keys) { |
| const value = sidecarCacheGet(key); |
| if (value != null) result.set(key, value); |
| } |
| } catch (error) { |
| console.warn('[redis] getCachedJsonBatch failed:', errMsg(error)); |
| } |
| return result; |
| } |
|
|
| const url = process.env.UPSTASH_REDIS_REST_URL; |
| const token = process.env.UPSTASH_REDIS_REST_TOKEN; |
| if (!url || !token) return result; |
|
|
| try { |
| const pipeline = keys.map((k) => ['GET', raw ? k : prefixKey(k)]); |
| const resp = await fetch(`${url}/pipeline`, { |
| method: 'POST', |
| headers: { |
| Authorization: `Bearer ${token}`, |
| 'Content-Type': 'application/json', |
| }, |
| body: JSON.stringify(pipeline), |
| signal: AbortSignal.timeout(REDIS_PIPELINE_TIMEOUT_MS), |
| }); |
| if (!resp.ok) { |
| console.warn(`[redis] getCachedJsonBatch HTTP ${resp.status}`); |
| return result; |
| } |
|
|
| const data = (await resp.json()) as Array<{ result?: string }>; |
| for (let i = 0; i < keys.length; i++) { |
| const rawResult = data[i]?.result; |
| if (rawResult) { |
| try { |
| const parsed = JSON.parse(rawResult); |
| if (parsed === NEG_SENTINEL) continue; |
| |
| |
| result.set(keys[i]!, unwrapEnvelope(parsed).data); |
| } catch { |
| |
| } |
| } |
| } |
| } catch (err) { |
| const isTimeout = err instanceof Error && (err.name === 'TimeoutError' || err.name === 'AbortError'); |
| if (isTimeout) { |
| console.error(`[REDIS-TIMEOUT] getCachedJsonBatch keys=${keys.length} timeoutMs=${REDIS_PIPELINE_TIMEOUT_MS}`); |
| } else { |
| console.warn('[redis] getCachedJsonBatch failed:', errMsg(err)); |
| } |
| } |
| return result; |
| } |
|
|
| export type RedisPipelineCommand = Array<string | number>; |
|
|
| function normalizePipelineCommand(command: RedisPipelineCommand, raw: boolean): RedisPipelineCommand { |
| if (raw || command.length < 2) return [...command]; |
| const [verb, key, ...rest] = command; |
| if (typeof verb !== 'string' || typeof key !== 'string') return [...command]; |
| return [verb, prefixKey(key), ...rest]; |
| } |
|
|
| export async function runRedisPipeline(commands: RedisPipelineCommand[], raw = false): Promise<Array<{ result?: unknown }>> { |
| if (process.env.LOCAL_API_MODE === 'tauri-sidecar') return []; |
| if (commands.length === 0) return []; |
|
|
| const url = process.env.UPSTASH_REDIS_REST_URL; |
| const token = process.env.UPSTASH_REDIS_REST_TOKEN; |
| if (!url || !token) return []; |
|
|
| try { |
| const response = await fetch(`${url}/pipeline`, { |
| method: 'POST', |
| headers: { |
| Authorization: `Bearer ${token}`, |
| 'Content-Type': 'application/json', |
| }, |
| body: JSON.stringify(commands.map((command) => normalizePipelineCommand(command, raw))), |
| signal: AbortSignal.timeout(REDIS_PIPELINE_TIMEOUT_MS), |
| }); |
| if (!response.ok) { |
| console.warn(`[redis] runRedisPipeline HTTP ${response.status}`); |
| return []; |
| } |
| return (await response.json()) as Array<{ result?: unknown }>; |
| } catch (err) { |
| console.warn('[redis] runRedisPipeline failed:', errMsg(err)); |
| return []; |
| } |
| } |
|
|
| export async function compareAndDeleteRedisKey(key: string, expectedValue: string, raw = false): Promise<boolean> { |
| if (process.env.LOCAL_API_MODE === 'tauri-sidecar') return false; |
| const url = process.env.UPSTASH_REDIS_REST_URL; |
| const token = process.env.UPSTASH_REDIS_REST_TOKEN; |
| if (!url || !token || !expectedValue) return false; |
|
|
| const finalKey = raw ? key : prefixKey(key); |
| const script = "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end"; |
| try { |
| const response = await fetch(`${url}/`, { |
| method: 'POST', |
| headers: { |
| Authorization: `Bearer ${token}`, |
| 'Content-Type': 'application/json', |
| }, |
| body: JSON.stringify(['EVAL', script, '1', finalKey, expectedValue]), |
| signal: AbortSignal.timeout(REDIS_PIPELINE_TIMEOUT_MS), |
| }); |
| if (!response.ok) { |
| console.warn(`[redis] compareAndDeleteRedisKey HTTP ${response.status}`); |
| return false; |
| } |
| const data = (await response.json().catch(() => null)) as { |
| result?: unknown; |
| error?: string; |
| } | null; |
| if (data?.error) { |
| console.warn('[redis] compareAndDeleteRedisKey failed:', data.error); |
| return false; |
| } |
| return data?.result === 1; |
| } catch (err) { |
| console.warn('[redis] compareAndDeleteRedisKey failed:', errMsg(err)); |
| return false; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| const inflight = new Map<string, Promise<unknown>>(); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const FETCHER_TIMEOUT_MS_DEFAULT = 30_000; |
| let fetcherTimeoutDefaultMs = FETCHER_TIMEOUT_MS_DEFAULT; |
|
|
| |
| |
| |
| export function __setFetcherTimeoutForTests(ms: number): void { |
| fetcherTimeoutDefaultMs = ms; |
| } |
| export function __resetFetcherTimeoutForTests(): void { |
| fetcherTimeoutDefaultMs = FETCHER_TIMEOUT_MS_DEFAULT; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function withFetcherTimeout<T>(promise: Promise<T>, key: string, timeoutMs: number, callerName: 'cachedFetchJson' | 'cachedFetchJsonWithMeta'): Promise<T> { |
| let timer: ReturnType<typeof setTimeout> | undefined; |
| const timeout = new Promise<never>((_, reject) => { |
| timer = setTimeout(() => { |
| reject(new Error(`${callerName} timeout after ${timeoutMs}ms for "${key}"`)); |
| }, timeoutMs); |
| }); |
| return Promise.race([promise, timeout]).finally(() => { |
| if (timer !== undefined) clearTimeout(timer); |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export interface CachedFetchOpts { |
| timeoutMs?: number; |
| cacheFetcherErrors?: boolean; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export async function cachedFetchJson<T extends object>( |
| key: string, |
| ttlSeconds: number, |
| fetcher: () => Promise<T | null>, |
| negativeTtlSeconds = 120, |
| opts?: CachedFetchOpts, |
| ): Promise<T | null> { |
| const cached = await readCachedJson(key); |
| if (cached.status === 'hit') { |
| if (cached.value === NEG_SENTINEL) return null; |
| return cached.value as T; |
| } |
| const localPositive = readLocalPositiveFallback(key); |
| if (localPositive !== undefined) return localPositive as T; |
| const hadCacheReadError = cached.status === 'error'; |
| if (cached.status === 'error') { |
| logCacheReadError(key, cached.error); |
| if (hasLocalNegativeCooldown(key)) return null; |
| } |
| |
| |
| if (hasLocalUnavailableBackoff(key)) { |
| throw new Error(`cachedFetchJson unavailable backoff active for "${key}"`); |
| } |
|
|
| const existing = inflight.get(key); |
| if (existing) return existing as Promise<T | null>; |
|
|
| const timeoutMs = opts?.timeoutMs ?? fetcherTimeoutDefaultMs; |
| const promise = withFetcherTimeout(fetcher(), key, timeoutMs, 'cachedFetchJson') |
| .then(async (result) => { |
| if (result != null) { |
| const noStoreReason = getRpcNoStoreReasonFromPayload(result, { includeAvailableFalse: false }); |
| if (noStoreReason) { |
| armLocalNegativeCooldown(key, negativeTtlSeconds); |
| await setCachedJson(key, NEG_SENTINEL, negativeTtlSeconds); |
| } else { |
| const wrote = await setCachedJson(key, result, ttlSeconds); |
| |
| |
| |
| if (hadCacheReadError || (!wrote && hasRemoteRedisConfig())) { |
| armLocalPositiveFallback(key, result, ttlSeconds); |
| } |
| } |
| } else { |
| armLocalNegativeCooldown(key, negativeTtlSeconds); |
| await setCachedJson(key, NEG_SENTINEL, negativeTtlSeconds); |
| } |
| return result; |
| }) |
| .catch(async (err: unknown) => { |
| if (opts?.cacheFetcherErrors !== false) { |
| const errorTtlSeconds = effectiveFetchErrorNegativeTtlSeconds(negativeTtlSeconds); |
| armLocalNegativeCooldown(key, errorTtlSeconds); |
| await setCachedJson(key, NEG_SENTINEL, errorTtlSeconds); |
| console.warn(`[redis] cachedFetchJson fetcher failed for "${key}":`, errMsg(err)); |
| } else { |
| |
| |
| armLocalUnavailableBackoff(key, FETCH_ERROR_UNAVAILABLE_BACKOFF_SECONDS); |
| } |
| throw err; |
| }) |
| .finally(() => { |
| inflight.delete(key); |
| }); |
|
|
| inflight.set(key, promise); |
| return promise; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export interface UsageHook { |
| provider: string; |
| operation?: string; |
| host?: string; |
| |
| ctx?: { waitUntil: (p: Promise<unknown>) => void }; |
| requestId?: string; |
| customerId?: string | null; |
| route?: string; |
| tier?: number; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export async function cachedFetchJsonWithMeta<T extends object>( |
| key: string, |
| ttlSeconds: number, |
| fetcher: () => Promise<T | null>, |
| negativeTtlSeconds = 120, |
| opts?: CachedFetchOpts & { |
| usage?: UsageHook; |
| shouldFetch?: () => boolean; |
| cacheFailures?: boolean; |
| inflightKey?: string; |
| }, |
| ): Promise<{ data: T | null; source: 'cache' | 'fresh' | 'skipped'; leader: boolean }> { |
| const cached = await readCachedJson(key); |
| if (cached.status === 'hit') { |
| if (cached.value === NEG_SENTINEL) return { data: null, source: 'cache', leader: false }; |
| return { data: cached.value as T, source: 'cache', leader: false }; |
| } |
| const localPositive = readLocalPositiveFallback(key); |
| if (localPositive !== undefined) return { data: localPositive as T, source: 'cache', leader: false }; |
| const hadCacheReadError = cached.status === 'error'; |
| if (cached.status === 'error') { |
| logCacheReadError(key, cached.error); |
| if (hasLocalNegativeCooldown(key)) return { data: null, source: 'cache', leader: false }; |
| } |
| if (hasLocalUnavailableBackoff(key)) { |
| throw new Error(`cachedFetchJsonWithMeta unavailable backoff active for "${key}"`); |
| } |
|
|
| const inflightKey = opts?.inflightKey ?? key; |
| const existing = inflight.get(inflightKey); |
| if (existing) { |
| const data = (await existing) as T | null; |
| return { data, source: 'fresh', leader: false }; |
| } |
|
|
| if (opts?.shouldFetch && !opts.shouldFetch()) { |
| return { data: null, source: 'skipped', leader: false }; |
| } |
|
|
| const fetchT0 = Date.now(); |
| let upstreamStatus = 0; |
| let cacheStatus: 'miss' | 'neg-sentinel' = 'miss'; |
|
|
| const timeoutMs = opts?.timeoutMs ?? fetcherTimeoutDefaultMs; |
| const promise = withFetcherTimeout(fetcher(), key, timeoutMs, 'cachedFetchJsonWithMeta') |
| .then(async (result) => { |
| |
| |
| |
| |
| |
| |
| if (result != null) { |
| const noStoreReason = getRpcNoStoreReasonFromPayload(result, { includeAvailableFalse: false }); |
| if (noStoreReason) { |
| upstreamStatus = 0; |
| if (opts?.cacheFailures !== false) { |
| cacheStatus = 'neg-sentinel'; |
| armLocalNegativeCooldown(key, negativeTtlSeconds); |
| await setCachedJson(key, NEG_SENTINEL, negativeTtlSeconds); |
| } |
| } else { |
| upstreamStatus = 200; |
| const wrote = await setCachedJson(key, result, ttlSeconds); |
| |
| |
| if (hadCacheReadError || (!wrote && hasRemoteRedisConfig())) { |
| armLocalPositiveFallback(key, result, ttlSeconds); |
| } |
| } |
| } else { |
| upstreamStatus = 0; |
| if (opts?.cacheFailures !== false) { |
| cacheStatus = 'neg-sentinel'; |
| armLocalNegativeCooldown(key, negativeTtlSeconds); |
| await setCachedJson(key, NEG_SENTINEL, negativeTtlSeconds); |
| } |
| } |
| return result; |
| }) |
| .catch(async (err: unknown) => { |
| upstreamStatus = 0; |
| if (opts?.cacheFailures === false) { |
| |
| } else if (opts?.cacheFetcherErrors !== false) { |
| cacheStatus = 'neg-sentinel'; |
| const errorTtlSeconds = effectiveFetchErrorNegativeTtlSeconds(negativeTtlSeconds); |
| armLocalNegativeCooldown(key, errorTtlSeconds); |
| await setCachedJson(key, NEG_SENTINEL, errorTtlSeconds); |
| console.warn(`[redis] cachedFetchJsonWithMeta fetcher failed for "${key}":`, errMsg(err)); |
| } else { |
| armLocalUnavailableBackoff(key, FETCH_ERROR_UNAVAILABLE_BACKOFF_SECONDS); |
| } |
| throw err; |
| }) |
| .finally(() => { |
| inflight.delete(inflightKey); |
| }); |
|
|
| inflight.set(inflightKey, promise); |
| let data: T | null; |
| try { |
| data = await promise; |
| } finally { |
| emitUpstreamFromHook(opts?.usage, upstreamStatus, Date.now() - fetchT0, cacheStatus); |
| } |
| return { data, source: 'fresh', leader: true }; |
| } |
|
|
| function emitUpstreamFromHook(usage: UsageHook | undefined, status: number, durationMs: number, cacheStatus: 'miss' | 'fresh' | 'stale-while-revalidate' | 'neg-sentinel'): void { |
| |
| if (!usage?.provider) return; |
| |
| |
| |
| const scope = getUsageScope(); |
| const ctx = usage.ctx ?? scope?.ctx; |
| if (!ctx) return; |
| const event = buildUpstreamEvent({ |
| requestId: usage.requestId ?? scope?.requestId ?? '', |
| customerId: usage.customerId ?? scope?.customerId ?? null, |
| route: usage.route ?? scope?.route ?? '', |
| tier: usage.tier ?? scope?.tier ?? 0, |
| provider: usage.provider, |
| operation: usage.operation ?? 'fetch', |
| host: usage.host ?? '', |
| status, |
| durationMs, |
| requestBytes: 0, |
| responseBytes: 0, |
| cacheStatus, |
| }); |
| try { |
| ctx.waitUntil(sendToAxiom([event])); |
| } catch { |
| |
| } |
| } |
|
|
| export async function geoSearchByBox(key: string, lon: number, lat: number, widthKm: number, heightKm: number, count: number, raw = false): Promise<string[]> { |
| const url = process.env.UPSTASH_REDIS_REST_URL; |
| const token = process.env.UPSTASH_REDIS_REST_TOKEN; |
| if (!url || !token) return []; |
| try { |
| const finalKey = raw ? key : prefixKey(key); |
| const pipeline = [['GEOSEARCH', finalKey, 'FROMLONLAT', String(lon), String(lat), 'BYBOX', String(widthKm), String(heightKm), 'km', 'ASC', 'COUNT', String(count)]]; |
| const resp = await fetch(`${url}/pipeline`, { |
| method: 'POST', |
| headers: { |
| Authorization: `Bearer ${token}`, |
| 'Content-Type': 'application/json', |
| }, |
| body: JSON.stringify(pipeline), |
| signal: AbortSignal.timeout(REDIS_PIPELINE_TIMEOUT_MS), |
| }); |
| if (!resp.ok) return []; |
| const data = (await resp.json()) as Array<{ result?: string[] }>; |
| return data[0]?.result ?? []; |
| } catch (err) { |
| console.warn('[redis] geoSearchByBox failed:', errMsg(err)); |
| return []; |
| } |
| } |
|
|
| export async function getHashFieldsBatch(key: string, fields: string[], raw = false): Promise<Map<string, string>> { |
| const result = new Map<string, string>(); |
| if (fields.length === 0) return result; |
| const url = process.env.UPSTASH_REDIS_REST_URL; |
| const token = process.env.UPSTASH_REDIS_REST_TOKEN; |
| if (!url || !token) return result; |
| try { |
| const finalKey = raw ? key : prefixKey(key); |
| const pipeline = [['HMGET', finalKey, ...fields]]; |
| const resp = await fetch(`${url}/pipeline`, { |
| method: 'POST', |
| headers: { |
| Authorization: `Bearer ${token}`, |
| 'Content-Type': 'application/json', |
| }, |
| body: JSON.stringify(pipeline), |
| signal: AbortSignal.timeout(REDIS_PIPELINE_TIMEOUT_MS), |
| }); |
| if (!resp.ok) return result; |
| const data = (await resp.json()) as Array<{ result?: (string | null)[] }>; |
| const values = data[0]?.result; |
| if (values) { |
| for (let i = 0; i < fields.length; i++) { |
| |
| |
| if (values[i] != null) result.set(fields[i]!, values[i]!); |
| } |
| } |
| } catch (err) { |
| console.warn('[redis] getHashFieldsBatch failed:', errMsg(err)); |
| } |
| return result; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export async function deleteRedisKey(key: string, raw = false): Promise<void> { |
| const url = process.env.UPSTASH_REDIS_REST_URL; |
| const token = process.env.UPSTASH_REDIS_REST_TOKEN; |
| if (!url || !token) return; |
|
|
| try { |
| const finalKey = raw ? key : prefixKey(key); |
| await fetch(`${url}/del/${encodeURIComponent(finalKey)}`, { |
| method: 'POST', |
| headers: { Authorization: `Bearer ${token}` }, |
| signal: AbortSignal.timeout(REDIS_OP_TIMEOUT_MS), |
| }); |
| } catch (err) { |
| console.warn('[redis] deleteRedisKey failed:', errMsg(err)); |
| } |
| } |
|
|